Add a manual debug-panel action to mint and PUT the 100-day AlertSearch authorization batch.
Reuse the existing delegated JWT minting and notification API auth so a signed-in ethr identity can upload the batch to the configured notify backend without changing production defaults or sending it automatically.
This commit is contained in:
@@ -64,6 +64,30 @@
|
||||
>
|
||||
Register Token Now
|
||||
</button>
|
||||
<button
|
||||
class="w-full text-md bg-gradient-to-b from-cyan-500 to-cyan-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
:disabled="busy"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': busy }"
|
||||
@click="onUploadAlertAuthorization"
|
||||
>
|
||||
Upload AlertSearch Authorization
|
||||
</button>
|
||||
<p
|
||||
v-if="alertAuthorizationStatus"
|
||||
class="text-xs rounded px-3 py-2 border"
|
||||
:class="
|
||||
alertAuthorizationStatus.ok
|
||||
? 'text-emerald-900 bg-emerald-50 border-emerald-200'
|
||||
: 'text-rose-900 bg-rose-50 border-rose-200'
|
||||
"
|
||||
role="status"
|
||||
>
|
||||
{{ alertAuthorizationStatus.message }}
|
||||
</p>
|
||||
<p v-else class="text-xs text-slate-500">
|
||||
Manually mints and uploads 100 delegated day JWTs. Requires an active
|
||||
did:ethr identity and JWT authentication; Test Mode is not used.
|
||||
</p>
|
||||
<button
|
||||
class="w-full text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
:disabled="busy"
|
||||
@@ -352,6 +376,10 @@ const fcmToken = ref<string | null>(NotificationDebugService.getFcmToken());
|
||||
|
||||
const activeBackendUrl = ref(NotificationDebugService.getActiveBackendUrl());
|
||||
const realWakeupStatus = ref<{ ok: boolean; message: string } | null>(null);
|
||||
const alertAuthorizationStatus = ref<{
|
||||
ok: boolean;
|
||||
message: string;
|
||||
} | null>(null);
|
||||
|
||||
const truncatedFcmToken = computed(() => {
|
||||
const t = fcmToken.value?.trim() ?? "";
|
||||
@@ -457,6 +485,36 @@ async function onRegisterToken(): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
async function onUploadAlertAuthorization(): Promise<void> {
|
||||
alertAuthorizationStatus.value = null;
|
||||
await withBusy(async () => {
|
||||
const result =
|
||||
await NotificationDebugService.uploadAlertSearchAuthorization();
|
||||
alertAuthorizationStatus.value = result.ok
|
||||
? {
|
||||
ok: true,
|
||||
message: [
|
||||
`Uploaded ${result.jwtCount} JWTs (HTTP ${result.status}).`,
|
||||
`Batch ${result.batchId}.`,
|
||||
`${result.timezone}: ${result.firstDay} through ${result.lastDay}.`,
|
||||
result.message,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
}
|
||||
: {
|
||||
ok: false,
|
||||
message: [
|
||||
`AlertSearch authorization upload failed: ${result.errorMessage}`,
|
||||
result.status != null ? `(HTTP ${result.status})` : undefined,
|
||||
result.errorCode ? `Code: ${result.errorCode}.` : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function onBackendRefresh(): Promise<void> {
|
||||
await withBusy(async () => {
|
||||
await NotificationDebugService.triggerBackendRefresh();
|
||||
|
||||
@@ -42,6 +42,10 @@ import {
|
||||
import { truncateFcmTokenForLog } from "./notificationLog";
|
||||
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
||||
import { NotificationInspector } from "@/plugins/NotificationInspectorPlugin";
|
||||
import {
|
||||
uploadAlertSearchAuthorization as uploadAlertSearchAuthorizationBatch,
|
||||
type AlertAuthorizationUploadResult,
|
||||
} from "./alertAuthorization";
|
||||
|
||||
type PendingNotificationInfo = {
|
||||
identifier: string;
|
||||
@@ -165,6 +169,30 @@ export const NotificationDebugService = {
|
||||
await reregisterFcmTokenNow();
|
||||
},
|
||||
|
||||
async uploadAlertSearchAuthorization(): Promise<AlertAuthorizationUploadResult> {
|
||||
logNotification("AlertSearch authorization upload requested");
|
||||
const result = await uploadAlertSearchAuthorizationBatch();
|
||||
if (result.ok) {
|
||||
logNotification("AlertSearch authorization upload succeeded", {
|
||||
batchId: result.batchId,
|
||||
timezone: result.timezone,
|
||||
jwtCount: result.jwtCount,
|
||||
firstDay: result.firstDay,
|
||||
lastDay: result.lastDay,
|
||||
status: result.status,
|
||||
});
|
||||
} else {
|
||||
logNotification(
|
||||
`AlertSearch authorization upload failed: ${result.errorMessage}`,
|
||||
{
|
||||
...(result.status != null ? { status: result.status } : {}),
|
||||
...(result.errorCode ? { errorCode: result.errorCode } : {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
async triggerBackendRefresh(): Promise<void> {
|
||||
await refreshNotificationsWithDiagnostics({ source: "debug panel" });
|
||||
},
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import { DELEGATED_NOTIFICATION_JWT_COUNT } from "@/constants/delegatedNotificationJwt";
|
||||
import type { DelegatedNotificationJwtBatch } from "@/interfaces/delegatedNotificationJwt";
|
||||
import {
|
||||
type AlertAuthorizationDependencies,
|
||||
uploadAlertSearchAuthorization,
|
||||
} from "./alertAuthorization";
|
||||
|
||||
jest.mock("@/libs/crypto/vc", () => ({
|
||||
ETHR_DID_PREFIX: "did:ethr:",
|
||||
}));
|
||||
jest.mock("@/libs/delegatedNotificationJwt", () => ({
|
||||
mintDelegatedNotificationJwtBatch: jest.fn(),
|
||||
}));
|
||||
jest.mock("./NotificationDebugConfig", () => ({
|
||||
getNotificationApiBaseUrl: jest.fn(),
|
||||
}));
|
||||
jest.mock("./notificationApiAuth", () => ({
|
||||
getActiveNotificationDid: jest.fn(),
|
||||
getNotificationApiHeaders: jest.fn(),
|
||||
httpAuthErrorMessage: (status: number) => `HTTP ${status}`,
|
||||
}));
|
||||
jest.mock("./notificationApiDebugMode", () => ({
|
||||
shouldBypassNotificationAuth: jest.fn(),
|
||||
}));
|
||||
|
||||
const ACTIVE_DID = `did:ethr:0x${"0".repeat(40)}`;
|
||||
const TIME_ZONE = "America/Denver";
|
||||
const BATCH_ID = "batch-fixture";
|
||||
|
||||
function createMintedBatch(): DelegatedNotificationJwtBatch {
|
||||
return {
|
||||
did: ACTIVE_DID,
|
||||
timeZone: TIME_ZONE,
|
||||
mintedAtEpoch: 1_788_220_800,
|
||||
tokens: Array.from(
|
||||
{ length: DELEGATED_NOTIFICATION_JWT_COUNT },
|
||||
(_, index) => ({
|
||||
sequence: index + 1,
|
||||
localDate: new Date(Date.UTC(2026, 8, index + 1))
|
||||
.toISOString()
|
||||
.slice(0, 10),
|
||||
nbf: 1_788_220_800 + index * 86_400,
|
||||
exp: 1_788_307_200 + index * 86_400,
|
||||
jwt: `delegated-token-${index + 1}`,
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function createResponse(
|
||||
status: number,
|
||||
body: Record<string, unknown>,
|
||||
): Response {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: jest.fn().mockResolvedValue(body),
|
||||
} as unknown as Response;
|
||||
}
|
||||
|
||||
function createDependencies(overrides?: {
|
||||
did?: string | null;
|
||||
bypassAuth?: boolean;
|
||||
response?: Response;
|
||||
}): {
|
||||
dependencies: AlertAuthorizationDependencies;
|
||||
fetchMock: jest.Mock;
|
||||
mintMock: jest.Mock;
|
||||
authMock: jest.Mock;
|
||||
} {
|
||||
const fetchMock = jest
|
||||
.fn()
|
||||
.mockResolvedValue(overrides?.response ?? createResponse(200, {}));
|
||||
const mintMock = jest.fn().mockResolvedValue(createMintedBatch());
|
||||
const authMock = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
authenticated: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer access-token-fixture",
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
dependencies: {
|
||||
getActiveDid: jest
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
overrides && "did" in overrides ? overrides.did : ACTIVE_DID,
|
||||
),
|
||||
isAuthBypassEnabled: jest
|
||||
.fn()
|
||||
.mockReturnValue(overrides?.bypassAuth ?? false),
|
||||
mintBatch: mintMock,
|
||||
getAuthHeaders: authMock,
|
||||
getBaseUrl: () => "https://notification-backend.invalid",
|
||||
createBatchId: () => BATCH_ID,
|
||||
fetch: fetchMock,
|
||||
},
|
||||
fetchMock,
|
||||
mintMock,
|
||||
authMock,
|
||||
};
|
||||
}
|
||||
|
||||
describe("uploadAlertSearchAuthorization", () => {
|
||||
it("uploads the existing 100-token batch with authenticated PUT semantics", async () => {
|
||||
const { dependencies, fetchMock, mintMock, authMock } =
|
||||
createDependencies();
|
||||
|
||||
const result = await uploadAlertSearchAuthorization(dependencies);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
batchId: BATCH_ID,
|
||||
timezone: TIME_ZONE,
|
||||
jwtCount: DELEGATED_NOTIFICATION_JWT_COUNT,
|
||||
});
|
||||
expect(mintMock).toHaveBeenCalledWith(ACTIVE_DID);
|
||||
expect(authMock).toHaveBeenCalledWith(undefined, ACTIVE_DID);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(url).toBe(
|
||||
"https://notification-backend.invalid/notifications/alert-authorization",
|
||||
);
|
||||
expect(init.method).toBe("PUT");
|
||||
expect(init.headers).toEqual({
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer access-token-fixture",
|
||||
});
|
||||
|
||||
const body = JSON.parse(String(init.body)) as {
|
||||
batchId: string;
|
||||
timezone: string;
|
||||
jwts: Array<{
|
||||
sequence: number;
|
||||
day: string;
|
||||
nbf: number;
|
||||
exp: number;
|
||||
jwt: string;
|
||||
}>;
|
||||
};
|
||||
expect(body.batchId).toBe(BATCH_ID);
|
||||
expect(body.timezone).toBe(TIME_ZONE);
|
||||
expect(body.jwts).toHaveLength(DELEGATED_NOTIFICATION_JWT_COUNT);
|
||||
expect(body.jwts.map((entry) => entry.sequence)).toEqual(
|
||||
Array.from(
|
||||
{ length: DELEGATED_NOTIFICATION_JWT_COUNT },
|
||||
(_, index) => index + 1,
|
||||
),
|
||||
);
|
||||
expect(body.jwts.map((entry) => entry.jwt)).toEqual(
|
||||
createMintedBatch().tokens.map((slot) => slot.jwt),
|
||||
);
|
||||
expect(body.jwts[0]).toEqual({
|
||||
sequence: 1,
|
||||
day: createMintedBatch().tokens[0]!.localDate,
|
||||
nbf: createMintedBatch().tokens[0]!.nbf,
|
||||
exp: createMintedBatch().tokens[0]!.exp,
|
||||
jwt: createMintedBatch().tokens[0]!.jwt,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not log delegated JWT contents", async () => {
|
||||
const logSpy = jest.spyOn(console, "log").mockImplementation();
|
||||
const warnSpy = jest.spyOn(console, "warn").mockImplementation();
|
||||
const errorSpy = jest.spyOn(console, "error").mockImplementation();
|
||||
const { dependencies } = createDependencies();
|
||||
|
||||
await uploadAlertSearchAuthorization(dependencies);
|
||||
|
||||
expect(logSpy).not.toHaveBeenCalled();
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
logSpy.mockRestore();
|
||||
warnSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("rejects unsupported identities before minting or network submission", async () => {
|
||||
const { dependencies, fetchMock, mintMock, authMock } = createDependencies({
|
||||
did: "did:peer:unsupported",
|
||||
});
|
||||
|
||||
const result = await uploadAlertSearchAuthorization(dependencies);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
errorMessage:
|
||||
"AlertSearch authorization requires an active did:ethr identity.",
|
||||
});
|
||||
expect(mintMock).not.toHaveBeenCalled();
|
||||
expect(authMock).not.toHaveBeenCalled();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects notification auth bypass before minting or submission", async () => {
|
||||
const { dependencies, fetchMock, mintMock } = createDependencies({
|
||||
bypassAuth: true,
|
||||
});
|
||||
|
||||
const result = await uploadAlertSearchAuthorization(dependencies);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(mintMock).not.toHaveBeenCalled();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("surfaces non-2xx responses using only safe response fields", async () => {
|
||||
const { dependencies } = createDependencies({
|
||||
response: createResponse(422, {
|
||||
code: "INVALID_BATCH",
|
||||
message: "Batch validation failed.",
|
||||
jwt: "must-not-be-returned",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await uploadAlertSearchAuthorization(dependencies);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
status: 422,
|
||||
errorCode: "INVALID_BATCH",
|
||||
errorMessage: "Batch validation failed.",
|
||||
});
|
||||
expect(JSON.stringify(result)).not.toContain("must-not-be-returned");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
import { DELEGATED_NOTIFICATION_JWT_COUNT } from "@/constants/delegatedNotificationJwt";
|
||||
import type { DelegatedNotificationJwtBatch } from "@/interfaces/delegatedNotificationJwt";
|
||||
import { ETHR_DID_PREFIX } from "@/libs/crypto/vc";
|
||||
import { mintDelegatedNotificationJwtBatch } from "@/libs/delegatedNotificationJwt";
|
||||
import { getNotificationApiBaseUrl } from "./NotificationDebugConfig";
|
||||
import {
|
||||
getActiveNotificationDid,
|
||||
getNotificationApiHeaders,
|
||||
httpAuthErrorMessage,
|
||||
} from "./notificationApiAuth";
|
||||
import { shouldBypassNotificationAuth } from "./notificationApiDebugMode";
|
||||
|
||||
export interface AlertAuthorizationRequestBody {
|
||||
batchId: string;
|
||||
timezone: string;
|
||||
jwts: Array<{
|
||||
sequence: number;
|
||||
day: string;
|
||||
nbf: number;
|
||||
exp: number;
|
||||
jwt: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type AlertAuthorizationUploadResult =
|
||||
| {
|
||||
ok: true;
|
||||
status: number;
|
||||
batchId: string;
|
||||
timezone: string;
|
||||
jwtCount: number;
|
||||
firstDay: string;
|
||||
lastDay: string;
|
||||
message?: string;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
errorMessage: string;
|
||||
status?: number;
|
||||
errorCode?: string;
|
||||
};
|
||||
|
||||
type FetchNotificationApi = (
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
) => Promise<Response>;
|
||||
|
||||
export interface AlertAuthorizationDependencies {
|
||||
getActiveDid: () => Promise<string | null>;
|
||||
isAuthBypassEnabled: () => boolean;
|
||||
mintBatch: (did: string) => Promise<DelegatedNotificationJwtBatch>;
|
||||
getAuthHeaders: typeof getNotificationApiHeaders;
|
||||
getBaseUrl: () => string;
|
||||
createBatchId: () => string;
|
||||
fetch: FetchNotificationApi;
|
||||
}
|
||||
|
||||
function createBatchId(): string {
|
||||
if (!globalThis.crypto?.randomUUID) {
|
||||
throw new Error("Secure batch ID generation is unavailable.");
|
||||
}
|
||||
return globalThis.crypto.randomUUID();
|
||||
}
|
||||
|
||||
const defaultDependencies: AlertAuthorizationDependencies = {
|
||||
getActiveDid: getActiveNotificationDid,
|
||||
isAuthBypassEnabled: shouldBypassNotificationAuth,
|
||||
mintBatch: mintDelegatedNotificationJwtBatch,
|
||||
getAuthHeaders: getNotificationApiHeaders,
|
||||
getBaseUrl: getNotificationApiBaseUrl,
|
||||
createBatchId,
|
||||
fetch: globalThis.fetch.bind(globalThis),
|
||||
};
|
||||
|
||||
function buildRequestBody(
|
||||
batchId: string,
|
||||
batch: DelegatedNotificationJwtBatch,
|
||||
): AlertAuthorizationRequestBody {
|
||||
if (batch.tokens.length !== DELEGATED_NOTIFICATION_JWT_COUNT) {
|
||||
throw new Error(
|
||||
`Expected ${DELEGATED_NOTIFICATION_JWT_COUNT} delegated JWTs, received ${batch.tokens.length}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
batchId,
|
||||
timezone: batch.timeZone,
|
||||
jwts: batch.tokens.map((slot) => ({
|
||||
sequence: slot.sequence,
|
||||
day: slot.localDate,
|
||||
nbf: slot.nbf,
|
||||
exp: slot.exp,
|
||||
jwt: slot.jwt,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async function readSafeResponseDetail(
|
||||
response: Response,
|
||||
): Promise<{ message?: string; errorCode?: string }> {
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
if (typeof body !== "object" || body === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const record = body as Record<string, unknown>;
|
||||
const messageKeys = ["message", "reason", "error"] as const;
|
||||
const codeKeys = ["code", "errorCode"] as const;
|
||||
const message = messageKeys
|
||||
.map((key) => record[key])
|
||||
.find((value): value is string => typeof value === "string" && !!value);
|
||||
const errorCode = codeKeys
|
||||
.map((key) => record[key])
|
||||
.find((value): value is string => typeof value === "string" && !!value);
|
||||
|
||||
return {
|
||||
...(message ? { message: message.trim() } : {}),
|
||||
...(errorCode ? { errorCode: errorCode.trim() } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mint and manually upload a delegated AlertSearch authorization batch.
|
||||
* This function never retries and refuses notification auth bypass mode.
|
||||
*/
|
||||
export async function uploadAlertSearchAuthorization(
|
||||
dependencies: AlertAuthorizationDependencies = defaultDependencies,
|
||||
): Promise<AlertAuthorizationUploadResult> {
|
||||
try {
|
||||
const did = await dependencies.getActiveDid();
|
||||
if (!did) {
|
||||
return { ok: false, errorMessage: "No active identity is available." };
|
||||
}
|
||||
if (!did.startsWith(ETHR_DID_PREFIX)) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage:
|
||||
"AlertSearch authorization requires an active did:ethr identity.",
|
||||
};
|
||||
}
|
||||
if (dependencies.isAuthBypassEnabled()) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage:
|
||||
"AlertSearch authorization cannot be uploaded while JWT authentication is skipped.",
|
||||
};
|
||||
}
|
||||
|
||||
const batch = await dependencies.mintBatch(did);
|
||||
if (batch.did !== did) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage:
|
||||
"Delegated JWT batch identity does not match active identity.",
|
||||
};
|
||||
}
|
||||
const batchId = dependencies.createBatchId();
|
||||
const body = buildRequestBody(batchId, batch);
|
||||
|
||||
const auth = await dependencies.getAuthHeaders(undefined, did);
|
||||
if (!auth.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage: `Authentication unavailable: ${auth.message}`,
|
||||
};
|
||||
}
|
||||
if (!auth.authenticated) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage:
|
||||
"AlertSearch authorization requires authenticated notification API headers.",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await dependencies.fetch(
|
||||
`${dependencies.getBaseUrl()}/notifications/alert-authorization`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: auth.headers,
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
);
|
||||
const detail = await readSafeResponseDetail(response);
|
||||
|
||||
if (!response.ok) {
|
||||
const fallback =
|
||||
response.status === 401 || response.status === 403
|
||||
? httpAuthErrorMessage(response.status)
|
||||
: `HTTP ${response.status}`;
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
errorMessage: detail.message || fallback,
|
||||
...(detail.errorCode ? { errorCode: detail.errorCode } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
batchId,
|
||||
timezone: body.timezone,
|
||||
jwtCount: body.jwts.length,
|
||||
firstDay: body.jwts[0]!.day,
|
||||
lastDay: body.jwts[body.jwts.length - 1]!.day,
|
||||
...(detail.message ? { message: detail.message } : {}),
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage: error instanceof Error ? error.message : "Upload failed.",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,11 @@ export {
|
||||
export { NotificationService, registerToken } from "./NotificationService";
|
||||
export { NativeNotificationService } from "./NativeNotificationService";
|
||||
export { WebPushNotificationService } from "./WebPushNotificationService";
|
||||
export { uploadAlertSearchAuthorization } from "./alertAuthorization";
|
||||
export type {
|
||||
AlertAuthorizationRequestBody,
|
||||
AlertAuthorizationUploadResult,
|
||||
} from "./alertAuthorization";
|
||||
|
||||
export { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
|
||||
export {
|
||||
|
||||
@@ -20,7 +20,7 @@ export type NotificationApiHeadersResult =
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
reason: "no_active_did" | "missing_token";
|
||||
reason: "no_active_did" | "missing_token" | "identity_changed";
|
||||
message: string;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ const DEBUG_HEADERS: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
async function resolveActiveDid(): Promise<string | null> {
|
||||
export async function getActiveNotificationDid(): Promise<string | null> {
|
||||
try {
|
||||
const service = PlatformServiceFactory.getInstance();
|
||||
const row = await service.dbGetOneRow(
|
||||
@@ -71,9 +71,11 @@ function logDebugUnauthenticatedNotificationRequest(): void {
|
||||
/**
|
||||
* Resolve headers for notification API requests.
|
||||
* @param kind Optional request kind for structured logs.
|
||||
* @param expectedDid Reject if the active identity changed before token minting.
|
||||
*/
|
||||
export async function getNotificationApiHeaders(
|
||||
kind?: NotificationRequestKind,
|
||||
expectedDid?: string,
|
||||
): Promise<NotificationApiHeadersResult> {
|
||||
if (shouldBypassNotificationAuth()) {
|
||||
logAuthBypassEnabled();
|
||||
@@ -83,7 +85,7 @@ export async function getNotificationApiHeaders(
|
||||
return { ok: true, authenticated: false, headers: { ...DEBUG_HEADERS } };
|
||||
}
|
||||
|
||||
const did = await resolveActiveDid();
|
||||
const did = await getActiveNotificationDid();
|
||||
if (!did) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -91,6 +93,13 @@ export async function getNotificationApiHeaders(
|
||||
message: "no active identity (cannot authenticate)",
|
||||
};
|
||||
}
|
||||
if (expectedDid && did !== expectedDid) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "identity_changed",
|
||||
message: "active identity changed before authentication",
|
||||
};
|
||||
}
|
||||
|
||||
const headers = await getHeaders(did);
|
||||
if (!hasBearerToken(headers)) {
|
||||
|
||||
Reference in New Issue
Block a user