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.
162 lines
4.3 KiB
TypeScript
162 lines
4.3 KiB
TypeScript
/**
|
|
* Authenticated headers for notification backend API calls (`/notifications/*`).
|
|
* Uses the same `getHeaders` + active DID flow as the rest of the app.
|
|
* Debug/local config can bypass auth for ngrok and panel testing.
|
|
*/
|
|
|
|
import { getHeaders } from "@/libs/endorserServer";
|
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
import { logger } from "@/utils/logger";
|
|
import { shouldBypassNotificationAuth } from "./notificationApiDebugMode";
|
|
import { logNotification } from "./NotificationDebugEvents";
|
|
|
|
export type NotificationRequestKind = "register" | "refresh";
|
|
|
|
export type NotificationApiHeadersResult =
|
|
| {
|
|
ok: true;
|
|
authenticated: boolean;
|
|
headers: Record<string, string>;
|
|
}
|
|
| {
|
|
ok: false;
|
|
reason: "no_active_did" | "missing_token" | "identity_changed";
|
|
message: string;
|
|
};
|
|
|
|
const DEBUG_HEADERS: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
};
|
|
|
|
export async function getActiveNotificationDid(): Promise<string | null> {
|
|
try {
|
|
const service = PlatformServiceFactory.getInstance();
|
|
const row = await service.dbGetOneRow(
|
|
"SELECT activeDid FROM active_identity WHERE id = 1",
|
|
);
|
|
if (!row?.[0]) {
|
|
return null;
|
|
}
|
|
const did = String(row[0]).trim();
|
|
return did || null;
|
|
} catch (err) {
|
|
logger.warn("[notificationApiAuth] Failed to read active DID", err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function hasBearerToken(headers: {
|
|
Authorization?: string;
|
|
}): headers is { Authorization: string } {
|
|
const auth = headers.Authorization;
|
|
return (
|
|
typeof auth === "string" &&
|
|
auth.startsWith("Bearer ") &&
|
|
auth.length > "Bearer ".length
|
|
);
|
|
}
|
|
|
|
function logAuthBypassEnabled(): void {
|
|
logNotification("Auth bypass enabled for debug/testing");
|
|
}
|
|
|
|
function logAuthenticatedNotificationRequest(): void {
|
|
logNotification("Using authenticated notification request");
|
|
}
|
|
|
|
function logDebugUnauthenticatedNotificationRequest(): void {
|
|
logNotification("Using debug unauthenticated notification request");
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
if (kind) {
|
|
logDebugUnauthenticatedNotificationRequest();
|
|
}
|
|
return { ok: true, authenticated: false, headers: { ...DEBUG_HEADERS } };
|
|
}
|
|
|
|
const did = await getActiveNotificationDid();
|
|
if (!did) {
|
|
return {
|
|
ok: false,
|
|
reason: "no_active_did",
|
|
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)) {
|
|
return {
|
|
ok: false,
|
|
reason: "missing_token",
|
|
message: "missing or empty Authorization token",
|
|
};
|
|
}
|
|
|
|
if (kind) {
|
|
logAuthenticatedNotificationRequest();
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
authenticated: true,
|
|
headers: {
|
|
"Content-Type": headers["Content-Type"],
|
|
Authorization: headers.Authorization,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function logNotificationRequestAuthenticated(
|
|
kind: NotificationRequestKind,
|
|
): void {
|
|
logNotification(
|
|
kind === "register"
|
|
? "Register request authenticated"
|
|
: "Refresh request authenticated",
|
|
);
|
|
}
|
|
|
|
export function logNotificationAuthFailure(
|
|
kind: NotificationRequestKind,
|
|
message: string,
|
|
): void {
|
|
const verb = kind === "register" ? "Register" : "Refresh";
|
|
logNotification(`${verb} auth unavailable: ${message}`);
|
|
}
|
|
|
|
export function logWaitingForAuthBeforeRegistration(): void {
|
|
logNotification("Waiting for auth before registration");
|
|
}
|
|
|
|
export function logSkippingRefreshDueToMissingAuth(): void {
|
|
logNotification("Skipping refresh due to missing auth");
|
|
}
|
|
|
|
export function httpAuthErrorMessage(status: number): string {
|
|
if (status === 401) {
|
|
return "unauthorized (expired or invalid auth)";
|
|
}
|
|
if (status === 403) {
|
|
return "forbidden (not authorized)";
|
|
}
|
|
return `HTTP ${status}`;
|
|
}
|