Align AlertSearch authorization uploads with the wakeup-service contract: UTC-day delegated JWTs and required notifyHourUtc/notifyMinuteUtc.
This commit is contained in:
@@ -1,18 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Batch of per-local-day delegated JWTs for notify-api / wakeup-service.
|
* Batch of per-UTC-day delegated JWTs for notify-api / wakeup-service.
|
||||||
*
|
*
|
||||||
* Not authentication JWTs, not alertSearch cursor ULIDs, and not the native
|
* Not authentication JWTs, not alertSearch cursor ULIDs, and not the native
|
||||||
* background prefetch pool (`mintBackgroundJwtTokenPool`).
|
* background prefetch pool (`mintBackgroundJwtTokenPool`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface DelegatedNotificationJwtWindow {
|
export interface DelegatedNotificationJwtWindow {
|
||||||
/** 1-based; 1 is today in the user's timezone. */
|
/** 1-based; 1 is the UTC calendar day of `now`. */
|
||||||
sequence: number;
|
sequence: number;
|
||||||
/** Calendar date of this slot, YYYY-MM-DD in the user's timezone. */
|
/** Calendar date of this slot, YYYY-MM-DD in UTC. */
|
||||||
localDate: string;
|
utcDay: string;
|
||||||
/** Unix seconds at 00:00:00 of this local day. */
|
/** Unix seconds at 00:00:00Z of this UTC day. */
|
||||||
nbf: number;
|
nbf: number;
|
||||||
/** Unix seconds at 00:00:00 of the following local day. */
|
/** Unix seconds at 00:00:00Z of the following UTC day. */
|
||||||
exp: number;
|
exp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* - Authentication JWT: short-lived access token (`iss`/`iat`/`exp`) sent as
|
* - Authentication JWT: short-lived access token (`iss`/`iat`/`exp`) sent as
|
||||||
* `Authorization: Bearer` for interactive API calls (`accessToken` /
|
* `Authorization: Bearer` for interactive API calls (`accessToken` /
|
||||||
* `getHeaders`). Identifies the requester DID.
|
* `getHeaders`). Identifies the requester DID.
|
||||||
* - Delegated notification JWT: 100 per-local-day tokens from
|
* - Delegated notification JWT: 100 per-UTC-day tokens from
|
||||||
* `mintDelegatedNotificationJwtBatch` for notify-api / wakeup-service.
|
* `mintDelegatedNotificationJwtBatch` for notify-api / wakeup-service.
|
||||||
* - Native background pool: `mintBackgroundJwtTokenPool` for daily-notification
|
* - Native background pool: `mintBackgroundJwtTokenPool` for daily-notification
|
||||||
* plugin prefetch. Unrelated to alertSearch and to the delegated batch.
|
* plugin prefetch. Unrelated to alertSearch and to the delegated batch.
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
import { DELEGATED_NOTIFICATION_JWT_COUNT } from "@/constants/delegatedNotificationJwt";
|
||||||
|
import {
|
||||||
|
buildDelegatedNotificationJwtWindows,
|
||||||
|
mintDelegatedNotificationJwtBatch,
|
||||||
|
} from "@/libs/delegatedNotificationJwt";
|
||||||
|
|
||||||
|
jest.mock("@/libs/util", () => ({
|
||||||
|
retrieveAccountMetadata: jest.fn().mockResolvedValue({}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@/libs/crypto/vc", () => ({
|
||||||
|
isFromPasskey: jest.fn().mockReturnValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@/libs/endorserServer", () => ({
|
||||||
|
createEndorserJwtForDid: jest.fn(
|
||||||
|
async (_did: string, claims: { jti?: string }) =>
|
||||||
|
`signed:${String(claims.jti)}`,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const FIXED_NOW = new Date("2026-09-10T16:00:00.000Z");
|
||||||
|
const UTC_DAY_0 = "2026-09-10";
|
||||||
|
const NBF_0 = Math.floor(Date.UTC(2026, 8, 10) / 1000);
|
||||||
|
const EXP_0 = Math.floor(Date.UTC(2026, 8, 11) / 1000);
|
||||||
|
|
||||||
|
describe("buildDelegatedNotificationJwtWindows", () => {
|
||||||
|
it("builds 100 consecutive UTC calendar-day windows from a fixed now", () => {
|
||||||
|
const windows = buildDelegatedNotificationJwtWindows(
|
||||||
|
DELEGATED_NOTIFICATION_JWT_COUNT,
|
||||||
|
"Asia/Manila",
|
||||||
|
FIXED_NOW,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(windows).toHaveLength(DELEGATED_NOTIFICATION_JWT_COUNT);
|
||||||
|
expect(windows[0]!.sequence).toBe(1);
|
||||||
|
expect(windows[0]!.utcDay).toBe(UTC_DAY_0);
|
||||||
|
expect(windows[0]!.nbf).toBe(NBF_0);
|
||||||
|
expect(windows[0]!.exp).toBe(EXP_0);
|
||||||
|
expect(windows[0]!.exp - windows[0]!.nbf).toBe(86_400);
|
||||||
|
|
||||||
|
const manilaLocalDate = DateTime.fromJSDate(FIXED_NOW, {
|
||||||
|
zone: "Asia/Manila",
|
||||||
|
}).toFormat("yyyy-LL-dd");
|
||||||
|
expect(manilaLocalDate).toBe("2026-09-11");
|
||||||
|
expect(windows[0]!.utcDay).not.toBe(manilaLocalDate);
|
||||||
|
|
||||||
|
for (let i = 0; i < windows.length; i++) {
|
||||||
|
const window = windows[i]!;
|
||||||
|
const expectedDay = DateTime.fromMillis(FIXED_NOW.getTime(), {
|
||||||
|
zone: "utc",
|
||||||
|
})
|
||||||
|
.startOf("day")
|
||||||
|
.plus({ days: i });
|
||||||
|
expect(window.sequence).toBe(i + 1);
|
||||||
|
expect(window.utcDay).toBe(expectedDay.toFormat("yyyy-LL-dd"));
|
||||||
|
expect(window.utcDay).toBe(
|
||||||
|
new Date(window.nbf * 1000).toISOString().slice(0, 10),
|
||||||
|
);
|
||||||
|
expect(window.nbf).toBe(Math.floor(expectedDay.toSeconds()));
|
||||||
|
expect(window.exp).toBe(
|
||||||
|
Math.floor(expectedDay.plus({ days: 1 }).toSeconds()),
|
||||||
|
);
|
||||||
|
expect(window.exp - window.nbf).toBe(86_400);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(windows[windows.length - 1]!.sequence).toBe(100);
|
||||||
|
expect(windows[windows.length - 1]!.utcDay).toBe("2026-12-18");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mintDelegatedNotificationJwtBatch jti", () => {
|
||||||
|
it("uses the UTC calendar day in jti even when the device zone is Asia/Manila", async () => {
|
||||||
|
const { createEndorserJwtForDid } = jest.requireMock(
|
||||||
|
"@/libs/endorserServer",
|
||||||
|
) as {
|
||||||
|
createEndorserJwtForDid: jest.Mock;
|
||||||
|
};
|
||||||
|
createEndorserJwtForDid.mockClear();
|
||||||
|
|
||||||
|
const did = `did:ethr:0x${"a".repeat(40)}`;
|
||||||
|
const batch = await mintDelegatedNotificationJwtBatch(did, {
|
||||||
|
timeZone: "Asia/Manila",
|
||||||
|
now: FIXED_NOW,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(batch.tokens[0]!.utcDay).toBe(UTC_DAY_0);
|
||||||
|
expect(createEndorserJwtForDid).toHaveBeenCalled();
|
||||||
|
const firstClaims = createEndorserJwtForDid.mock.calls[0]![1] as {
|
||||||
|
jti: string;
|
||||||
|
nbf: number;
|
||||||
|
exp: number;
|
||||||
|
};
|
||||||
|
expect(firstClaims.jti).toBe(`${did}#delegated-notify#${UTC_DAY_0}`);
|
||||||
|
expect(firstClaims.jti).not.toContain("2026-09-11");
|
||||||
|
expect(firstClaims.nbf).toBe(NBF_0);
|
||||||
|
expect(firstClaims.exp).toBe(EXP_0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* Mint 100 delegated notification JWTs (one local day each) for notify-api.
|
* Mint 100 delegated notification JWTs (one UTC calendar day each) for notify-api.
|
||||||
*
|
*
|
||||||
* JWT kinds (do not mix):
|
* JWT kinds (do not mix):
|
||||||
* - Authentication JWT: short-lived `accessToken` / `getHeaders` Bearer for the
|
* - Authentication JWT: short-lived `accessToken` / `getHeaders` Bearer for the
|
||||||
* setup request itself (not generated here).
|
* setup request itself (not generated here).
|
||||||
* - Delegated notification JWT: this module. Signed like other Endorser JWTs
|
* - Delegated notification JWT: this module. Signed like other Endorser JWTs
|
||||||
* (`createEndorserJwtForDid`). `nbf`/`exp` are that local day's bounds.
|
* (`createEndorserJwtForDid`). `nbf`/`exp` are that UTC day's bounds.
|
||||||
* Sequence is array order: index 0 / sequence 1 = today.
|
* Sequence is array order: index 0 / sequence 1 = the UTC calendar day of `now`.
|
||||||
* - Native background pool: `mintBackgroundJwtTokenPool` — unchanged, unused here.
|
* - Native background pool: `mintBackgroundJwtTokenPool` — unchanged, unused here.
|
||||||
* - alertSearch cursor ULID: server record id, not a signed token.
|
* - alertSearch cursor ULID: server record id, not a signed token.
|
||||||
*
|
*
|
||||||
* Timezone: device IANA zone via Luxon `DateTime.local().zoneName` (same source
|
* Timezone: device IANA zone via Luxon `DateTime.local().zoneName` (same source
|
||||||
* as project create/edit). Not persisted in settings. Pass `timeZone` to override.
|
* as project create/edit). Stored on the batch for the wakeup-service optional
|
||||||
|
* `timezone` field; it does not change the UTC-day JWT windows. Pass `timeZone`
|
||||||
|
* to override.
|
||||||
*
|
*
|
||||||
* Passkey (JWANT) identities cannot carry per-day nbf/exp; minting throws.
|
* Passkey (JWANT) identities cannot carry per-day nbf/exp; minting throws.
|
||||||
*/
|
*/
|
||||||
@@ -43,23 +45,25 @@ export function resolveUserTimeZone(timeZone?: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Local-day [nbf, exp) windows for sequence 1..count.
|
* UTC-day [nbf, exp) windows for sequence 1..count.
|
||||||
* DST is handled by Luxon startOf('day') in the given IANA zone.
|
* `timeZone` is accepted for call-site compatibility and is not used for bounds.
|
||||||
*/
|
*/
|
||||||
export function buildDelegatedNotificationJwtWindows(
|
export function buildDelegatedNotificationJwtWindows(
|
||||||
count: number = DELEGATED_NOTIFICATION_JWT_COUNT,
|
count: number = DELEGATED_NOTIFICATION_JWT_COUNT,
|
||||||
timeZone?: string,
|
timeZone?: string,
|
||||||
now: Date = new Date(),
|
now: Date = new Date(),
|
||||||
): DelegatedNotificationJwtWindow[] {
|
): DelegatedNotificationJwtWindow[] {
|
||||||
const zone = resolveUserTimeZone(timeZone);
|
if (timeZone !== undefined) {
|
||||||
const todayStart = DateTime.fromJSDate(now, { zone }).startOf("day");
|
resolveUserTimeZone(timeZone);
|
||||||
|
}
|
||||||
|
const todayStart = DateTime.fromJSDate(now, { zone: "utc" }).startOf("day");
|
||||||
const windows: DelegatedNotificationJwtWindow[] = [];
|
const windows: DelegatedNotificationJwtWindow[] = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const dayStart = todayStart.plus({ days: i }).startOf("day");
|
const dayStart = todayStart.plus({ days: i });
|
||||||
const nextStart = dayStart.plus({ days: 1 }).startOf("day");
|
const nextStart = dayStart.plus({ days: 1 });
|
||||||
windows.push({
|
windows.push({
|
||||||
sequence: i + 1,
|
sequence: i + 1,
|
||||||
localDate: dayStart.toFormat("yyyy-LL-dd"),
|
utcDay: dayStart.toFormat("yyyy-LL-dd"),
|
||||||
nbf: Math.floor(dayStart.toSeconds()),
|
nbf: Math.floor(dayStart.toSeconds()),
|
||||||
exp: Math.floor(nextStart.toSeconds()),
|
exp: Math.floor(nextStart.toSeconds()),
|
||||||
});
|
});
|
||||||
@@ -104,7 +108,7 @@ export async function mintDelegatedNotificationJwtBatch(
|
|||||||
iat: mintedAtEpoch,
|
iat: mintedAtEpoch,
|
||||||
nbf: window.nbf,
|
nbf: window.nbf,
|
||||||
exp: window.exp,
|
exp: window.exp,
|
||||||
jti: `${did}#delegated-notify#${window.localDate}`,
|
jti: `${did}#delegated-notify#${window.utcDay}`,
|
||||||
});
|
});
|
||||||
tokens.push({ ...window, jwt });
|
tokens.push({ ...window, jwt });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
type AlertAuthorizationDependencies,
|
type AlertAuthorizationDependencies,
|
||||||
uploadAlertSearchAuthorization,
|
uploadAlertSearchAuthorization,
|
||||||
} from "./alertAuthorization";
|
} from "./alertAuthorization";
|
||||||
|
import { alertSearchNotifyTimeUtcFromLocalNineAm } from "./alertSearchNotifyTime";
|
||||||
|
|
||||||
jest.mock("@/libs/crypto/vc", () => ({
|
jest.mock("@/libs/crypto/vc", () => ({
|
||||||
ETHR_DID_PREFIX: "did:ethr:",
|
ETHR_DID_PREFIX: "did:ethr:",
|
||||||
@@ -36,7 +37,7 @@ function createMintedBatch(): DelegatedNotificationJwtBatch {
|
|||||||
{ length: DELEGATED_NOTIFICATION_JWT_COUNT },
|
{ length: DELEGATED_NOTIFICATION_JWT_COUNT },
|
||||||
(_, index) => ({
|
(_, index) => ({
|
||||||
sequence: index + 1,
|
sequence: index + 1,
|
||||||
localDate: new Date(Date.UTC(2026, 8, index + 1))
|
utcDay: new Date(Date.UTC(2026, 8, index + 1))
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.slice(0, 10),
|
.slice(0, 10),
|
||||||
nbf: 1_788_220_800 + index * 86_400,
|
nbf: 1_788_220_800 + index * 86_400,
|
||||||
@@ -132,6 +133,8 @@ describe("uploadAlertSearchAuthorization", () => {
|
|||||||
|
|
||||||
const body = JSON.parse(String(init.body)) as {
|
const body = JSON.parse(String(init.body)) as {
|
||||||
batchId: string;
|
batchId: string;
|
||||||
|
notifyHourUtc: number;
|
||||||
|
notifyMinuteUtc: number;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
jwts: Array<{
|
jwts: Array<{
|
||||||
sequence: number;
|
sequence: number;
|
||||||
@@ -141,7 +144,16 @@ describe("uploadAlertSearchAuthorization", () => {
|
|||||||
jwt: string;
|
jwt: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
const expectedNotify = alertSearchNotifyTimeUtcFromLocalNineAm(TIME_ZONE);
|
||||||
expect(body.batchId).toBe(BATCH_ID);
|
expect(body.batchId).toBe(BATCH_ID);
|
||||||
|
expect(body.notifyHourUtc).toBe(expectedNotify.notifyHourUtc);
|
||||||
|
expect(body.notifyMinuteUtc).toBe(expectedNotify.notifyMinuteUtc);
|
||||||
|
expect(Number.isInteger(body.notifyHourUtc)).toBe(true);
|
||||||
|
expect(Number.isInteger(body.notifyMinuteUtc)).toBe(true);
|
||||||
|
expect(body.notifyHourUtc).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(body.notifyHourUtc).toBeLessThanOrEqual(23);
|
||||||
|
expect(body.notifyMinuteUtc).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(body.notifyMinuteUtc).toBeLessThanOrEqual(59);
|
||||||
expect(body.timezone).toBe(TIME_ZONE);
|
expect(body.timezone).toBe(TIME_ZONE);
|
||||||
expect(body.jwts).toHaveLength(DELEGATED_NOTIFICATION_JWT_COUNT);
|
expect(body.jwts).toHaveLength(DELEGATED_NOTIFICATION_JWT_COUNT);
|
||||||
expect(body.jwts.map((entry) => entry.sequence)).toEqual(
|
expect(body.jwts.map((entry) => entry.sequence)).toEqual(
|
||||||
@@ -155,7 +167,7 @@ describe("uploadAlertSearchAuthorization", () => {
|
|||||||
);
|
);
|
||||||
expect(body.jwts[0]).toEqual({
|
expect(body.jwts[0]).toEqual({
|
||||||
sequence: 1,
|
sequence: 1,
|
||||||
day: createMintedBatch().tokens[0]!.localDate,
|
day: createMintedBatch().tokens[0]!.utcDay,
|
||||||
nbf: createMintedBatch().tokens[0]!.nbf,
|
nbf: createMintedBatch().tokens[0]!.nbf,
|
||||||
exp: createMintedBatch().tokens[0]!.exp,
|
exp: createMintedBatch().tokens[0]!.exp,
|
||||||
jwt: createMintedBatch().tokens[0]!.jwt,
|
jwt: createMintedBatch().tokens[0]!.jwt,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { DELEGATED_NOTIFICATION_JWT_COUNT } from "@/constants/delegatedNotificat
|
|||||||
import type { DelegatedNotificationJwtBatch } from "@/interfaces/delegatedNotificationJwt";
|
import type { DelegatedNotificationJwtBatch } from "@/interfaces/delegatedNotificationJwt";
|
||||||
import { ETHR_DID_PREFIX } from "@/libs/crypto/vc";
|
import { ETHR_DID_PREFIX } from "@/libs/crypto/vc";
|
||||||
import { mintDelegatedNotificationJwtBatch } from "@/libs/delegatedNotificationJwt";
|
import { mintDelegatedNotificationJwtBatch } from "@/libs/delegatedNotificationJwt";
|
||||||
|
import { alertSearchNotifyTimeUtcFromLocalNineAm } from "./alertSearchNotifyTime";
|
||||||
import { getNotificationApiBaseUrl } from "./NotificationDebugConfig";
|
import { getNotificationApiBaseUrl } from "./NotificationDebugConfig";
|
||||||
import {
|
import {
|
||||||
getActiveNotificationDid,
|
getActiveNotificationDid,
|
||||||
@@ -12,6 +13,8 @@ import { shouldBypassNotificationAuth } from "./notificationApiDebugMode";
|
|||||||
|
|
||||||
export interface AlertAuthorizationRequestBody {
|
export interface AlertAuthorizationRequestBody {
|
||||||
batchId: string;
|
batchId: string;
|
||||||
|
notifyHourUtc: number;
|
||||||
|
notifyMinuteUtc: number;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
jwts: Array<{
|
jwts: Array<{
|
||||||
sequence: number;
|
sequence: number;
|
||||||
@@ -82,12 +85,16 @@ function buildRequestBody(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notifyTime = alertSearchNotifyTimeUtcFromLocalNineAm(batch.timeZone);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
batchId,
|
batchId,
|
||||||
|
notifyHourUtc: notifyTime.notifyHourUtc,
|
||||||
|
notifyMinuteUtc: notifyTime.notifyMinuteUtc,
|
||||||
timezone: batch.timeZone,
|
timezone: batch.timeZone,
|
||||||
jwts: batch.tokens.map((slot) => ({
|
jwts: batch.tokens.map((slot) => ({
|
||||||
sequence: slot.sequence,
|
sequence: slot.sequence,
|
||||||
day: slot.localDate,
|
day: slot.utcDay,
|
||||||
nbf: slot.nbf,
|
nbf: slot.nbf,
|
||||||
exp: slot.exp,
|
exp: slot.exp,
|
||||||
jwt: slot.jwt,
|
jwt: slot.jwt,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { alertSearchNotifyTimeUtcFromLocalNineAm } from "./alertSearchNotifyTime";
|
||||||
|
|
||||||
|
describe("alertSearchNotifyTimeUtcFromLocalNineAm", () => {
|
||||||
|
it("converts 09:00 Asia/Manila to 01:00 UTC", () => {
|
||||||
|
const result = alertSearchNotifyTimeUtcFromLocalNineAm(
|
||||||
|
"Asia/Manila",
|
||||||
|
new Date("2026-09-10T16:00:00.000Z"),
|
||||||
|
);
|
||||||
|
expect(result).toEqual({ notifyHourUtc: 1, notifyMinuteUtc: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns integers in the UTC hour/minute ranges", () => {
|
||||||
|
const result = alertSearchNotifyTimeUtcFromLocalNineAm(
|
||||||
|
"America/Denver",
|
||||||
|
new Date("2026-01-15T12:00:00.000Z"),
|
||||||
|
);
|
||||||
|
expect(Number.isInteger(result.notifyHourUtc)).toBe(true);
|
||||||
|
expect(Number.isInteger(result.notifyMinuteUtc)).toBe(true);
|
||||||
|
expect(result.notifyHourUtc).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(result.notifyHourUtc).toBeLessThanOrEqual(23);
|
||||||
|
expect(result.notifyMinuteUtc).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(result.notifyMinuteUtc).toBeLessThanOrEqual(59);
|
||||||
|
expect(result).toEqual({ notifyHourUtc: 16, notifyMinuteUtc: 0 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Debug/E2E AlertSearch notify-time conversion only.
|
||||||
|
* Not a user-facing setting; does not read Daily Reminder or New Activity times.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
/** Temporary default wall-clock time for AlertSearch authorization uploads. */
|
||||||
|
export const ALERT_SEARCH_DEBUG_LOCAL_NOTIFY_HOUR = 9;
|
||||||
|
export const ALERT_SEARCH_DEBUG_LOCAL_NOTIFY_MINUTE = 0;
|
||||||
|
|
||||||
|
function requireIanaTimeZone(timeZone: string): string {
|
||||||
|
const probe = DateTime.now().setZone(timeZone);
|
||||||
|
if (!timeZone || !probe.isValid) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid IANA timezone for AlertSearch notify time: " + timeZone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return timeZone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert 09:00 in `timeZone` (IANA) on the calendar day of `now` to UTC hour/minute.
|
||||||
|
*/
|
||||||
|
export function alertSearchNotifyTimeUtcFromLocalNineAm(
|
||||||
|
timeZone: string,
|
||||||
|
now: Date = new Date(),
|
||||||
|
): { notifyHourUtc: number; notifyMinuteUtc: number } {
|
||||||
|
const zone = requireIanaTimeZone(timeZone);
|
||||||
|
const localNine = DateTime.fromJSDate(now, { zone }).set({
|
||||||
|
hour: ALERT_SEARCH_DEBUG_LOCAL_NOTIFY_HOUR,
|
||||||
|
minute: ALERT_SEARCH_DEBUG_LOCAL_NOTIFY_MINUTE,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
});
|
||||||
|
if (!localNine.isValid) {
|
||||||
|
throw new Error(
|
||||||
|
"Could not convert AlertSearch 09:00 local notify time to UTC for zone " +
|
||||||
|
zone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const utc = localNine.toUTC();
|
||||||
|
return {
|
||||||
|
notifyHourUtc: utc.hour,
|
||||||
|
notifyMinuteUtc: utc.minute,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user