easy-ping 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/dist/adapters/drizzle.cjs +3 -2
- package/dist/adapters/drizzle.cjs.map +1 -1
- package/dist/adapters/drizzle.d.cts +34 -0
- package/dist/adapters/drizzle.d.ts +34 -0
- package/dist/adapters/drizzle.js +4 -3
- package/dist/adapters/drizzle.js.map +1 -1
- package/dist/adapters/mongodb.cjs +6 -2
- package/dist/adapters/mongodb.cjs.map +1 -1
- package/dist/adapters/mongodb.d.cts +1 -1
- package/dist/adapters/mongodb.d.ts +1 -1
- package/dist/adapters/mongodb.js +5 -2
- package/dist/adapters/mongodb.js.map +1 -1
- package/dist/adapters/postgres.cjs +3 -2
- package/dist/adapters/postgres.cjs.map +1 -1
- package/dist/adapters/postgres.d.cts +1 -1
- package/dist/adapters/postgres.d.ts +1 -1
- package/dist/adapters/postgres.js +3 -3
- package/dist/{chunk-OZSOIX23.js → chunk-3Y2MBUAW.js} +53 -12
- package/dist/chunk-3Y2MBUAW.js.map +1 -0
- package/dist/chunk-CYKAM7LE.js +226 -0
- package/dist/chunk-CYKAM7LE.js.map +1 -0
- package/dist/chunk-DG3ZL5NP.js +315 -0
- package/dist/chunk-DG3ZL5NP.js.map +1 -0
- package/dist/{chunk-NYDVNMKG.js → chunk-PSJHXZPR.js} +4 -3
- package/dist/chunk-PSJHXZPR.js.map +1 -0
- package/dist/{chunk-BV3U542P.js → chunk-W6I3B2CI.js} +2 -2
- package/dist/chunk-W6I3B2CI.js.map +1 -0
- package/dist/{chunk-RBDW4S6F.js → chunk-YEMRRMJQ.js} +3 -3
- package/dist/{chunk-RBDW4S6F.js.map → chunk-YEMRRMJQ.js.map} +1 -1
- package/dist/index.cjs +360 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -20
- package/dist/index.d.ts +84 -20
- package/dist/index.js +119 -357
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +27 -6
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +8 -2
- package/dist/node.d.ts +8 -2
- package/dist/node.js +26 -6
- package/dist/node.js.map +1 -1
- package/dist/{plugin-CmPOFVmn.d.ts → plugin-C9Hoxus2.d.ts} +37 -9
- package/dist/{plugin-f5KaFWSN.d.cts → plugin-CiMsGlQe.d.cts} +37 -9
- package/dist/plugins/digests.d.cts +1 -1
- package/dist/plugins/digests.d.ts +1 -1
- package/dist/plugins/preferences-client.cjs.map +1 -1
- package/dist/plugins/preferences-client.d.cts +1 -1
- package/dist/plugins/preferences-client.d.ts +1 -1
- package/dist/plugins/preferences-client.js +1 -1
- package/dist/plugins/preferences.cjs +100 -15
- package/dist/plugins/preferences.cjs.map +1 -1
- package/dist/plugins/preferences.d.cts +3 -1
- package/dist/plugins/preferences.d.ts +3 -1
- package/dist/plugins/preferences.js +35 -7
- package/dist/plugins/preferences.js.map +1 -1
- package/dist/plugins/push.cjs +197 -38
- package/dist/plugins/push.cjs.map +1 -1
- package/dist/plugins/push.d.cts +32 -10
- package/dist/plugins/push.d.ts +32 -10
- package/dist/plugins/push.js +140 -39
- package/dist/plugins/push.js.map +1 -1
- package/dist/providers/web-push.cjs +37 -11
- package/dist/providers/web-push.cjs.map +1 -1
- package/dist/providers/web-push.d.cts +8 -2
- package/dist/providers/web-push.d.ts +8 -2
- package/dist/providers/web-push.js +37 -12
- package/dist/providers/web-push.js.map +1 -1
- package/dist/schema.cjs +2 -1
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +6 -1
- package/dist/schema.d.ts +6 -1
- package/dist/schema.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-BV3U542P.js.map +0 -1
- package/dist/chunk-NYDVNMKG.js.map +0 -1
- package/dist/chunk-OZSOIX23.js.map +0 -1
- package/dist/chunk-W3BMEPPQ.js +0 -50
- package/dist/chunk-W3BMEPPQ.js.map +0 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/definition.ts","../src/core/handler.ts","../src/core/backoff.ts","../src/core/runner.ts","../src/core/send.ts","../src/core/instance.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { Channel } from \"./types\";\n\nexport type PayloadFromSchema<TSchema> = TSchema extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<TSchema>\n : Record<string, unknown>;\n\nexport type EmailTemplate<TPayload> = {\n subject: (payload: TPayload) => string;\n /** Rendered HTML. react-email users wrap: `(p) => render(<Email {...p} />)`. */\n template: (payload: TPayload) => string | Promise<string>;\n};\n\nexport type NotificationDefinition<\n TSchema extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined,\n> = {\n schema?: TSchema;\n channels: readonly Channel[];\n maxAttempts?: number;\n email?: EmailTemplate<PayloadFromSchema<TSchema>>;\n};\n\n/** Binds TSchema so email callbacks infer the payload; TS cannot infer from a sibling property. */\nexport function defineNotification<TSchema extends StandardSchemaV1 | undefined = undefined>(\n definition: NotificationDefinition<TSchema>,\n): NotificationDefinition<TSchema> {\n return definition;\n}\n\n/**\n * Erased form. `EmailTemplate<never>` because callbacks are contravariant —\n * anything else makes every schema-typed definition fail to assign.\n */\nexport type AnyNotificationDefinition = {\n schema?: StandardSchemaV1 | undefined;\n channels: readonly Channel[];\n maxAttempts?: number | undefined;\n email?: EmailTemplate<never> | undefined;\n};\n\nexport type NotificationDefinitions = Record<string, AnyNotificationDefinition>;\n\nexport type PayloadOf<TDef> = TDef extends { schema: infer TSchema extends StandardSchemaV1 }\n ? StandardSchemaV1.InferOutput<TSchema>\n : Record<string, unknown>;\n","import type { DatabaseAdapter } from \"./adapter\";\nimport type { SessionConfig } from \"./config\";\nimport type { Logger } from \"./errors\";\nimport type { RouteDefinition } from \"./plugin\";\nimport type { Runner } from \"./runner\";\nimport { verifyToken } from \"./tokens\";\n\nexport type HandlerDeps = {\n adapter: DatabaseAdapter;\n session: SessionConfig;\n runner: Runner;\n logger: Logger;\n basePath: string;\n secret: string;\n cronSecret?: string | undefined;\n pluginRoutes?: readonly RouteDefinition[] | undefined;\n};\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\" },\n });\n\nconst empty = (status: number) => new Response(null, { status });\n\nconst MAX_READ_IDS = 200;\n\n/**\n * Constant-time comparison. Node's timingSafeEqual is unavailable on edge\n * runtimes, and `===` on a secret leaks its length and prefix through timing.\n */\nfunction timingSafeEqual(a: string, b: string): boolean {\n const encoder = new TextEncoder();\n const left = encoder.encode(a);\n const right = encoder.encode(b);\n\n let diff = left.length ^ right.length;\n const length = Math.max(left.length, right.length);\n for (let i = 0; i < length; i += 1) {\n diff |= (left[i] ?? 0) ^ (right[i] ?? 0);\n }\n return diff === 0;\n}\n\nfunction routePath(url: URL, basePath: string): string {\n const index = url.pathname.indexOf(basePath);\n if (index < 0) return url.pathname;\n return url.pathname.slice(index + basePath.length) || \"/\";\n}\n\nasync function readJson(request: Request): Promise<Record<string, unknown>> {\n try {\n const body = await request.json();\n return body && typeof body === \"object\" ? (body as Record<string, unknown>) : {};\n } catch {\n return {};\n }\n}\n\nexport function createHandler(deps: HandlerDeps) {\n async function resolveUserId(request: Request): Promise<string | null | \"error\"> {\n try {\n return await deps.session.getUserId(request);\n } catch (error) {\n // A broken session lookup is a 500, never a 401. Collapsing them makes\n // an outage look like a mass logout. RFC 0002 §1.\n deps.logger.error(\"session.getUserId threw\", { error });\n return \"error\";\n }\n }\n\n async function handleCron(request: Request): Promise<Response> {\n if (!deps.cronSecret) return empty(404);\n\n const header = request.headers.get(\"authorization\") ?? \"\";\n const presented = header.startsWith(\"Bearer \") ? header.slice(7) : header;\n\n if (!timingSafeEqual(presented, deps.cronSecret)) return empty(401);\n\n const result = await deps.runner.drain();\n return json(result);\n }\n\n /**\n * Plugin routes are dispatched before core ones and carry their scope with\n * them. Authentication happens here rather than in the plugin handler, so a\n * plugin physically cannot forget to check a signature or scope a query.\n */\n async function handlePluginRoute(\n route: RouteDefinition,\n request: Request,\n url: URL,\n ): Promise<Response> {\n if (route.scope.type === \"user\") {\n const userId = await resolveUserId(request);\n if (userId === \"error\") return empty(500);\n if (userId === null) return empty(401);\n return route.handler({ request, userId, claims: null, params: {} });\n }\n\n if (route.scope.type === \"machine\") {\n if (!deps.cronSecret) return empty(404);\n const header = request.headers.get(\"authorization\") ?? \"\";\n const presented = header.startsWith(\"Bearer \") ? header.slice(7) : header;\n if (!timingSafeEqual(presented, deps.cronSecret)) return empty(401);\n return route.handler({ request, userId: null, claims: null, params: {} });\n }\n\n if (route.scope.type === \"signed\") {\n const token =\n url.searchParams.get(\"token\") ??\n (request.headers.get(\"authorization\") ?? \"\").replace(/^Bearer /, \"\");\n\n const claims = await verifyToken(deps.secret, token, route.scope.purpose);\n // One response for every failure mode. Distinguishing expired from\n // forged tells an attacker which part to fix. RFC 0002 §5.\n if (!claims) return empty(400);\n\n return route.handler({ request, userId: claims.uid, claims, params: {} });\n }\n\n return route.handler({ request, userId: null, claims: null, params: {} });\n }\n\n async function handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const path = routePath(url, deps.basePath);\n\n if (path === \"/cron\") {\n if (request.method !== \"POST\") return empty(405);\n return handleCron(request);\n }\n\n const pluginRoute = deps.pluginRoutes?.find(\n (route) => route.path === path && route.method === request.method,\n );\n if (pluginRoute) return handlePluginRoute(pluginRoute, request, url);\n\n const userId = await resolveUserId(request);\n if (userId === \"error\") return empty(500);\n if (userId === null) return empty(401);\n\n if (request.method === \"GET\" && path === \"/\") {\n const limitParam = Number(url.searchParams.get(\"limit\") ?? 20);\n const limit = Number.isFinite(limitParam) ? Math.min(Math.max(limitParam, 1), 100) : 20;\n const cursor = url.searchParams.get(\"cursor\");\n\n // The badge count rides along with the first page, so a polling client\n // needs one request per tick instead of two. Paginating past the first\n // page omits it: a cursor request is scrollback, not a poll.\n const [page, unseenCount] = await Promise.all([\n deps.adapter.listNotifications({\n userId,\n limit,\n ...(cursor ? { cursor } : {}),\n ...(url.searchParams.get(\"unreadOnly\") === \"true\" ? { unreadOnly: true } : {}),\n }),\n cursor ? Promise.resolve(null) : deps.adapter.countUnseen(userId),\n ]);\n\n return json(unseenCount === null ? page : { ...page, unseenCount });\n }\n\n if (request.method === \"GET\" && path === \"/count\") {\n return json({ unseen: await deps.adapter.countUnseen(userId) });\n }\n\n if (request.method === \"POST\" && path === \"/seen\") {\n const body = await readJson(request);\n const before = typeof body.before === \"string\" ? new Date(body.before) : null;\n\n // Defaults to now, but a client should send the timestamp of the newest\n // notification it has actually rendered. Otherwise anything that arrived\n // between the last poll and the click is marked seen without ever having\n // produced a badge — the user silently misses it.\n //\n // +1ms because Postgres keeps microseconds while an ISO string carries\n // only milliseconds. Without it the truncated cutoff falls *before* the\n // very row it was taken from, and markSeen matches nothing at all.\n const cutoff =\n before && !Number.isNaN(before.getTime()) ? new Date(before.getTime() + 1) : new Date();\n\n await deps.adapter.markSeen(userId, cutoff);\n return json({ ok: true });\n }\n\n if (request.method === \"POST\" && path === \"/read\") {\n const body = await readJson(request);\n const ids = Array.isArray(body.ids) ? body.ids.filter((id) => typeof id === \"string\") : [];\n if (ids.length === 0) return json({ error: \"ids must be a non-empty string array\" }, 400);\n\n // Unbounded, this builds a query with as many bind parameters as the\n // caller cares to send. Postgres caps at 65535 and degrades long before.\n if (ids.length > MAX_READ_IDS) {\n return json({ error: `at most ${MAX_READ_IDS} ids per request` }, 400);\n }\n\n // Scoped by userId in the adapter. A miss is 404, not 403 — a 403 would\n // confirm the row exists and belongs to someone else. RFC 0002 §2.\n const updated = await deps.adapter.markRead(userId, ids);\n return updated === 0 ? empty(404) : json({ updated });\n }\n\n if (request.method === \"POST\" && path === \"/read-all\") {\n return json({ updated: await deps.adapter.markAllRead(userId) });\n }\n\n return empty(404);\n }\n\n return {\n handle,\n GET: handle,\n POST: handle,\n };\n}\n","/**\n * Exponential backoff with ±20% jitter. RFC 0001 §4.\n *\n * These are minimums, not promises — the sweep interval floors them. Under an\n * hourly cron a 30s backoff is an hour.\n */\nconst SCHEDULE_MS = [30_000, 120_000, 480_000, 1_920_000] as const;\n\nexport type Backoff = \"exponential\" | ((attempt: number) => number);\n\nexport function backoffMs(attempt: number, strategy: Backoff = \"exponential\"): number {\n if (typeof strategy === \"function\") return Math.max(0, strategy(attempt));\n\n const index = Math.min(Math.max(attempt, 1), SCHEDULE_MS.length) - 1;\n const base = SCHEDULE_MS[index] ?? SCHEDULE_MS[SCHEDULE_MS.length - 1] ?? 30_000;\n return Math.round(base * (0.8 + Math.random() * 0.4));\n}\n\nexport const nextAttemptAt = (attempt: number, strategy?: Backoff, now = new Date()): Date =>\n new Date(now.getTime() + backoffMs(attempt, strategy));\n","import type { ClaimedDelivery, DatabaseAdapter, DeliveryOutcome, DeliveryRelease } from \"./adapter\";\nimport { type Backoff, nextAttemptAt } from \"./backoff\";\nimport type { ChannelsConfig, Worker } from \"./config\";\nimport type { NotificationDefinitions } from \"./definition\";\nimport type { Logger } from \"./errors\";\nimport type { AnyPlugin } from \"./plugin\";\nimport type { Channel, Recipient } from \"./types\";\n\nexport type RunnerDeps = {\n adapter: DatabaseAdapter;\n definitions: NotificationDefinitions;\n channels: ChannelsConfig;\n plugins: readonly AnyPlugin[];\n getRecipients: (userIds: readonly string[]) => Promise<readonly Recipient[]>;\n logger: Logger;\n leaseMs: number;\n batchSize: number;\n backoff: Backoff;\n};\n\nexport type SweepResult = {\n claimed: number;\n sent: number;\n failed: number;\n /** Deliveries that had nothing to do. Not errors; see DeliveryOutcome. */\n skipped: number;\n};\n\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nasync function withTimeout<T>(\n run: (signal: AbortSignal) => Promise<T>,\n ms: number,\n label: string,\n): Promise<T> {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n // Abort first: racing a promise leaves the original request running.\n controller.abort();\n reject(new Error(`${label} timed out after ${ms}ms`));\n }, ms);\n });\n\n try {\n return await Promise.race([run(controller.signal), timeout]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nconst message = (error: unknown) => (error instanceof Error ? error.message : String(error));\n\nexport function createRunner(deps: RunnerDeps) {\n async function deliverOne(\n delivery: ClaimedDelivery,\n recipient: Recipient | undefined,\n ): Promise<DeliveryOutcome> {\n if (!recipient) {\n return { result: \"failed\", error: \"recipient could not be resolved\", retryable: false };\n }\n\n if (delivery.channel === \"inApp\") {\n // The notification row is the delivery. Nothing to transmit.\n return { result: \"sent\" };\n }\n\n if (delivery.channel === \"email\") {\n const provider = deps.channels.email?.provider;\n if (!provider) {\n return { result: \"failed\", error: \"no email provider configured\", retryable: false };\n }\n // Captured before the closure below: narrowing does not survive into it.\n const to = recipient.email;\n if (!to) {\n return { result: \"failed\", error: \"recipient has no email address\", retryable: false };\n }\n\n const email = deps.definitions[delivery.notification.type]?.email;\n if (!email) {\n return {\n result: \"failed\",\n error: `notification type \"${delivery.notification.type}\" has no email template`,\n retryable: false,\n };\n }\n\n try {\n const payload = delivery.notification.payload as never;\n const html = await email.template(payload);\n\n await withTimeout(\n (signal) =>\n provider.send({\n to,\n subject: email.subject(payload),\n html,\n // Delivery is at-least-once; the provider dedupes on this.\n idempotencyKey: delivery.id,\n signal,\n }),\n provider.timeoutMs ?? 30_000,\n `${provider.name} send`,\n );\n\n return { result: \"sent\" };\n } catch (error) {\n return {\n result: \"failed\",\n error: message(error),\n retryable: provider.isRetryable(error),\n };\n }\n }\n\n const owner = deps.plugins.find(\n (plugin) => plugin.channels?.includes(delivery.channel) && plugin.hooks?.deliver,\n );\n if (!owner?.hooks?.deliver) {\n return {\n result: \"failed\",\n error: `no plugin delivers channel \"${delivery.channel}\"`,\n retryable: false,\n };\n }\n\n try {\n return await withTimeout(\n (signal) =>\n Promise.resolve(\n // biome-ignore lint/style/noNonNullAssertion: guarded directly above\n owner.hooks!.deliver!({\n channel: delivery.channel,\n deliveryId: delivery.id,\n attempt: delivery.attempts + 1,\n recipient,\n notification: {\n id: delivery.notificationId,\n userId: delivery.notification.userId,\n type: delivery.notification.type,\n payload: delivery.notification.payload,\n },\n signal,\n }),\n ),\n 30_000,\n `plugin \"${owner.id}\" deliver`,\n );\n } catch (error) {\n return { result: \"failed\", error: message(error), retryable: true };\n }\n }\n\n async function notifyAfterDeliver(delivery: ClaimedDelivery, outcome: DeliveryOutcome) {\n const isTerminal =\n outcome.result !== \"failed\" ||\n !outcome.retryable ||\n delivery.attempts + 1 >= delivery.maxAttempts;\n\n for (const plugin of deps.plugins) {\n const hook = plugin.hooks?.afterDeliver;\n if (!hook) continue;\n try {\n await hook({\n deliveryId: delivery.id,\n notificationId: delivery.notificationId,\n userId: delivery.notification.userId,\n channel: delivery.channel,\n outcome,\n attempt: delivery.attempts + 1,\n isTerminal,\n });\n } catch (error) {\n // Fail open. The message is already sent; a broken analytics hook must\n // not mark it failed and trigger a retry. RFC 0004 §5.\n deps.logger.error(`plugin \"${plugin.id}\" afterDeliver threw; ignoring`, {\n deliveryId: delivery.id,\n error,\n });\n }\n }\n }\n\n async function runOnce(\n options: { limit?: number; channels?: readonly Channel[]; ids?: readonly string[] } = {},\n ): Promise<SweepResult> {\n const claimToken = crypto.randomUUID();\n\n const claimed = await deps.adapter.claimPendingDeliveries({\n limit: options.limit ?? deps.batchSize,\n leaseMs: deps.leaseMs,\n claimToken,\n ...(options.channels ? { channels: options.channels } : {}),\n ...(options.ids ? { ids: options.ids } : {}),\n });\n\n if (claimed.length === 0) return { claimed: 0, sent: 0, failed: 0, skipped: 0 };\n\n const userIds = [...new Set(claimed.map((row) => row.notification.userId))];\n const recipients = await deps.getRecipients(userIds);\n const byUserId = new Map(recipients.map((recipient) => [recipient.userId, recipient]));\n\n const releases: DeliveryRelease[] = [];\n let sent = 0;\n let failed = 0;\n let skipped = 0;\n\n for (const delivery of claimed) {\n const outcome = await deliverOne(delivery, byUserId.get(delivery.notification.userId));\n\n if (outcome.result === \"sent\") sent += 1;\n else if (outcome.result === \"skipped\") skipped += 1;\n else failed += 1;\n\n releases.push({\n id: delivery.id,\n claimToken,\n outcome,\n ...(outcome.result === \"failed\" && outcome.retryable\n ? { nextAttemptAt: nextAttemptAt(delivery.attempts + 1, deps.backoff) }\n : {}),\n });\n\n await notifyAfterDeliver(delivery, outcome);\n }\n\n await deps.adapter.releaseDeliveries(releases);\n return { claimed: claimed.length, sent, failed, skipped };\n }\n\n /** Drains until empty. Pass `ids` to bound it to one send. */\n async function drain(\n options: { maxSweeps?: number; ids?: readonly string[] } = {},\n ): Promise<SweepResult> {\n const maxSweeps = options.maxSweeps ?? 50;\n const total: SweepResult = { claimed: 0, sent: 0, failed: 0, skipped: 0 };\n\n for (let sweep = 0; sweep < maxSweeps; sweep += 1) {\n const result = await runOnce(options.ids ? { ids: options.ids } : {});\n total.claimed += result.claimed;\n total.sent += result.sent;\n total.failed += result.failed;\n total.skipped += result.skipped;\n if (result.claimed === 0) break;\n }\n\n return total;\n }\n\n let warnedMissingWaitUntil = false;\n\n function scheduleDeferred(\n waitUntil?: (promise: Promise<unknown>) => void,\n ids?: readonly string[],\n ): void {\n if (waitUntil) {\n waitUntil(\n drain({ maxSweeps: 10, ...(ids ? { ids } : {}) }).catch((error) =>\n deps.logger.error(\"deferred sweep failed\", { error }),\n ),\n );\n return;\n }\n\n if (!warnedMissingWaitUntil) {\n warnedMissingWaitUntil = true;\n deps.logger.warn(\n \"delivery.mode is 'deferred' but no delivery.waitUntil was supplied — \" +\n \"falling back to the cron sweep. Notifications are not lost, but they \" +\n \"will not be delivered until POST /cron runs.\",\n );\n }\n }\n\n function startWorker(options: { intervalMs?: number; batchSize?: number } = {}): Worker {\n const intervalMs = options.intervalMs ?? 1_000;\n let stopped = false;\n\n const loop = (async () => {\n while (!stopped) {\n try {\n const result = await runOnce({ limit: options.batchSize ?? deps.batchSize });\n // Only idle when there was nothing to do; otherwise keep draining.\n if (result.claimed === 0) await sleep(intervalMs);\n } catch (error) {\n deps.logger.error(\"worker sweep failed\", { error });\n await sleep(intervalMs);\n }\n }\n })();\n\n return {\n stop: async () => {\n stopped = true;\n // Awaiting the loop lets the in-flight sweep finish and release its\n // leases, rather than leaving rows claimed until they expire.\n await loop;\n },\n };\n }\n\n return { runOnce, drain, scheduleDeferred, startWorker };\n}\n\nexport type Runner = ReturnType<typeof createRunner>;\n","import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { DatabaseAdapter, InsertDelivery, InsertNotification } from \"./adapter\";\nimport type { ChannelsConfig, SendResult } from \"./config\";\nimport type { NotificationDefinitions } from \"./definition\";\nimport { type Logger, ValidationError } from \"./errors\";\nimport type { AnyPlugin, BeforeSendResult, ChannelDecision } from \"./plugin\";\nimport type { Channel, Recipient, SkipReason } from \"./types\";\n\nexport type SendPipelineDeps = {\n adapter: DatabaseAdapter;\n definitions: NotificationDefinitions;\n channels: ChannelsConfig;\n plugins: readonly AnyPlugin[];\n getRecipients: (userIds: readonly string[]) => Promise<readonly Recipient[]>;\n maxAttempts: number;\n logger: Logger;\n generateId?: () => string;\n};\n\nexport type SendInput = {\n to: string | readonly string[];\n payload: unknown;\n actorId?: string | undefined;\n dedupeKey?: string | undefined;\n overrides?: { channels?: readonly Channel[] } | undefined;\n};\n\nasync function validatePayload(type: string, schema: StandardSchemaV1, payload: unknown) {\n const result = await schema[\"~standard\"].validate(payload);\n if (result.issues) {\n throw new ValidationError(\n type,\n result.issues.map((issue) => ({\n message: issue.message,\n ...(issue.path ? { path: issue.path.map(String).join(\".\") } : {}),\n })),\n );\n }\n return result.value;\n}\n\n/**\n * A channel is deliverable only if something can actually carry it.\n *\n * inApp defaults to on when unconfigured — it needs no provider, it is the\n * library's core feature, and making every app write `inApp: { enabled: true }`\n * is noise. Every other channel is off until given a provider, so a type\n * declaring `channels: [\"email\"]` with no email provider silently degrades to\n * in-app rather than throwing at send time.\n */\nexport function isChannelUsable(\n channel: Channel,\n channels: ChannelsConfig,\n plugins: readonly AnyPlugin[] = [],\n): boolean {\n if (channel === \"inApp\") return channels.inApp?.enabled !== false;\n if (channel === \"email\") return Boolean(channels.email?.provider);\n // Anything else is usable only if a plugin claims it and can deliver it.\n return plugins.some((plugin) => plugin.channels?.includes(channel) && plugin.hooks?.deliver);\n}\n\n/** One prepare() per plugin per send, keyed by plugin id. */\nasync function runPrepare(\n plugins: readonly AnyPlugin[],\n ctx: { type: string; recipients: readonly Recipient[] },\n logger: Logger,\n): Promise<Map<string, unknown>> {\n const prepared = new Map<string, unknown>();\n\n for (const plugin of plugins) {\n const hook = plugin.hooks?.prepare;\n if (!hook) continue;\n try {\n prepared.set(plugin.id, await hook(ctx));\n } catch (error) {\n // Fail closed, consistent with the hooks that consume this: a plugin\n // whose bulk load failed must not then decide with stale assumptions.\n logger.error(`plugin \"${plugin.id}\" prepare threw; its hooks will see undefined`, {\n type: ctx.type,\n error,\n });\n prepared.set(plugin.id, undefined);\n }\n }\n\n return prepared;\n}\n\nasync function runBeforeSend(\n plugins: readonly AnyPlugin[],\n base: { type: string; recipient: Recipient; actorId: string | null },\n payload: unknown,\n dedupeKey: string | null,\n prepared: Map<string, unknown>,\n logger: Logger,\n): Promise<BeforeSendResult> {\n let currentPayload = payload;\n let currentDedupeKey = dedupeKey;\n\n for (const plugin of plugins) {\n const hook = plugin.hooks?.beforeSend;\n if (!hook) continue;\n\n let result: BeforeSendResult;\n try {\n result = await hook({\n ...base,\n payload: currentPayload,\n dedupeKey: currentDedupeKey,\n prepared: prepared.get(plugin.id),\n });\n } catch (error) {\n // Fail closed. A crashed preferences hook failing open would email\n // people who opted out — a compliance incident. RFC 0004 §5.\n logger.error(`plugin \"${plugin.id}\" beforeSend threw; cancelling send`, {\n type: base.type,\n userId: base.recipient.userId,\n error,\n });\n return { action: \"cancel\", reason: `plugin \"${plugin.id}\" beforeSend threw` };\n }\n\n if (result.action === \"cancel\") return result;\n if (result.payload !== undefined) currentPayload = result.payload;\n if (result.dedupeKey !== undefined) currentDedupeKey = result.dedupeKey;\n }\n\n return {\n action: \"continue\",\n payload: currentPayload,\n ...(currentDedupeKey === null ? {} : { dedupeKey: currentDedupeKey }),\n };\n}\n\nasync function runResolveChannels(\n plugins: readonly AnyPlugin[],\n base: { type: string; recipient: Recipient; payload: unknown },\n initial: readonly ChannelDecision[],\n prepared: Map<string, unknown>,\n logger: Logger,\n): Promise<readonly ChannelDecision[]> {\n let decisions = initial;\n\n for (const plugin of plugins) {\n const hook = plugin.hooks?.resolveChannels;\n if (!hook) continue;\n\n try {\n decisions = [...(await hook({ ...base, decisions, prepared: prepared.get(plugin.id) }))];\n } catch (error) {\n // Fail closed: keep the decisions already made rather than falling back\n // to the permissive default. RFC 0004 §5.\n logger.error(`plugin \"${plugin.id}\" resolveChannels threw; keeping prior decisions`, {\n type: base.type,\n userId: base.recipient.userId,\n error,\n });\n return decisions;\n }\n }\n\n return decisions;\n}\n\nexport async function runSend(\n type: string,\n input: SendInput,\n deps: SendPipelineDeps,\n): Promise<SendResult> {\n const definition = deps.definitions[type];\n if (!definition) {\n throw new ValidationError(type, [{ message: `unknown notification type \"${type}\"` }]);\n }\n\n const payload = definition.schema\n ? await validatePayload(type, definition.schema, input.payload)\n : input.payload;\n\n const userIds = [...new Set(Array.isArray(input.to) ? input.to : [input.to as string])];\n const recipients = await deps.getRecipients(userIds);\n const byUserId = new Map(recipients.map((recipient) => [recipient.userId, recipient]));\n\n const newId = deps.generateId ?? (() => crypto.randomUUID());\n const skipped: { userId: string; reason: SkipReason }[] = [];\n const rows: InsertNotification[] = [];\n const actorId = input.actorId ?? null;\n\n const declared = input.overrides?.channels ?? definition.channels;\n const requested = declared.filter((channel) =>\n isChannelUsable(channel, deps.channels, deps.plugins),\n );\n\n // Distinguished from \"no-channels\" so a missing provider does not look\n // identical to a user having opted out of everything.\n if (declared.length > 0 && requested.length === 0) {\n return {\n notifications: [],\n skipped: userIds.map((userId) => ({ userId, reason: \"channel-unavailable\" as const })),\n };\n }\n\n const prepared = await runPrepare(deps.plugins, { type, recipients }, deps.logger);\n\n for (const userId of userIds) {\n const recipient = byUserId.get(userId);\n if (!recipient) {\n skipped.push({ userId, reason: \"no-recipient\" });\n continue;\n }\n\n const before = await runBeforeSend(\n deps.plugins,\n { type, recipient, actorId },\n payload,\n input.dedupeKey ?? null,\n prepared,\n deps.logger,\n );\n\n if (before.action === \"cancel\") {\n skipped.push({ userId, reason: \"no-channels\" });\n continue;\n }\n\n const resolved = await runResolveChannels(\n deps.plugins,\n { type, recipient, payload: before.payload ?? payload },\n requested.map((channel) => ({ channel, action: \"send\" as const })),\n prepared,\n deps.logger,\n );\n\n const now = new Date();\n const deliveries: InsertDelivery[] = resolved\n .filter((decision) => decision.action !== \"skip\")\n .filter((decision) => isChannelUsable(decision.channel, deps.channels, deps.plugins))\n .map((decision) => ({\n id: newId(),\n channel: decision.channel,\n maxAttempts: definition.maxAttempts ?? deps.maxAttempts,\n // `defer` needs no machinery beyond not_before — the claim query\n // already refuses rows whose time has not come. RFC 0004 §4.2.\n notBefore: decision.action === \"defer\" && decision.notBefore ? decision.notBefore : now,\n }));\n\n if (deliveries.length === 0) {\n skipped.push({ userId, reason: \"no-channels\" });\n continue;\n }\n\n const dedupeKey = before.action === \"continue\" ? before.dedupeKey : undefined;\n\n rows.push({\n id: newId(),\n userId,\n type,\n payload: before.payload ?? payload,\n ...(actorId ? { actorId } : {}),\n ...(dedupeKey ? { dedupeKey } : {}),\n deliveries,\n });\n }\n\n if (rows.length === 0) return { notifications: [], skipped };\n\n const { created, deduped } = await deps.adapter.createNotifications(rows);\n const createdSet = new Set(created);\n\n for (const row of rows) {\n if (deduped.includes(row.id)) skipped.push({ userId: row.userId, reason: \"deduped\" });\n }\n\n const notifications = rows\n .filter((row) => createdSet.has(row.id))\n .map((row) => ({\n id: row.id,\n userId: row.userId,\n deliveries: row.deliveries.map((delivery) => ({\n id: delivery.id,\n channel: delivery.channel,\n })),\n }));\n\n for (const plugin of deps.plugins) {\n const hook = plugin.hooks?.afterSend;\n if (!hook) continue;\n try {\n await hook({\n type,\n payload,\n notifications: notifications.map(({ id, userId }) => ({ id, userId })),\n prepared: prepared.get(plugin.id),\n });\n } catch (error) {\n // Fails open: the rows are committed, and throwing here would report a\n // send that actually happened as a failure.\n deps.logger.error(`plugin \"${plugin.id}\" afterSend threw; ignoring`, { type, error });\n }\n }\n\n return { notifications, skipped };\n}\n","import type { Backoff } from \"./backoff\";\nimport type { EasyPing, EasyPingConfig, HealthReport, SendArgs, SendResult } from \"./config\";\nimport type { NotificationDefinitions } from \"./definition\";\nimport { ConfigError, consoleLogger } from \"./errors\";\nimport { createHandler } from \"./handler\";\nimport type { AnyPlugin } from \"./plugin\";\nimport { createRunner } from \"./runner\";\nimport { isChannelUsable, runSend } from \"./send\";\nimport { createPluginStore } from \"./store\";\nimport type { DeliveryMode } from \"./types\";\n\nconst DEFAULTS = {\n maxAttempts: 5,\n leaseMs: 60_000,\n batchSize: 20,\n basePath: \"/api/notifications\",\n mode: \"cron\" as DeliveryMode,\n};\n\nfunction validate(config: EasyPingConfig<NotificationDefinitions>, plugins: readonly AnyPlugin[]) {\n if (!config.database) throw new ConfigError(\"`database` is required.\");\n if (!config.notifications) throw new ConfigError(\"`notifications` is required.\");\n\n // No default resolver and no dev bypass: the inbox endpoints serve per-user\n // data, and an insecure default ships where a startup crash does not.\n if (typeof config.session?.getUserId !== \"function\") {\n throw new ConfigError(\n \"`session.getUserId` is required — the mounted endpoints serve a user's private inbox \" +\n \"and must resolve the caller. See RFC 0002.\",\n );\n }\n\n if (!config.secret) {\n throw new ConfigError(\n \"`secret` is required — it signs unsubscribe and other session-less links. \" +\n \"Adding it later is a breaking change, so it is mandatory from the start.\",\n );\n }\n\n if (typeof config.getRecipients !== \"function\") {\n throw new ConfigError(\"`getRecipients` is required to resolve emails and timezones.\");\n }\n\n const seen = new Set<string>();\n for (const plugin of plugins) {\n if (seen.has(plugin.id)) {\n throw new ConfigError(`duplicate plugin id \"${plugin.id}\" — ids must be unique.`);\n }\n seen.add(plugin.id);\n }\n\n for (const plugin of plugins) {\n for (const dependency of plugin.dependsOn ?? []) {\n if (!seen.has(dependency)) {\n throw new ConfigError(\n `plugin \"${plugin.id}\" requires \"${dependency}\", which is not registered.`,\n );\n }\n }\n }\n\n // Two plugins claiming the same route silently shadow each other otherwise,\n // and which one wins depends on registration order.\n const routes = new Set<string>();\n for (const plugin of plugins) {\n for (const route of plugin.routes ?? []) {\n const signature = `${route.method} ${route.path}`;\n if (routes.has(signature)) {\n throw new ConfigError(`route collision: two plugins both registered \"${signature}\".`);\n }\n routes.add(signature);\n }\n }\n\n const mode = config.delivery?.mode ?? DEFAULTS.mode;\n if ((mode === \"cron\" || mode === \"deferred\") && !config.cron?.secret) {\n throw new ConfigError(\n `delivery.mode \"${mode}\" relies on the cron sweep, so \\`cron.secret\\` is required. ` +\n \"Left unset the endpoint would be unauthenticated — a free flush-everything trigger \" +\n \"against your email provider. See RFC 0002 §4.\",\n );\n }\n}\n\nexport function easyPing<TDefs extends NotificationDefinitions>(\n config: EasyPingConfig<TDefs>,\n): EasyPing<TDefs> {\n const plugins = config.plugins ?? [];\n validate(config as EasyPingConfig<NotificationDefinitions>, plugins);\n\n const logger = config.logger ?? consoleLogger;\n const tablePrefix = config.tablePrefix ?? \"\";\n const mode = config.delivery?.mode ?? DEFAULTS.mode;\n const leaseMs = config.delivery?.leaseMs ?? DEFAULTS.leaseMs;\n const warnings: string[] = [];\n\n // Below this ratio a slow send is re-claimed mid-flight, and duplicates stop\n // being occasional and become systematic. RFC 0003 §7.\n const providerTimeout = config.channels.email?.provider?.timeoutMs;\n if (providerTimeout !== undefined && providerTimeout >= leaseMs / 2) {\n warnings.push(\n `email provider timeout (${providerTimeout}ms) is at least half of delivery.leaseMs ` +\n `(${leaseMs}ms); raise leaseMs or duplicate sends become systematic.`,\n );\n }\n\n // Declaring `channels: [\"push\"]` used to do nothing at all: no warning, and\n // a skip reason indistinguishable from the user having opted out.\n const unusable = new Map<string, string[]>();\n for (const [type, definition] of Object.entries(config.notifications)) {\n const missing = definition.channels.filter(\n (channel) => !isChannelUsable(channel, config.channels, plugins),\n );\n if (missing.length > 0) unusable.set(type, [...missing]);\n }\n\n for (const [type, channels] of unusable) {\n const dead = channels.length === config.notifications[type]?.channels.length;\n warnings.push(\n `notification \"${type}\" declares ${channels.map((c) => `\"${c}\"`).join(\", \")} ` +\n `but no provider is configured for ${channels.length > 1 ? \"them\" : \"it\"}` +\n (dead ? \" — this type can never be delivered.\" : \"; those channels will be skipped.\"),\n );\n }\n\n if (mode === \"deferred\" && !config.delivery?.waitUntil) {\n warnings.push(\n \"delivery.mode is 'deferred' but no delivery.waitUntil was supplied; \" +\n \"delivery will wait for the cron sweep.\",\n );\n }\n\n for (const warning of warnings) logger.warn(warning);\n\n const runner = createRunner({\n adapter: config.database,\n definitions: config.notifications,\n channels: config.channels,\n plugins,\n getRecipients: config.getRecipients,\n logger,\n leaseMs,\n batchSize: config.delivery?.batchSize ?? DEFAULTS.batchSize,\n backoff: (config.delivery?.backoff ?? \"exponential\") as Backoff,\n });\n\n const pluginRoutes = plugins.flatMap((plugin) => plugin.routes ?? []);\n\n const handler = createHandler({\n adapter: config.database,\n session: config.session,\n runner,\n logger,\n secret: config.secret,\n basePath: config.basePath ?? DEFAULTS.basePath,\n pluginRoutes,\n ...(config.cron?.secret ? { cronSecret: config.cron.secret } : {}),\n });\n\n async function send<TKey extends keyof TDefs & string>(\n type: TKey,\n args: SendArgs<TDefs, TKey>,\n ): Promise<SendResult> {\n const result = await runSend(\n type,\n {\n to: args.to,\n payload: args.payload,\n actorId: args.actorId,\n dedupeKey: args.dedupeKey,\n overrides: args.overrides,\n },\n {\n adapter: config.database,\n definitions: config.notifications,\n channels: config.channels,\n plugins,\n getRecipients: config.getRecipients,\n maxAttempts: config.delivery?.maxAttempts ?? DEFAULTS.maxAttempts,\n logger,\n },\n );\n\n // The rows are committed by this point, so every branch below only affects\n // latency — never whether the notification survives. RFC 0001 §8.1.\n if (result.notifications.length > 0) {\n // Scoped to this send's deliveries. Unscoped, a single inline send in a\n // request handler flushes the whole backlog.\n const ids = result.notifications.flatMap((notification) =>\n notification.deliveries.map((delivery) => delivery.id),\n );\n\n if (mode === \"inline\") await runner.drain({ ids });\n else if (mode === \"deferred\") runner.scheduleDeferred(config.delivery?.waitUntil, ids);\n }\n\n return result;\n }\n\n // After send() exists, before any hook or route can run.\n for (const plugin of plugins) {\n plugin.init?.({\n store: createPluginStore(plugin.id, plugin.schema, config.database, tablePrefix),\n secret: config.secret,\n logger,\n getRecipients: config.getRecipients,\n send: (type, args) => send(type as keyof TDefs & string, args as never) as Promise<unknown>,\n });\n }\n\n return {\n send,\n handler,\n\n startWorker: (options) => runner.startWorker(options ?? {}),\n\n getFailedDeliveries: (options = {}) =>\n config.database.getFailedDeliveries({\n since: options.since ?? new Date(Date.now() - 24 * 3600_000),\n limit: Math.min(options.limit ?? 100, 1000),\n }),\n\n healthCheck: async (): Promise<HealthReport> => {\n const cronMounted = Boolean(config.cron?.secret);\n return {\n mode,\n cronMounted,\n cronRequiredButMissing:\n !cronMounted && (mode === \"cron\" || mode === \"deferred\" || mode === \"inline\"),\n warnings,\n };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/core/definition.ts","../src/core/html.ts","../src/core/rate-limit.ts","../src/core/send.ts","../src/core/instance.ts"],"names":["bucket"],"mappings":";;;;;;;;;;;AAuBO,SAAS,mBACd,UAAA,EACiC;AACjC,EAAA,OAAO,UAAA;AACT;;;AC3BA,IAAM,OAAA,GAAkC;AAAA,EACtC,GAAA,EAAK,OAAA;AAAA,EACL,GAAA,EAAK,MAAA;AAAA,EACL,GAAA,EAAK,MAAA;AAAA,EACL,GAAA,EAAK,QAAA;AAAA,EACL,GAAA,EAAK;AACP,CAAA;AAOO,IAAM,UAAA,GAAa,CAAC,KAAA,KACzB,MAAA,CAAO,SAAS,EAAE,CAAA,CAAE,OAAA,CAAQ,UAAA,EAAY,CAAC,IAAA,KAAS,OAAA,CAAQ,IAAI,KAAK,IAAI;;;ACFzE,IAAM,aAAA,GAAgB,CAAC,OAAA,KAAoC;AACzD,EAAA,MAAM,UAAU,OAAA,CAAQ,OAAA;AACxB,EAAA,OACE,QAAQ,GAAA,CAAI,kBAAkB,KAC9B,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA,IACvB,OAAA,CAAQ,GAAA,CAAI,iBAAiB,GAAG,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA,EAAG,MAAK,IACpD,IAAA;AAEJ,CAAA;AAQO,SAAS,kBAAkB,MAAA,EAAgE;AAChG,EAAA,MAAM,KAAA,GAAQ,OAAO,GAAA,IAAO,aAAA;AAC5B,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAgD;AACpE,EAAA,IAAI,SAAA,GAAY,CAAA;AAEhB,EAAA,OAAO,CAAC,OAAA,KAAY;AAClB,IAAA,MAAM,GAAA,GAAM,MAAM,OAAO,CAAA;AACzB,IAAA,IAAI,GAAA,KAAQ,MAAM,OAAO,IAAA;AAEzB,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AAIrB,IAAA,IAAI,GAAA,GAAM,SAAA,GAAY,MAAA,CAAO,QAAA,EAAU;AACrC,MAAA,KAAA,MAAW,CAAC,EAAA,EAAIA,OAAM,CAAA,IAAK,OAAA,EAAS,IAAIA,OAAAA,CAAO,OAAA,IAAW,GAAA,EAAK,OAAA,CAAQ,MAAA,CAAO,EAAE,CAAA;AAChF,MAAA,SAAA,GAAY,GAAA;AAAA,IACd;AAEA,IAAA,IAAI,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,GAAG,CAAA;AAC5B,IAAA,IAAI,CAAC,MAAA,IAAU,MAAA,CAAO,OAAA,IAAW,GAAA,EAAK;AACpC,MAAA,MAAA,GAAS,EAAE,KAAA,EAAO,CAAA,EAAG,OAAA,EAAS,GAAA,GAAM,OAAO,QAAA,EAAS;AACpD,MAAA,OAAA,CAAQ,GAAA,CAAI,KAAK,MAAM,CAAA;AAAA,IACzB;AAEA,IAAA,MAAA,CAAO,KAAA,IAAS,CAAA;AAChB,IAAA,IAAI,MAAA,CAAO,KAAA,IAAS,MAAA,CAAO,GAAA,EAAK,OAAO,IAAA;AAEvC,IAAA,OAAO,IAAI,SAAS,IAAA,EAAM;AAAA,MACxB,MAAA,EAAQ,GAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,aAAA,EAAe,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,IAAA,CAAA,CAAM,MAAA,CAAO,OAAA,GAAU,GAAA,IAAO,GAAI,CAAC,CAAC,CAAA;AAAA,QAC3E,eAAA,EAAiB;AAAA;AACnB,KACD,CAAA;AAAA,EACH,CAAA;AACF;;;ACpCA,eAAe,eAAA,CAAgB,IAAA,EAAc,MAAA,EAA0B,OAAA,EAAkB;AACvF,EAAA,MAAM,SAAS,MAAM,MAAA,CAAO,WAAW,CAAA,CAAE,SAAS,OAAO,CAAA;AACzD,EAAA,IAAI,OAAO,MAAA,EAAQ;AACjB,IAAA,MAAM,IAAI,eAAA;AAAA,MACR,IAAA;AAAA,MACA,MAAA,CAAO,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,MAAW;AAAA,QAC5B,SAAS,KAAA,CAAM,OAAA;AAAA,QACf,GAAI,KAAA,CAAM,IAAA,GAAO,EAAE,MAAM,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA,KAAM;AAAC,OACjE,CAAE;AAAA,KACJ;AAAA,EACF;AACA,EAAA,OAAO,MAAA,CAAO,KAAA;AAChB;AAWO,SAAS,eAAA,CACd,OAAA,EACA,QAAA,EACA,OAAA,GAAgC,EAAC,EACxB;AACT,EAAA,IAAI,OAAA,KAAY,OAAA,EAAS,OAAO,QAAA,CAAS,OAAO,OAAA,KAAY,KAAA;AAC5D,EAAA,IAAI,YAAY,OAAA,EAAS,OAAO,OAAA,CAAQ,QAAA,CAAS,OAAO,QAAQ,CAAA;AAEhE,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,CAAC,MAAA,KAAW,MAAA,CAAO,QAAA,EAAU,QAAA,CAAS,OAAO,CAAA,IAAK,MAAA,CAAO,KAAA,EAAO,OAAO,CAAA;AAC7F;AAGA,eAAe,UAAA,CACb,OAAA,EACA,GAAA,EACA,MAAA,EAC+B;AAC/B,EAAA,MAAM,QAAA,uBAAe,GAAA,EAAqB;AAE1C,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,MAAM,IAAA,GAAO,OAAO,KAAA,EAAO,OAAA;AAC3B,IAAA,IAAI,CAAC,IAAA,EAAM;AACX,IAAA,IAAI;AACF,MAAA,QAAA,CAAS,IAAI,MAAA,CAAO,EAAA,EAAI,MAAM,IAAA,CAAK,GAAG,CAAC,CAAA;AAAA,IACzC,SAAS,KAAA,EAAO;AAGd,MAAA,MAAA,CAAO,KAAA,CAAM,CAAA,QAAA,EAAW,MAAA,CAAO,EAAE,CAAA,6CAAA,CAAA,EAAiD;AAAA,QAChF,MAAM,GAAA,CAAI,IAAA;AAAA,QACV;AAAA,OACD,CAAA;AACD,MAAA,QAAA,CAAS,GAAA,CAAI,MAAA,CAAO,EAAA,EAAI,MAAS,CAAA;AAAA,IACnC;AAAA,EACF;AAEA,EAAA,OAAO,QAAA;AACT;AAEA,eAAe,cACb,OAAA,EACA,IAAA,EACA,OAAA,EACA,SAAA,EACA,UACA,MAAA,EAC2B;AAC3B,EAAA,IAAI,cAAA,GAAiB,OAAA;AACrB,EAAA,IAAI,gBAAA,GAAmB,SAAA;AAEvB,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,MAAM,IAAA,GAAO,OAAO,KAAA,EAAO,UAAA;AAC3B,IAAA,IAAI,CAAC,IAAA,EAAM;AAEX,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI;AACF,MAAA,MAAA,GAAS,MAAM,IAAA,CAAK;AAAA,QAClB,GAAG,IAAA;AAAA,QACH,OAAA,EAAS,cAAA;AAAA,QACT,SAAA,EAAW,gBAAA;AAAA,QACX,QAAA,EAAU,QAAA,CAAS,GAAA,CAAI,MAAA,CAAO,EAAE;AAAA,OACjC,CAAA;AAAA,IACH,SAAS,KAAA,EAAO;AAGd,MAAA,MAAA,CAAO,KAAA,CAAM,CAAA,QAAA,EAAW,MAAA,CAAO,EAAE,CAAA,mCAAA,CAAA,EAAuC;AAAA,QACtE,MAAM,IAAA,CAAK,IAAA;AAAA,QACX,MAAA,EAAQ,KAAK,SAAA,CAAU,MAAA;AAAA,QACvB;AAAA,OACD,CAAA;AACD,MAAA,OAAO,EAAE,MAAA,EAAQ,QAAA,EAAU,QAAQ,CAAA,QAAA,EAAW,MAAA,CAAO,EAAE,CAAA,kBAAA,CAAA,EAAqB;AAAA,IAC9E;AAEA,IAAA,IAAI,MAAA,CAAO,MAAA,KAAW,QAAA,EAAU,OAAO,MAAA;AACvC,IAAA,IAAI,MAAA,CAAO,OAAA,KAAY,MAAA,EAAW,cAAA,GAAiB,MAAA,CAAO,OAAA;AAC1D,IAAA,IAAI,MAAA,CAAO,SAAA,KAAc,MAAA,EAAW,gBAAA,GAAmB,MAAA,CAAO,SAAA;AAAA,EAChE;AAEA,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,UAAA;AAAA,IACR,OAAA,EAAS,cAAA;AAAA,IACT,GAAI,gBAAA,KAAqB,IAAA,GAAO,EAAC,GAAI,EAAE,WAAW,gBAAA;AAAiB,GACrE;AACF;AAEA,eAAe,kBAAA,CACb,OAAA,EACA,IAAA,EACA,OAAA,EACA,UACA,MAAA,EACqC;AACrC,EAAA,IAAI,SAAA,GAAY,OAAA;AAEhB,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,MAAM,IAAA,GAAO,OAAO,KAAA,EAAO,eAAA;AAC3B,IAAA,IAAI,CAAC,IAAA,EAAM;AAEX,IAAA,IAAI;AACF,MAAA,SAAA,GAAY,CAAC,GAAI,MAAM,IAAA,CAAK,EAAE,GAAG,IAAA,EAAM,SAAA,EAAW,QAAA,EAAU,SAAS,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG,CAAE,CAAA;AAAA,IACzF,SAAS,KAAA,EAAO;AAGd,MAAA,MAAA,CAAO,KAAA,CAAM,CAAA,QAAA,EAAW,MAAA,CAAO,EAAE,CAAA,gDAAA,CAAA,EAAoD;AAAA,QACnF,MAAM,IAAA,CAAK,IAAA;AAAA,QACX,MAAA,EAAQ,KAAK,SAAA,CAAU,MAAA;AAAA,QACvB;AAAA,OACD,CAAA;AACD,MAAA,OAAO,SAAA;AAAA,IACT;AAAA,EACF;AAEA,EAAA,OAAO,SAAA;AACT;AAEA,eAAsB,OAAA,CACpB,IAAA,EACA,KAAA,EACA,IAAA,EACqB;AACrB,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,WAAA,CAAY,IAAI,CAAA;AACxC,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,MAAM,IAAI,eAAA,CAAgB,IAAA,EAAM,CAAC,EAAE,SAAS,CAAA,2BAAA,EAA8B,IAAI,CAAA,CAAA,CAAA,EAAK,CAAC,CAAA;AAAA,EACtF;AAEA,EAAA,MAAM,OAAA,GAAU,UAAA,CAAW,MAAA,GACvB,MAAM,eAAA,CAAgB,IAAA,EAAM,UAAA,CAAW,MAAA,EAAQ,KAAA,CAAM,OAAO,CAAA,GAC5D,KAAA,CAAM,OAAA;AAEV,EAAA,MAAM,UAAU,CAAC,GAAG,IAAI,GAAA,CAAI,MAAM,OAAA,CAAQ,KAAA,CAAM,EAAE,CAAA,GAAI,MAAM,EAAA,GAAK,CAAC,KAAA,CAAM,EAAY,CAAC,CAAC,CAAA;AACtF,EAAA,MAAM,UAAA,GAAa,MAAM,IAAA,CAAK,aAAA,CAAc,OAAO,CAAA;AACnD,EAAA,MAAM,QAAA,GAAW,IAAI,GAAA,CAAI,UAAA,CAAW,GAAA,CAAI,CAAC,SAAA,KAAc,CAAC,SAAA,CAAU,MAAA,EAAQ,SAAS,CAAC,CAAC,CAAA;AAErF,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,UAAA,KAAe,MAAM,OAAO,UAAA,EAAW,CAAA;AAC1D,EAAA,MAAM,UAAoD,EAAC;AAC3D,EAAA,MAAM,OAA6B,EAAC;AACpC,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,IAAW,IAAA;AAEjC,EAAA,MAAM,QAAA,GAAW,KAAA,CAAM,SAAA,EAAW,QAAA,IAAY,UAAA,CAAW,QAAA;AACzD,EAAA,MAAM,YAAY,QAAA,CAAS,MAAA;AAAA,IAAO,CAAC,OAAA,KACjC,eAAA,CAAgB,SAAS,IAAA,CAAK,QAAA,EAAU,KAAK,OAAO;AAAA,GACtD;AAIA,EAAA,IAAI,QAAA,CAAS,MAAA,GAAS,CAAA,IAAK,SAAA,CAAU,WAAW,CAAA,EAAG;AACjD,IAAA,OAAO;AAAA,MACL,eAAe,EAAC;AAAA,MAChB,OAAA,EAAS,QAAQ,GAAA,CAAI,CAAC,YAAY,EAAE,MAAA,EAAQ,MAAA,EAAQ,qBAAA,EAA+B,CAAE;AAAA,KACvF;AAAA,EACF;AAEA,EAAA,MAAM,QAAA,GAAW,MAAM,UAAA,CAAW,IAAA,CAAK,OAAA,EAAS,EAAE,IAAA,EAAM,UAAA,EAAW,EAAG,IAAA,CAAK,MAAM,CAAA;AAEjF,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,MAAM,SAAA,GAAY,QAAA,CAAS,GAAA,CAAI,MAAM,CAAA;AACrC,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,OAAA,CAAQ,IAAA,CAAK,EAAE,MAAA,EAAQ,MAAA,EAAQ,gBAAgB,CAAA;AAC/C,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,SAAS,MAAM,aAAA;AAAA,MACnB,IAAA,CAAK,OAAA;AAAA,MACL,EAAE,IAAA,EAAM,SAAA,EAAW,OAAA,EAAQ;AAAA,MAC3B,OAAA;AAAA,MACA,MAAM,SAAA,IAAa,IAAA;AAAA,MACnB,QAAA;AAAA,MACA,IAAA,CAAK;AAAA,KACP;AAEA,IAAA,IAAI,MAAA,CAAO,WAAW,QAAA,EAAU;AAC9B,MAAA,OAAA,CAAQ,IAAA,CAAK,EAAE,MAAA,EAAQ,MAAA,EAAQ,eAAe,CAAA;AAC9C,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,WAAW,MAAM,kBAAA;AAAA,MACrB,IAAA,CAAK,OAAA;AAAA,MACL,EAAE,IAAA,EAAM,SAAA,EAAW,OAAA,EAAS,MAAA,CAAO,WAAW,OAAA,EAAQ;AAAA,MACtD,SAAA,CAAU,IAAI,CAAC,OAAA,MAAa,EAAE,OAAA,EAAS,MAAA,EAAQ,QAAgB,CAAE,CAAA;AAAA,MACjE,QAAA;AAAA,MACA,IAAA,CAAK;AAAA,KACP;AAEA,IAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,IAAA,MAAM,UAAA,GAA+B,SAClC,MAAA,CAAO,CAAC,aAAa,QAAA,CAAS,MAAA,KAAW,MAAM,CAAA,CAC/C,MAAA,CAAO,CAAC,aAAa,eAAA,CAAgB,QAAA,CAAS,OAAA,EAAS,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,OAAO,CAAC,CAAA,CACnF,GAAA,CAAI,CAAC,QAAA,MAAc;AAAA,MAClB,IAAI,KAAA,EAAM;AAAA,MACV,SAAS,QAAA,CAAS,OAAA;AAAA,MAClB,WAAA,EAAa,UAAA,CAAW,WAAA,IAAe,IAAA,CAAK,WAAA;AAAA;AAAA;AAAA,MAG5C,WAAW,QAAA,CAAS,MAAA,KAAW,WAAW,QAAA,CAAS,SAAA,GAAY,SAAS,SAAA,GAAY;AAAA,KACtF,CAAE,CAAA;AAEJ,IAAA,IAAI,UAAA,CAAW,WAAW,CAAA,EAAG;AAC3B,MAAA,OAAA,CAAQ,IAAA,CAAK,EAAE,MAAA,EAAQ,MAAA,EAAQ,eAAe,CAAA;AAC9C,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,SAAA,GAAY,MAAA,CAAO,MAAA,KAAW,UAAA,GAAa,OAAO,SAAA,GAAY,MAAA;AAEpE,IAAA,IAAA,CAAK,IAAA,CAAK;AAAA,MACR,IAAI,KAAA,EAAM;AAAA,MACV,MAAA;AAAA,MACA,IAAA;AAAA,MACA,OAAA,EAAS,OAAO,OAAA,IAAW,OAAA;AAAA,MAC3B,GAAI,OAAA,GAAU,EAAE,OAAA,KAAY,EAAC;AAAA,MAC7B,GAAI,SAAA,GAAY,EAAE,SAAA,KAAc,EAAC;AAAA,MACjC;AAAA,KACD,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG,OAAO,EAAE,aAAA,EAAe,IAAI,OAAA,EAAQ;AAE3D,EAAA,MAAM,EAAE,SAAS,OAAA,EAAQ,GAAI,MAAM,IAAA,CAAK,OAAA,CAAQ,oBAAoB,IAAI,CAAA;AACxE,EAAA,MAAM,UAAA,GAAa,IAAI,GAAA,CAAI,OAAO,CAAA;AAElC,EAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,IAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,GAAA,CAAI,EAAE,CAAA,EAAG,OAAA,CAAQ,IAAA,CAAK,EAAE,MAAA,EAAQ,GAAA,CAAI,MAAA,EAAQ,MAAA,EAAQ,WAAW,CAAA;AAAA,EACtF;AAEA,EAAA,MAAM,aAAA,GAAgB,IAAA,CACnB,MAAA,CAAO,CAAC,GAAA,KAAQ,UAAA,CAAW,GAAA,CAAI,GAAA,CAAI,EAAE,CAAC,CAAA,CACtC,GAAA,CAAI,CAAC,GAAA,MAAS;AAAA,IACb,IAAI,GAAA,CAAI,EAAA;AAAA,IACR,QAAQ,GAAA,CAAI,MAAA;AAAA,IACZ,UAAA,EAAY,GAAA,CAAI,UAAA,CAAW,GAAA,CAAI,CAAC,QAAA,MAAc;AAAA,MAC5C,IAAI,QAAA,CAAS,EAAA;AAAA,MACb,SAAS,QAAA,CAAS;AAAA,KACpB,CAAE;AAAA,GACJ,CAAE,CAAA;AAEJ,EAAA,KAAA,MAAW,MAAA,IAAU,KAAK,OAAA,EAAS;AACjC,IAAA,MAAM,IAAA,GAAO,OAAO,KAAA,EAAO,SAAA;AAC3B,IAAA,IAAI,CAAC,IAAA,EAAM;AACX,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAK;AAAA,QACT,IAAA;AAAA,QACA,OAAA;AAAA,QACA,aAAA,EAAe,aAAA,CAAc,GAAA,CAAI,CAAC,EAAE,EAAA,EAAI,MAAA,EAAO,MAAO,EAAE,EAAA,EAAI,MAAA,EAAO,CAAE,CAAA;AAAA,QACrE,QAAA,EAAU,QAAA,CAAS,GAAA,CAAI,MAAA,CAAO,EAAE;AAAA,OACjC,CAAA;AAAA,IACH,SAAS,KAAA,EAAO;AAGd,MAAA,IAAA,CAAK,MAAA,CAAO,MAAM,CAAA,QAAA,EAAW,MAAA,CAAO,EAAE,CAAA,2BAAA,CAAA,EAA+B,EAAE,IAAA,EAAM,KAAA,EAAO,CAAA;AAAA,IACtF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,eAAe,OAAA,EAAQ;AAClC;;;ACzRA,IAAM,WAAA,GAAuC;AAAA,EAC3C,EAAE,MAAA,EAAQ,KAAA,EAAO,IAAA,EAAM,GAAA,EAAK,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAO,EAAG,KAAA,EAAO,MAAA,EAAO;AAAA,EACnE,EAAE,MAAA,EAAQ,KAAA,EAAO,IAAA,EAAM,QAAA,EAAU,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAO,EAAG,KAAA,EAAO,MAAA,EAAO;AAAA,EACxE,EAAE,MAAA,EAAQ,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAO,EAAG,KAAA,EAAO,MAAA,EAAO;AAAA,EACxE,EAAE,MAAA,EAAQ,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAO,EAAG,KAAA,EAAO,MAAA,EAAO;AAAA,EACxE,EAAE,MAAA,EAAQ,MAAA,EAAQ,IAAA,EAAM,WAAA,EAAa,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAO,EAAG,KAAA,EAAO,MAAA,EAAO;AAAA,EAC5E,EAAE,MAAA,EAAQ,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,EAAE,IAAA,EAAM,SAAA,EAAU,EAAG,KAAA,EAAO,MAAA;AACtE,CAAA;AAEA,IAAM,QAAA,GAAW;AAAA,EACf,WAAA,EAAa,CAAA;AAAA,EACb,OAAA,EAAS,GAAA;AAAA,EACT,SAAA,EAAW,EAAA;AAAA,EACX,QAAA,EAAU,oBAAA;AAAA,EACV,IAAA,EAAM;AACR,CAAA;AAEO,IAAM,iBAAA,GAAoB;AAEjC,IAAM,mBAAA,uBAA0B,GAAA,CAAI;AAAA,EAClC,QAAA;AAAA,EACA,UAAA;AAAA,EACA,WAAA;AAAA,EACA,YAAA;AAAA,EACA,gBAAA;AAAA,EACA,UAAA;AAAA,EACA,qBAAA;AAAA,EACA;AACF,CAAC,CAAA;AAOD,SAAS,mBAAA,CAAoB,MAAc,KAAA,EAAe;AACxD,EAAA,IAAI,KAAA,CAAM,SAAS,iBAAA,IAAqB,mBAAA,CAAoB,IAAI,KAAA,CAAM,WAAA,EAAa,CAAA,EAAG;AACpF,IAAA,MAAM,IAAI,WAAA;AAAA,MACR,CAAA,EAAA,EAAK,IAAI,CAAA,oBAAA,EAAuB,iBAAiB,CAAA,iFAAA;AAAA,KAEnD;AAAA,EACF;AACF;AAEA,SAAS,QAAA,CAAS,QAAiD,OAAA,EAA+B;AAChG,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,EAAU,MAAM,IAAI,YAAY,yBAAyB,CAAA;AACrE,EAAA,IAAI,CAAC,MAAA,CAAO,aAAA,EAAe,MAAM,IAAI,YAAY,8BAA8B,CAAA;AAI/E,EAAA,IAAI,OAAO,MAAA,CAAO,OAAA,EAAS,SAAA,KAAc,UAAA,EAAY;AACnD,IAAA,MAAM,IAAI,WAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AAEA,EAAA,IAAI,CAAC,OAAO,MAAA,EAAQ;AAClB,IAAA,MAAM,IAAI,WAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AACA,EAAA,mBAAA,CAAoB,QAAA,EAAU,OAAO,MAAM,CAAA;AAC3C,EAAA,IAAI,OAAO,IAAA,EAAM,MAAA,sBAA4B,aAAA,EAAe,MAAA,CAAO,KAAK,MAAM,CAAA;AAC9E,EAAA,IAAI,MAAA,CAAO,aAAA,EAAe,mBAAA,CAAoB,eAAA,EAAiB,OAAO,aAAa,CAAA;AAEnF,EAAA,IAAI,MAAA,CAAO,cAAc,MAAA,CAAO,SAAA,CAAU,MAAM,CAAA,IAAK,MAAA,CAAO,SAAA,CAAU,QAAA,GAAW,CAAA,CAAA,EAAI;AACnF,IAAA,MAAM,IAAI,YAAY,iEAAiE,CAAA;AAAA,EACzF;AAEA,EAAA,IAAI,OAAO,MAAA,CAAO,aAAA,KAAkB,UAAA,EAAY;AAC9C,IAAA,MAAM,IAAI,YAAY,8DAA8D,CAAA;AAAA,EACtF;AAEA,EAAA,MAAM,IAAA,uBAAW,GAAA,EAAY;AAC7B,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,MAAM,IAAI,WAAA,CAAY,CAAA,qBAAA,EAAwB,MAAA,CAAO,EAAE,CAAA,4BAAA,CAAyB,CAAA;AAAA,IAClF;AACA,IAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAAA,EACpB;AAEA,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,KAAA,MAAW,UAAA,IAAc,MAAA,CAAO,SAAA,IAAa,EAAC,EAAG;AAC/C,MAAA,IAAI,CAAC,IAAA,CAAK,GAAA,CAAI,UAAU,CAAA,EAAG;AACzB,QAAA,MAAM,IAAI,WAAA;AAAA,UACR,CAAA,QAAA,EAAW,MAAA,CAAO,EAAE,CAAA,YAAA,EAAe,UAAU,CAAA,2BAAA;AAAA,SAC/C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAIA,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAY;AAC/B,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,KAAA,MAAW,KAAA,IAAS,MAAA,CAAO,MAAA,IAAU,EAAC,EAAG;AACvC,MAAA,MAAM,YAAY,CAAA,EAAG,KAAA,CAAM,MAAM,CAAA,CAAA,EAAI,MAAM,IAAI,CAAA,CAAA;AAC/C,MAAA,IAAI,MAAA,CAAO,GAAA,CAAI,SAAS,CAAA,EAAG;AACzB,QAAA,MAAM,IAAI,WAAA,CAAY,CAAA,8CAAA,EAAiD,SAAS,CAAA,EAAA,CAAI,CAAA;AAAA,MACtF;AACA,MAAA,MAAA,CAAO,IAAI,SAAS,CAAA;AAAA,IACtB;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,QAAA,EAAU,IAAA,IAAQ,QAAA,CAAS,IAAA;AAC/C,EAAA,IAAA,CAAK,SAAS,MAAA,IAAU,IAAA,KAAS,eAAe,CAAC,MAAA,CAAO,MAAM,MAAA,EAAQ;AACpE,IAAA,MAAM,IAAI,WAAA;AAAA,MACR,kBAAkB,IAAI,CAAA,oMAAA;AAAA,KAGxB;AAAA,EACF;AACF;AAEO,SAAS,SACd,MAAA,EACiB;AACjB,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,IAAW,EAAC;AACnC,EAAA,QAAA,CAAS,QAAmD,OAAO,CAAA;AAEnE,EAAA,MAAM,MAAA,GAAS,OAAO,MAAA,IAAU,aAAA;AAChC,EAAA,MAAM,WAAA,GAAc,OAAO,WAAA,IAAe,EAAA;AAC1C,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,QAAA,EAAU,IAAA,IAAQ,QAAA,CAAS,IAAA;AAC/C,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,QAAA,EAAU,OAAA,IAAW,QAAA,CAAS,OAAA;AACrD,EAAA,MAAM,WAAqB,EAAC;AAI5B,EAAA,MAAM,eAAA,GAAkB,MAAA,CAAO,QAAA,CAAS,KAAA,EAAO,QAAA,EAAU,SAAA;AACzD,EAAA,IAAI,eAAA,KAAoB,MAAA,IAAa,eAAA,IAAmB,OAAA,GAAU,CAAA,EAAG;AACnE,IAAA,QAAA,CAAS,IAAA;AAAA,MACP,CAAA,wBAAA,EAA2B,eAAe,CAAA,0CAAA,EACpC,OAAO,CAAA,wDAAA;AAAA,KACf;AAAA,EACF;AAIA,EAAA,MAAM,QAAA,uBAAe,GAAA,EAAsB;AAC3C,EAAA,KAAA,MAAW,CAAC,MAAM,UAAU,CAAA,IAAK,OAAO,OAAA,CAAQ,MAAA,CAAO,aAAa,CAAA,EAAG;AACrE,IAAA,MAAM,OAAA,GAAU,WAAW,QAAA,CAAS,MAAA;AAAA,MAClC,CAAC,OAAA,KAAY,CAAC,gBAAgB,OAAA,EAAS,MAAA,CAAO,UAAU,OAAO;AAAA,KACjE;AACA,IAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG,QAAA,CAAS,IAAI,IAAA,EAAM,CAAC,GAAG,OAAO,CAAC,CAAA;AAAA,EACzD;AAEA,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,QAAQ,CAAA,IAAK,QAAA,EAAU;AACvC,IAAA,MAAM,OAAO,QAAA,CAAS,MAAA,KAAW,OAAO,aAAA,CAAc,IAAI,GAAG,QAAA,CAAS,MAAA;AACtE,IAAA,QAAA,CAAS,IAAA;AAAA,MACP,CAAA,cAAA,EAAiB,IAAI,CAAA,WAAA,EAAc,QAAA,CAAS,IAAI,CAAC,CAAA,KAAM,CAAA,CAAA,EAAI,CAAC,CAAA,CAAA,CAAG,CAAA,CAAE,KAAK,IAAI,CAAC,sCACpC,QAAA,CAAS,MAAA,GAAS,IAAI,MAAA,GAAS,IAAI,CAAA,CAAA,IACvE,IAAA,GAAO,2CAAA,GAAyC,mCAAA;AAAA,KACrD;AAAA,EACF;AAEA,EAAA,IAAI,IAAA,KAAS,UAAA,IAAc,CAAC,MAAA,CAAO,UAAU,SAAA,EAAW;AACtD,IAAA,QAAA,CAAS,IAAA;AAAA,MACP;AAAA,KAEF;AAAA,EACF;AAIA,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,KAAA,MAAW,KAAA,IAAS,MAAA,CAAO,MAAA,IAAU,EAAC,EAAG;AACvC,MAAA,IAAI,KAAA,CAAM,KAAA,CAAM,IAAA,KAAS,QAAA,EAAU;AACjC,QAAA,QAAA,CAAS,IAAA;AAAA,UACP,CAAA,QAAA,EAAW,MAAA,CAAO,EAAE,CAAA,SAAA,EAAY,KAAA,CAAM,MAAM,CAAA,CAAA,EAAI,KAAA,CAAM,IAAI,CAAA,mBAAA,CAAA,GACxD,KAAA,CAAM,KAAA,CAAM;AAAA,SAChB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,KAAA,MAAW,OAAA,IAAW,QAAA,EAAU,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA;AAEnD,EAAA,MAAM,SAAS,YAAA,CAAa;AAAA,IAC1B,SAAS,MAAA,CAAO,QAAA;AAAA,IAChB,aAAa,MAAA,CAAO,aAAA;AAAA,IACpB,UAAU,MAAA,CAAO,QAAA;AAAA,IACjB,OAAA;AAAA,IACA,eAAe,MAAA,CAAO,aAAA;AAAA,IACtB,MAAA;AAAA,IACA,OAAA;AAAA,IACA,SAAA,EAAW,MAAA,CAAO,QAAA,EAAU,SAAA,IAAa,QAAA,CAAS,SAAA;AAAA,IAClD,OAAA,EAAU,MAAA,CAAO,QAAA,EAAU,OAAA,IAAW;AAAA,GACvC,CAAA;AAED,EAAA,MAAM,YAAA,GAAe,QAAQ,OAAA,CAAQ,CAAC,WAAW,MAAA,CAAO,MAAA,IAAU,EAAE,CAAA;AAEpE,EAAA,MAAM,UAAU,aAAA,CAAc;AAAA,IAC5B,SAAS,MAAA,CAAO,QAAA;AAAA,IAChB,SAAS,MAAA,CAAO,OAAA;AAAA,IAChB,MAAA;AAAA,IACA,MAAA;AAAA,IACA,QAAQ,MAAA,CAAO,MAAA;AAAA,IACf,QAAA,EAAU,MAAA,CAAO,QAAA,IAAY,QAAA,CAAS,QAAA;AAAA,IACtC,YAAA;AAAA,IACA,gBAAgB,MAAA,CAAO,cAAA;AAAA,IACvB,WAAW,MAAA,CAAO,SAAA;AAAA,IAClB,cAAc,MAAA,CAAO,YAAA;AAAA,IACrB,WAAW,MAAA,CAAO,SAAA,GAAY,iBAAA,CAAkB,MAAA,CAAO,SAAS,CAAA,GAAI,MAAA;AAAA,IACpE,aAAA,EAAe,OAAO,IAAA,EAAM,SAAA;AAAA,IAC5B,GAAI,MAAA,CAAO,IAAA,EAAM,MAAA,GAAS,EAAE,YAAY,MAAA,CAAO,IAAA,CAAK,MAAA,EAAO,GAAI,EAAC;AAAA,IAChE,GAAK,MAAA,CAAO,aAAA,IAAiB,MAAA,CAAO,MAAM,MAAA,GACtC,EAAE,aAAA,EAAe,MAAA,CAAO,aAAA,IAAiB,MAAA,CAAO,IAAA,EAAM,MAAA,KACtD;AAAC,GACN,CAAA;AAED,EAAA,eAAe,IAAA,CACb,MACA,IAAA,EACqB;AACrB,IAAA,MAAM,SAAS,MAAM,OAAA;AAAA,MACnB,IAAA;AAAA,MACA;AAAA,QACE,IAAI,IAAA,CAAK,EAAA;AAAA,QACT,SAAS,IAAA,CAAK,OAAA;AAAA,QACd,SAAS,IAAA,CAAK,OAAA;AAAA,QACd,WAAW,IAAA,CAAK,SAAA;AAAA,QAChB,WAAW,IAAA,CAAK;AAAA,OAClB;AAAA,MACA;AAAA,QACE,SAAS,MAAA,CAAO,QAAA;AAAA,QAChB,aAAa,MAAA,CAAO,aAAA;AAAA,QACpB,UAAU,MAAA,CAAO,QAAA;AAAA,QACjB,OAAA;AAAA,QACA,eAAe,MAAA,CAAO,aAAA;AAAA,QACtB,WAAA,EAAa,MAAA,CAAO,QAAA,EAAU,WAAA,IAAe,QAAA,CAAS,WAAA;AAAA,QACtD;AAAA;AACF,KACF;AAIA,IAAA,IAAI,MAAA,CAAO,aAAA,CAAc,MAAA,GAAS,CAAA,EAAG;AAGnC,MAAA,MAAM,GAAA,GAAM,OAAO,aAAA,CAAc,OAAA;AAAA,QAAQ,CAAC,iBACxC,YAAA,CAAa,UAAA,CAAW,IAAI,CAAC,QAAA,KAAa,SAAS,EAAE;AAAA,OACvD;AAEA,MAAA,IAAI,SAAS,QAAA,EAAU,MAAM,OAAO,KAAA,CAAM,EAAE,KAAK,CAAA;AAAA,WAAA,IACxC,SAAS,UAAA,EAAY,MAAA,CAAO,iBAAiB,MAAA,CAAO,QAAA,EAAU,WAAW,GAAG,CAAA;AAAA,IACvF;AAEA,IAAA,OAAO,MAAA;AAAA,EACT;AAGA,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,MAAM,WAAW,IAAI,GAAA;AAAA,MAAA,CAClB,MAAA,CAAO,MAAA,IAAU,EAAC,EAAG,OAAA;AAAA,QAAQ,CAAC,KAAA,KAC7B,KAAA,CAAM,KAAA,CAAM,IAAA,KAAS,QAAA,GAAW,CAAC,KAAA,CAAM,KAAA,CAAM,OAAO,CAAA,GAAI;AAAC;AAC3D,KACF;AACA,IAAA,MAAA,CAAO,IAAA,GAAO;AAAA,MACZ,KAAA,EAAO,kBAAkB,MAAA,CAAO,EAAA,EAAI,OAAO,MAAA,EAAQ,MAAA,CAAO,UAAU,WAAW,CAAA;AAAA,MAC/E,IAAA,EAAM,kBAAA;AAAA,QACJ,MAAA,CAAO,MAAA;AAAA,QACP,MAAA,CAAO,EAAA;AAAA,QACP,QAAA;AAAA,QACA,CAAC,OAAA,KAAY,IAAI,WAAA,CAAY,OAAO;AAAA,OACtC;AAAA,MACA,MAAA;AAAA,MACA,iBAAA,EAAmB,MAAA,CAAO,IAAA,CAAK,MAAA,CAAO,aAAa,CAAA;AAAA,MACnD,eAAe,MAAA,CAAO,aAAA;AAAA,MACtB,MAAM,CAAC,IAAA,EAAM,IAAA,KAAS,IAAA,CAAK,MAA8B,IAAa;AAAA,KACvE,CAAA;AAAA,EACH;AAEA,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,OAAA;AAAA,IAEA,aAAa,CAAC,OAAA,KAAY,OAAO,WAAA,CAAY,OAAA,IAAW,EAAE,CAAA;AAAA,IAE1D,YAAY,MAAM;AAAA,MAChB,GAAG,WAAA,CAAY,MAAA,CAAO,CAAC,KAAA,KAAU,KAAA,CAAM,IAAA,KAAS,OAAA,IAAW,OAAA,CAAQ,MAAA,CAAO,IAAA,EAAM,MAAM,CAAC,CAAA;AAAA,MACvF,GAAG,OAAA,CAAQ,OAAA;AAAA,QAAQ,CAAC,YACjB,MAAA,CAAO,MAAA,IAAU,EAAC,EAAG,GAAA,CAAI,CAAC,KAAA,MAAW;AAAA,UACpC,QAAQ,KAAA,CAAM,MAAA;AAAA,UACd,MAAM,KAAA,CAAM,IAAA;AAAA,UACZ,OAAO,KAAA,CAAM,KAAA;AAAA,UACb,OAAO,MAAA,CAAO;AAAA,SAChB,CAAE;AAAA;AACJ,KACF;AAAA,IAEA,qBAAqB,CAAC,OAAA,GAAU,EAAC,KAC/B,MAAA,CAAO,SAAS,mBAAA,CAAoB;AAAA,MAClC,KAAA,EAAO,QAAQ,KAAA,IAAS,IAAI,KAAK,IAAA,CAAK,GAAA,EAAI,GAAI,EAAA,GAAK,IAAQ,CAAA;AAAA,MAC3D,OAAO,IAAA,CAAK,GAAA,CAAI,OAAA,CAAQ,KAAA,IAAS,KAAK,GAAI;AAAA,KAC3C,CAAA;AAAA,IAEH,aAAa,YAAmC;AAC9C,MAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,MAAA,CAAO,IAAA,EAAM,MAAM,CAAA;AAC/C,MAAA,OAAO;AAAA,QACL,IAAA;AAAA,QACA,WAAA;AAAA,QACA,wBACE,CAAC,WAAA,KAAgB,SAAS,MAAA,IAAU,IAAA,KAAS,cAAc,IAAA,KAAS,QAAA,CAAA;AAAA,QACtE;AAAA,OACF;AAAA,IACF;AAAA,GACF;AACF","file":"index.js","sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { Channel } from \"./types\";\n\nexport type PayloadFromSchema<TSchema> = TSchema extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<TSchema>\n : Record<string, unknown>;\n\nexport type EmailTemplate<TPayload> = {\n subject: (payload: TPayload) => string;\n /** Rendered HTML. react-email users wrap: `(p) => render(<Email {...p} />)`. */\n template: (payload: TPayload) => string | Promise<string>;\n};\n\nexport type NotificationDefinition<\n TSchema extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined,\n> = {\n schema?: TSchema;\n channels: readonly Channel[];\n maxAttempts?: number;\n email?: EmailTemplate<PayloadFromSchema<TSchema>>;\n};\n\n/** Binds TSchema so email callbacks infer the payload; TS cannot infer from a sibling property. */\nexport function defineNotification<TSchema extends StandardSchemaV1 | undefined = undefined>(\n definition: NotificationDefinition<TSchema>,\n): NotificationDefinition<TSchema> {\n return definition;\n}\n\n/**\n * Erased form. `EmailTemplate<never>` because callbacks are contravariant —\n * anything else makes every schema-typed definition fail to assign.\n */\nexport type AnyNotificationDefinition = {\n schema?: StandardSchemaV1 | undefined;\n channels: readonly Channel[];\n maxAttempts?: number | undefined;\n email?: EmailTemplate<never> | undefined;\n};\n\nexport type NotificationDefinitions = Record<string, AnyNotificationDefinition>;\n\nexport type PayloadOf<TDef> = TDef extends { schema: infer TSchema extends StandardSchemaV1 }\n ? StandardSchemaV1.InferOutput<TSchema>\n : Record<string, unknown>;\n","const ESCAPES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\n/**\n * For email templates built with template literals. Anything in a payload\n * that a user typed — a display name, a comment excerpt — goes through this\n * or the recipient gets that user's HTML delivered from your sending domain.\n */\nexport const escapeHtml = (value: unknown): string =>\n String(value ?? \"\").replace(/[&<>\"']/g, (char) => ESCAPES[char] ?? char);\n","export type RateLimitConfig = {\n /** Requests allowed per key per window. */\n max: number;\n windowMs: number;\n /**\n * Which bucket a request lands in. Defaults to the client address as the\n * usual proxy headers report it; returning null exempts the request. Behind\n * no proxy those headers are absent, so supply your own key there.\n */\n key?: (request: Request) => string | null;\n};\n\nconst clientAddress = (request: Request): string | null => {\n const headers = request.headers;\n return (\n headers.get(\"cf-connecting-ip\") ??\n headers.get(\"x-real-ip\") ??\n headers.get(\"x-forwarded-for\")?.split(\",\")[0]?.trim() ??\n null\n );\n};\n\n/**\n * Fixed-window counter held in process memory. Per isolate on edge runtimes\n * and per replica anywhere else, so it bounds one client's cost to one\n * process rather than enforcing a global quota. That is enough to blunt\n * secret guessing and poll storms, which is what it is for.\n */\nexport function createRateLimiter(config: RateLimitConfig): (request: Request) => Response | null {\n const keyOf = config.key ?? clientAddress;\n const buckets = new Map<string, { count: number; resetAt: number }>();\n let lastSweep = 0;\n\n return (request) => {\n const key = keyOf(request);\n if (key === null) return null;\n\n const now = Date.now();\n\n // Drop expired buckets no more than once per window, so memory tracks\n // active clients rather than everyone ever seen.\n if (now - lastSweep > config.windowMs) {\n for (const [id, bucket] of buckets) if (bucket.resetAt <= now) buckets.delete(id);\n lastSweep = now;\n }\n\n let bucket = buckets.get(key);\n if (!bucket || bucket.resetAt <= now) {\n bucket = { count: 0, resetAt: now + config.windowMs };\n buckets.set(key, bucket);\n }\n\n bucket.count += 1;\n if (bucket.count <= config.max) return null;\n\n return new Response(null, {\n status: 429,\n headers: {\n \"retry-after\": String(Math.max(1, Math.ceil((bucket.resetAt - now) / 1000))),\n \"cache-control\": \"private, no-store\",\n },\n });\n };\n}\n","import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { DatabaseAdapter, InsertDelivery, InsertNotification } from \"./adapter\";\nimport type { ChannelsConfig, SendResult } from \"./config\";\nimport type { NotificationDefinitions } from \"./definition\";\nimport { type Logger, ValidationError } from \"./errors\";\nimport type { AnyPlugin, BeforeSendResult, ChannelDecision } from \"./plugin\";\nimport type { Channel, Recipient, SkipReason } from \"./types\";\n\nexport type SendPipelineDeps = {\n adapter: DatabaseAdapter;\n definitions: NotificationDefinitions;\n channels: ChannelsConfig;\n plugins: readonly AnyPlugin[];\n getRecipients: (userIds: readonly string[]) => Promise<readonly Recipient[]>;\n maxAttempts: number;\n logger: Logger;\n generateId?: () => string;\n};\n\nexport type SendInput = {\n to: string | readonly string[];\n payload: unknown;\n actorId?: string | undefined;\n dedupeKey?: string | undefined;\n overrides?: { channels?: readonly Channel[] } | undefined;\n};\n\nasync function validatePayload(type: string, schema: StandardSchemaV1, payload: unknown) {\n const result = await schema[\"~standard\"].validate(payload);\n if (result.issues) {\n throw new ValidationError(\n type,\n result.issues.map((issue) => ({\n message: issue.message,\n ...(issue.path ? { path: issue.path.map(String).join(\".\") } : {}),\n })),\n );\n }\n return result.value;\n}\n\n/**\n * A channel is deliverable only if something can actually carry it.\n *\n * inApp defaults to on when unconfigured — it needs no provider, it is the\n * library's core feature, and making every app write `inApp: { enabled: true }`\n * is noise. Every other channel is off until given a provider, so a type\n * declaring `channels: [\"email\"]` with no email provider silently degrades to\n * in-app rather than throwing at send time.\n */\nexport function isChannelUsable(\n channel: Channel,\n channels: ChannelsConfig,\n plugins: readonly AnyPlugin[] = [],\n): boolean {\n if (channel === \"inApp\") return channels.inApp?.enabled !== false;\n if (channel === \"email\") return Boolean(channels.email?.provider);\n // Anything else is usable only if a plugin claims it and can deliver it.\n return plugins.some((plugin) => plugin.channels?.includes(channel) && plugin.hooks?.deliver);\n}\n\n/** One prepare() per plugin per send, keyed by plugin id. */\nasync function runPrepare(\n plugins: readonly AnyPlugin[],\n ctx: { type: string; recipients: readonly Recipient[] },\n logger: Logger,\n): Promise<Map<string, unknown>> {\n const prepared = new Map<string, unknown>();\n\n for (const plugin of plugins) {\n const hook = plugin.hooks?.prepare;\n if (!hook) continue;\n try {\n prepared.set(plugin.id, await hook(ctx));\n } catch (error) {\n // Fail closed, consistent with the hooks that consume this: a plugin\n // whose bulk load failed must not then decide with stale assumptions.\n logger.error(`plugin \"${plugin.id}\" prepare threw; its hooks will see undefined`, {\n type: ctx.type,\n error,\n });\n prepared.set(plugin.id, undefined);\n }\n }\n\n return prepared;\n}\n\nasync function runBeforeSend(\n plugins: readonly AnyPlugin[],\n base: { type: string; recipient: Recipient; actorId: string | null },\n payload: unknown,\n dedupeKey: string | null,\n prepared: Map<string, unknown>,\n logger: Logger,\n): Promise<BeforeSendResult> {\n let currentPayload = payload;\n let currentDedupeKey = dedupeKey;\n\n for (const plugin of plugins) {\n const hook = plugin.hooks?.beforeSend;\n if (!hook) continue;\n\n let result: BeforeSendResult;\n try {\n result = await hook({\n ...base,\n payload: currentPayload,\n dedupeKey: currentDedupeKey,\n prepared: prepared.get(plugin.id),\n });\n } catch (error) {\n // Fail closed. A crashed preferences hook failing open would email\n // people who opted out — a compliance incident. RFC 0004 §5.\n logger.error(`plugin \"${plugin.id}\" beforeSend threw; cancelling send`, {\n type: base.type,\n userId: base.recipient.userId,\n error,\n });\n return { action: \"cancel\", reason: `plugin \"${plugin.id}\" beforeSend threw` };\n }\n\n if (result.action === \"cancel\") return result;\n if (result.payload !== undefined) currentPayload = result.payload;\n if (result.dedupeKey !== undefined) currentDedupeKey = result.dedupeKey;\n }\n\n return {\n action: \"continue\",\n payload: currentPayload,\n ...(currentDedupeKey === null ? {} : { dedupeKey: currentDedupeKey }),\n };\n}\n\nasync function runResolveChannels(\n plugins: readonly AnyPlugin[],\n base: { type: string; recipient: Recipient; payload: unknown },\n initial: readonly ChannelDecision[],\n prepared: Map<string, unknown>,\n logger: Logger,\n): Promise<readonly ChannelDecision[]> {\n let decisions = initial;\n\n for (const plugin of plugins) {\n const hook = plugin.hooks?.resolveChannels;\n if (!hook) continue;\n\n try {\n decisions = [...(await hook({ ...base, decisions, prepared: prepared.get(plugin.id) }))];\n } catch (error) {\n // Fail closed: keep the decisions already made rather than falling back\n // to the permissive default. RFC 0004 §5.\n logger.error(`plugin \"${plugin.id}\" resolveChannels threw; keeping prior decisions`, {\n type: base.type,\n userId: base.recipient.userId,\n error,\n });\n return decisions;\n }\n }\n\n return decisions;\n}\n\nexport async function runSend(\n type: string,\n input: SendInput,\n deps: SendPipelineDeps,\n): Promise<SendResult> {\n const definition = deps.definitions[type];\n if (!definition) {\n throw new ValidationError(type, [{ message: `unknown notification type \"${type}\"` }]);\n }\n\n const payload = definition.schema\n ? await validatePayload(type, definition.schema, input.payload)\n : input.payload;\n\n const userIds = [...new Set(Array.isArray(input.to) ? input.to : [input.to as string])];\n const recipients = await deps.getRecipients(userIds);\n const byUserId = new Map(recipients.map((recipient) => [recipient.userId, recipient]));\n\n const newId = deps.generateId ?? (() => crypto.randomUUID());\n const skipped: { userId: string; reason: SkipReason }[] = [];\n const rows: InsertNotification[] = [];\n const actorId = input.actorId ?? null;\n\n const declared = input.overrides?.channels ?? definition.channels;\n const requested = declared.filter((channel) =>\n isChannelUsable(channel, deps.channels, deps.plugins),\n );\n\n // Distinguished from \"no-channels\" so a missing provider does not look\n // identical to a user having opted out of everything.\n if (declared.length > 0 && requested.length === 0) {\n return {\n notifications: [],\n skipped: userIds.map((userId) => ({ userId, reason: \"channel-unavailable\" as const })),\n };\n }\n\n const prepared = await runPrepare(deps.plugins, { type, recipients }, deps.logger);\n\n for (const userId of userIds) {\n const recipient = byUserId.get(userId);\n if (!recipient) {\n skipped.push({ userId, reason: \"no-recipient\" });\n continue;\n }\n\n const before = await runBeforeSend(\n deps.plugins,\n { type, recipient, actorId },\n payload,\n input.dedupeKey ?? null,\n prepared,\n deps.logger,\n );\n\n if (before.action === \"cancel\") {\n skipped.push({ userId, reason: \"no-channels\" });\n continue;\n }\n\n const resolved = await runResolveChannels(\n deps.plugins,\n { type, recipient, payload: before.payload ?? payload },\n requested.map((channel) => ({ channel, action: \"send\" as const })),\n prepared,\n deps.logger,\n );\n\n const now = new Date();\n const deliveries: InsertDelivery[] = resolved\n .filter((decision) => decision.action !== \"skip\")\n .filter((decision) => isChannelUsable(decision.channel, deps.channels, deps.plugins))\n .map((decision) => ({\n id: newId(),\n channel: decision.channel,\n maxAttempts: definition.maxAttempts ?? deps.maxAttempts,\n // `defer` needs no machinery beyond not_before — the claim query\n // already refuses rows whose time has not come. RFC 0004 §4.2.\n notBefore: decision.action === \"defer\" && decision.notBefore ? decision.notBefore : now,\n }));\n\n if (deliveries.length === 0) {\n skipped.push({ userId, reason: \"no-channels\" });\n continue;\n }\n\n const dedupeKey = before.action === \"continue\" ? before.dedupeKey : undefined;\n\n rows.push({\n id: newId(),\n userId,\n type,\n payload: before.payload ?? payload,\n ...(actorId ? { actorId } : {}),\n ...(dedupeKey ? { dedupeKey } : {}),\n deliveries,\n });\n }\n\n if (rows.length === 0) return { notifications: [], skipped };\n\n const { created, deduped } = await deps.adapter.createNotifications(rows);\n const createdSet = new Set(created);\n\n for (const row of rows) {\n if (deduped.includes(row.id)) skipped.push({ userId: row.userId, reason: \"deduped\" });\n }\n\n const notifications = rows\n .filter((row) => createdSet.has(row.id))\n .map((row) => ({\n id: row.id,\n userId: row.userId,\n deliveries: row.deliveries.map((delivery) => ({\n id: delivery.id,\n channel: delivery.channel,\n })),\n }));\n\n for (const plugin of deps.plugins) {\n const hook = plugin.hooks?.afterSend;\n if (!hook) continue;\n try {\n await hook({\n type,\n payload,\n notifications: notifications.map(({ id, userId }) => ({ id, userId })),\n prepared: prepared.get(plugin.id),\n });\n } catch (error) {\n // Fails open: the rows are committed, and throwing here would report a\n // send that actually happened as a failure.\n deps.logger.error(`plugin \"${plugin.id}\" afterSend threw; ignoring`, { type, error });\n }\n }\n\n return { notifications, skipped };\n}\n","import type { Backoff } from \"./backoff\";\nimport type {\n EasyPing,\n EasyPingConfig,\n HealthReport,\n MountedRoute,\n SendArgs,\n SendResult,\n} from \"./config\";\nimport type { NotificationDefinitions } from \"./definition\";\nimport { ConfigError, consoleLogger } from \"./errors\";\nimport { createHandler } from \"./handler\";\nimport type { AnyPlugin } from \"./plugin\";\nimport { createRateLimiter } from \"./rate-limit\";\nimport { createRunner } from \"./runner\";\nimport { isChannelUsable, runSend } from \"./send\";\nimport { createPluginStore } from \"./store\";\nimport { createScopedSigner } from \"./tokens\";\nimport type { DeliveryMode } from \"./types\";\n\nconst CORE_ROUTES: readonly MountedRoute[] = [\n { method: \"GET\", path: \"/\", scope: { type: \"user\" }, owner: \"core\" },\n { method: \"GET\", path: \"/count\", scope: { type: \"user\" }, owner: \"core\" },\n { method: \"POST\", path: \"/seen\", scope: { type: \"user\" }, owner: \"core\" },\n { method: \"POST\", path: \"/read\", scope: { type: \"user\" }, owner: \"core\" },\n { method: \"POST\", path: \"/read-all\", scope: { type: \"user\" }, owner: \"core\" },\n { method: \"POST\", path: \"/cron\", scope: { type: \"machine\" }, owner: \"core\" },\n];\n\nconst DEFAULTS = {\n maxAttempts: 5,\n leaseMs: 60_000,\n batchSize: 20,\n basePath: \"/api/notifications\",\n mode: \"cron\" as DeliveryMode,\n};\n\nexport const MIN_SECRET_LENGTH = 16;\n\nconst PLACEHOLDER_SECRETS = new Set([\n \"secret\",\n \"changeme\",\n \"change-me\",\n \"replace-me\",\n \"replace-me-too\",\n \"password\",\n \"demo-signing-secret\",\n \"demo-cron-secret\",\n]);\n\n/**\n * `secret` keys the HMAC behind every unsubscribe link; `cron.secret` is the\n * only thing between the internet and a flush of your email provider. A short\n * or placeholder value is a startup error, because it ships otherwise.\n */\nfunction checkSecretStrength(name: string, value: string) {\n if (value.length < MIN_SECRET_LENGTH || PLACEHOLDER_SECRETS.has(value.toLowerCase())) {\n throw new ConfigError(\n `\\`${name}\\` must be at least ${MIN_SECRET_LENGTH} characters and not a placeholder. ` +\n \"Generate one with `openssl rand -base64 32`.\",\n );\n }\n}\n\nfunction validate(config: EasyPingConfig<NotificationDefinitions>, plugins: readonly AnyPlugin[]) {\n if (!config.database) throw new ConfigError(\"`database` is required.\");\n if (!config.notifications) throw new ConfigError(\"`notifications` is required.\");\n\n // No default resolver and no dev bypass: the inbox endpoints serve per-user\n // data, and an insecure default ships where a startup crash does not.\n if (typeof config.session?.getUserId !== \"function\") {\n throw new ConfigError(\n \"`session.getUserId` is required — the mounted endpoints serve a user's private inbox \" +\n \"and must resolve the caller. See RFC 0002.\",\n );\n }\n\n if (!config.secret) {\n throw new ConfigError(\n \"`secret` is required — it signs unsubscribe and other session-less links. \" +\n \"Adding it later is a breaking change, so it is mandatory from the start.\",\n );\n }\n checkSecretStrength(\"secret\", config.secret);\n if (config.cron?.secret) checkSecretStrength(\"cron.secret\", config.cron.secret);\n if (config.machineSecret) checkSecretStrength(\"machineSecret\", config.machineSecret);\n\n if (config.rateLimit && (config.rateLimit.max < 1 || config.rateLimit.windowMs < 1)) {\n throw new ConfigError(\"`rateLimit.max` and `rateLimit.windowMs` must both be positive.\");\n }\n\n if (typeof config.getRecipients !== \"function\") {\n throw new ConfigError(\"`getRecipients` is required to resolve emails and timezones.\");\n }\n\n const seen = new Set<string>();\n for (const plugin of plugins) {\n if (seen.has(plugin.id)) {\n throw new ConfigError(`duplicate plugin id \"${plugin.id}\" — ids must be unique.`);\n }\n seen.add(plugin.id);\n }\n\n for (const plugin of plugins) {\n for (const dependency of plugin.dependsOn ?? []) {\n if (!seen.has(dependency)) {\n throw new ConfigError(\n `plugin \"${plugin.id}\" requires \"${dependency}\", which is not registered.`,\n );\n }\n }\n }\n\n // Two plugins claiming the same route silently shadow each other otherwise,\n // and which one wins depends on registration order.\n const routes = new Set<string>();\n for (const plugin of plugins) {\n for (const route of plugin.routes ?? []) {\n const signature = `${route.method} ${route.path}`;\n if (routes.has(signature)) {\n throw new ConfigError(`route collision: two plugins both registered \"${signature}\".`);\n }\n routes.add(signature);\n }\n }\n\n const mode = config.delivery?.mode ?? DEFAULTS.mode;\n if ((mode === \"cron\" || mode === \"deferred\") && !config.cron?.secret) {\n throw new ConfigError(\n `delivery.mode \"${mode}\" relies on the cron sweep, so \\`cron.secret\\` is required. ` +\n \"Left unset the endpoint would be unauthenticated — a free flush-everything trigger \" +\n \"against your email provider. See RFC 0002 §4.\",\n );\n }\n}\n\nexport function easyPing<TDefs extends NotificationDefinitions>(\n config: EasyPingConfig<TDefs>,\n): EasyPing<TDefs> {\n const plugins = config.plugins ?? [];\n validate(config as EasyPingConfig<NotificationDefinitions>, plugins);\n\n const logger = config.logger ?? consoleLogger;\n const tablePrefix = config.tablePrefix ?? \"\";\n const mode = config.delivery?.mode ?? DEFAULTS.mode;\n const leaseMs = config.delivery?.leaseMs ?? DEFAULTS.leaseMs;\n const warnings: string[] = [];\n\n // Below this ratio a slow send is re-claimed mid-flight, and duplicates stop\n // being occasional and become systematic. RFC 0003 §7.\n const providerTimeout = config.channels.email?.provider?.timeoutMs;\n if (providerTimeout !== undefined && providerTimeout >= leaseMs / 2) {\n warnings.push(\n `email provider timeout (${providerTimeout}ms) is at least half of delivery.leaseMs ` +\n `(${leaseMs}ms); raise leaseMs or duplicate sends become systematic.`,\n );\n }\n\n // Declaring `channels: [\"push\"]` used to do nothing at all: no warning, and\n // a skip reason indistinguishable from the user having opted out.\n const unusable = new Map<string, string[]>();\n for (const [type, definition] of Object.entries(config.notifications)) {\n const missing = definition.channels.filter(\n (channel) => !isChannelUsable(channel, config.channels, plugins),\n );\n if (missing.length > 0) unusable.set(type, [...missing]);\n }\n\n for (const [type, channels] of unusable) {\n const dead = channels.length === config.notifications[type]?.channels.length;\n warnings.push(\n `notification \"${type}\" declares ${channels.map((c) => `\"${c}\"`).join(\", \")} ` +\n `but no provider is configured for ${channels.length > 1 ? \"them\" : \"it\"}` +\n (dead ? \" — this type can never be delivered.\" : \"; those channels will be skipped.\"),\n );\n }\n\n if (mode === \"deferred\" && !config.delivery?.waitUntil) {\n warnings.push(\n \"delivery.mode is 'deferred' but no delivery.waitUntil was supplied; \" +\n \"delivery will wait for the cron sweep.\",\n );\n }\n\n // The escape hatch stays visible: every custom-scoped route is named at\n // startup with the reason its author gave. RFC 0002 §3.\n for (const plugin of plugins) {\n for (const route of plugin.routes ?? []) {\n if (route.scope.type === \"custom\") {\n warnings.push(\n `plugin \"${plugin.id}\" mounts ${route.method} ${route.path} with custom auth: ` +\n route.scope.justification,\n );\n }\n }\n }\n\n for (const warning of warnings) logger.warn(warning);\n\n const runner = createRunner({\n adapter: config.database,\n definitions: config.notifications,\n channels: config.channels,\n plugins,\n getRecipients: config.getRecipients,\n logger,\n leaseMs,\n batchSize: config.delivery?.batchSize ?? DEFAULTS.batchSize,\n backoff: (config.delivery?.backoff ?? \"exponential\") as Backoff,\n });\n\n const pluginRoutes = plugins.flatMap((plugin) => plugin.routes ?? []);\n\n const handler = createHandler({\n adapter: config.database,\n session: config.session,\n runner,\n logger,\n secret: config.secret,\n basePath: config.basePath ?? DEFAULTS.basePath,\n pluginRoutes,\n trustedOrigins: config.trustedOrigins,\n onRequest: config.onRequest,\n maxBodyBytes: config.maxBodyBytes,\n rateLimit: config.rateLimit ? createRateLimiter(config.rateLimit) : undefined,\n cronMaxSweeps: config.cron?.maxSweeps,\n ...(config.cron?.secret ? { cronSecret: config.cron.secret } : {}),\n ...((config.machineSecret ?? config.cron?.secret)\n ? { machineSecret: config.machineSecret ?? config.cron?.secret }\n : {}),\n });\n\n async function send<TKey extends keyof TDefs & string>(\n type: TKey,\n args: SendArgs<TDefs, TKey>,\n ): Promise<SendResult> {\n const result = await runSend(\n type,\n {\n to: args.to,\n payload: args.payload,\n actorId: args.actorId,\n dedupeKey: args.dedupeKey,\n overrides: args.overrides,\n },\n {\n adapter: config.database,\n definitions: config.notifications,\n channels: config.channels,\n plugins,\n getRecipients: config.getRecipients,\n maxAttempts: config.delivery?.maxAttempts ?? DEFAULTS.maxAttempts,\n logger,\n },\n );\n\n // The rows are committed by this point, so every branch below only affects\n // latency — never whether the notification survives. RFC 0001 §8.1.\n if (result.notifications.length > 0) {\n // Scoped to this send's deliveries. Unscoped, a single inline send in a\n // request handler flushes the whole backlog.\n const ids = result.notifications.flatMap((notification) =>\n notification.deliveries.map((delivery) => delivery.id),\n );\n\n if (mode === \"inline\") await runner.drain({ ids });\n else if (mode === \"deferred\") runner.scheduleDeferred(config.delivery?.waitUntil, ids);\n }\n\n return result;\n }\n\n // After send() exists, before any hook or route can run.\n for (const plugin of plugins) {\n const purposes = new Set(\n (plugin.routes ?? []).flatMap((route) =>\n route.scope.type === \"signed\" ? [route.scope.purpose] : [],\n ),\n );\n plugin.init?.({\n store: createPluginStore(plugin.id, plugin.schema, config.database, tablePrefix),\n sign: createScopedSigner(\n config.secret,\n plugin.id,\n purposes,\n (message) => new ConfigError(message),\n ),\n logger,\n notificationTypes: Object.keys(config.notifications),\n getRecipients: config.getRecipients,\n send: (type, args) => send(type as keyof TDefs & string, args as never) as Promise<unknown>,\n });\n }\n\n return {\n send,\n handler,\n\n startWorker: (options) => runner.startWorker(options ?? {}),\n\n listRoutes: () => [\n ...CORE_ROUTES.filter((route) => route.path !== \"/cron\" || Boolean(config.cron?.secret)),\n ...plugins.flatMap((plugin) =>\n (plugin.routes ?? []).map((route) => ({\n method: route.method,\n path: route.path,\n scope: route.scope,\n owner: plugin.id,\n })),\n ),\n ],\n\n getFailedDeliveries: (options = {}) =>\n config.database.getFailedDeliveries({\n since: options.since ?? new Date(Date.now() - 24 * 3600_000),\n limit: Math.min(options.limit ?? 100, 1000),\n }),\n\n healthCheck: async (): Promise<HealthReport> => {\n const cronMounted = Boolean(config.cron?.secret);\n return {\n mode,\n cronMounted,\n cronRequiredButMissing:\n !cronMounted && (mode === \"cron\" || mode === \"deferred\" || mode === \"inline\"),\n warnings,\n };\n },\n };\n}\n"]}
|
package/dist/node.cjs
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/core/handler.ts
|
|
4
|
+
var MAX_BODY_BYTES = 64 * 1024;
|
|
5
|
+
new TextEncoder();
|
|
6
|
+
|
|
3
7
|
// src/node/index.ts
|
|
4
|
-
|
|
8
|
+
var BodyTooLargeError = class extends Error {
|
|
9
|
+
};
|
|
10
|
+
function readBody(req, maxBytes) {
|
|
5
11
|
if (req.method === "GET" || req.method === "HEAD") return Promise.resolve(void 0);
|
|
12
|
+
const declared = Number(req.headers["content-length"]);
|
|
13
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
14
|
+
return Promise.reject(new BodyTooLargeError());
|
|
15
|
+
}
|
|
6
16
|
return new Promise((resolve, reject) => {
|
|
7
17
|
const chunks = [];
|
|
8
|
-
|
|
18
|
+
let total = 0;
|
|
19
|
+
req.on("data", (chunk) => {
|
|
20
|
+
total += chunk.length;
|
|
21
|
+
if (total > maxBytes) {
|
|
22
|
+
req.destroy();
|
|
23
|
+
reject(new BodyTooLargeError());
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
chunks.push(chunk);
|
|
27
|
+
});
|
|
9
28
|
req.on("end", () => resolve(chunks.length ? Buffer.concat(chunks) : void 0));
|
|
10
29
|
req.on("error", reject);
|
|
11
30
|
});
|
|
@@ -25,10 +44,12 @@ function toWebRequest(req, body) {
|
|
|
25
44
|
...body ? { body: new Uint8Array(body) } : {}
|
|
26
45
|
});
|
|
27
46
|
}
|
|
28
|
-
function toNodeHandler(handler) {
|
|
47
|
+
function toNodeHandler(handler, options = {}) {
|
|
48
|
+
const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
49
|
+
const onError = options.onError ?? ((error) => console.error("[easy-ping]", error));
|
|
29
50
|
return async (req, res) => {
|
|
30
51
|
try {
|
|
31
|
-
const response = await handler(toWebRequest(req, await readBody(req)));
|
|
52
|
+
const response = await handler(toWebRequest(req, await readBody(req, maxBodyBytes)));
|
|
32
53
|
res.statusCode = response.status;
|
|
33
54
|
response.headers.forEach((value, key) => {
|
|
34
55
|
res.setHeader(key, value);
|
|
@@ -36,9 +57,9 @@ function toNodeHandler(handler) {
|
|
|
36
57
|
const buffer = response.body ? Buffer.from(await response.arrayBuffer()) : null;
|
|
37
58
|
res.end(buffer ?? void 0);
|
|
38
59
|
} catch (error) {
|
|
39
|
-
res.statusCode = 500;
|
|
60
|
+
if (!res.headersSent) res.statusCode = error instanceof BodyTooLargeError ? 413 : 500;
|
|
40
61
|
res.end();
|
|
41
|
-
|
|
62
|
+
if (!(error instanceof BodyTooLargeError)) onError(error);
|
|
42
63
|
}
|
|
43
64
|
};
|
|
44
65
|
}
|
package/dist/node.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/node/index.ts"],"names":[],"mappings":";;;AAKA,SAAS,SAAS,GAAA,EAAmD;AACnE,EAAA,IAAI,GAAA,CAAI,WAAW,KAAA,IAAS,GAAA,CAAI,WAAW,MAAA,EAAQ,OAAO,OAAA,CAAQ,OAAA,CAAQ,MAAS,CAAA;AAEnF,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAM,SAAmB,EAAC;AAC1B,IAAA,GAAA,CAAI,GAAG,MAAA,EAAQ,CAAC,UAAkB,MAAA,CAAO,IAAA,CAAK,KAAK,CAAC,CAAA;AACpD,IAAA,GAAA,CAAI,EAAA,CAAG,KAAA,EAAO,MAAM,OAAA,CAAQ,MAAA,CAAO,MAAA,GAAS,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA,GAAI,MAAS,CAAC,CAAA;AAC9E,IAAA,GAAA,CAAI,EAAA,CAAG,SAAS,MAAM,CAAA;AAAA,EACxB,CAAC,CAAA;AACH;AAEO,SAAS,YAAA,CAAa,KAAsB,IAAA,EAAwB;AAEzE,EAAA,MAAM,QAAA,GAAY,GAAA,CAAI,OAAA,CAAQ,mBAAmB,CAAA,IAA4B,MAAA;AAC7E,EAAA,MAAM,IAAA,GAAO,GAAA,CAAI,OAAA,CAAQ,IAAA,IAAQ,WAAA;AAEjC,EAAA,MAAM,OAAA,GAAU,IAAI,OAAA,EAAQ;AAC5B,EAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,GAAA,CAAI,OAAO,CAAA,EAAG;AACtD,IAAA,IAAI,UAAU,MAAA,EAAW;AACzB,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG,KAAA,MAAW,QAAQ,KAAA,EAAO,OAAA,CAAQ,MAAA,CAAO,GAAA,EAAK,IAAI,CAAA;AAAA,SACvE,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAAA,EAC7B;AAEA,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAA,EAAG,QAAQ,CAAA,GAAA,EAAM,IAAI,CAAA,EAAG,GAAA,CAAI,GAAA,IAAO,GAAG,CAAA,CAAA,EAAI;AAAA,IAC3D,MAAA,EAAQ,IAAI,MAAA,IAAU,KAAA;AAAA,IACtB,OAAA;AAAA,IACA,GAAI,OAAO,EAAE,IAAA,EAAM,IAAI,UAAA,CAAW,IAAI,CAAA,EAAE,GAAI;AAAC,GAC9C,CAAA;AACH;AAEO,SAAS,cAAc,OAAA,EAA+D;AAC3F,EAAA,OAAO,OAAO,KAAK,GAAA,KAAQ;AACzB,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,YAAA,CAAa,KAAK,MAAM,QAAA,CAAS,GAAG,CAAC,CAAC,CAAA;AAErE,MAAA,GAAA,CAAI,aAAa,QAAA,CAAS,MAAA;AAE1B,MAAA,QAAA,CAAS,OAAA,CAAQ,OAAA,CAAQ,CAAC,KAAA,EAAO,GAAA,KAAQ;AACvC,QAAA,GAAA,CAAI,SAAA,CAAU,KAAK,KAAK,CAAA;AAAA,MAC1B,CAAC,CAAA;AAED,MAAA,MAAM,MAAA,GAAS,SAAS,IAAA,GAAO,MAAA,CAAO,KAAK,MAAM,QAAA,CAAS,WAAA,EAAa,CAAA,GAAI,IAAA;AAC3E,MAAA,GAAA,CAAI,GAAA,CAAI,UAAU,KAAA,CAAS,CAAA;AAAA,IAC7B,SAAS,KAAA,EAAO;AAEd,MAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,MAAA,GAAA,CAAI,GAAA,EAAI;AACR,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF,CAAA;AACF","file":"node.cjs","sourcesContent":["import type { IncomingMessage, ServerResponse } from \"node:http\";\n\n/** Adapts the web-standard handler to Node's http/Express signature. */\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nfunction readBody(req: IncomingMessage): Promise<Buffer | undefined> {\n if (req.method === \"GET\" || req.method === \"HEAD\") return Promise.resolve(undefined);\n\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(chunks.length ? Buffer.concat(chunks) : undefined));\n req.on(\"error\", reject);\n });\n}\n\nexport function toWebRequest(req: IncomingMessage, body?: Buffer): Request {\n // Behind a load balancer this is the only signal that the site is https.\n const protocol = (req.headers[\"x-forwarded-proto\"] as string | undefined) ?? \"http\";\n const host = req.headers.host ?? \"localhost\";\n\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) for (const item of value) headers.append(key, item);\n else headers.set(key, value);\n }\n\n return new Request(`${protocol}://${host}${req.url ?? \"/\"}`, {\n method: req.method ?? \"GET\",\n headers,\n ...(body ? { body: new Uint8Array(body) } : {}),\n });\n}\n\nexport function toNodeHandler(handler: (request: Request) => Promise<Response>): NodeHandler {\n return async (req, res) => {\n try {\n const response = await handler(toWebRequest(req, await readBody(req)));\n\n res.statusCode = response.status;\n // forEach: Headers is only iterable with \"DOM.Iterable\" in lib.\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n\n const buffer = response.body ? Buffer.from(await response.arrayBuffer()) : null;\n res.end(buffer ?? undefined);\n } catch (error) {\n // A throw here would leave the socket hanging until the client times out.\n res.statusCode = 500;\n res.end();\n throw error;\n }\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/core/handler.ts","../src/node/index.ts"],"names":[],"mappings":"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","file":"node.cjs","sourcesContent":["import type { DatabaseAdapter } from \"./adapter\";\nimport type { SessionConfig } from \"./config\";\nimport type { Logger } from \"./errors\";\nimport type { Promisable, RouteDefinition } from \"./plugin\";\nimport type { Runner } from \"./runner\";\nimport { verifyToken } from \"./tokens\";\n\nexport type HandlerDeps = {\n adapter: DatabaseAdapter;\n session: SessionConfig;\n runner: Runner;\n logger: Logger;\n basePath: string;\n secret: string;\n cronSecret?: string | undefined;\n /** Guards plugin machine routes. The instance defaults it to cronSecret. */\n machineSecret?: string | undefined;\n cronMaxSweeps?: number | undefined;\n pluginRoutes?: readonly RouteDefinition[] | undefined;\n /** Returns a 429 to send, or null to proceed. Runs after onRequest, before routing. */\n rateLimit?: ((request: Request) => Response | null) | undefined;\n /** Origins other than the request's own host that may POST. `*.example.com` allowed. */\n trustedOrigins?: readonly string[] | undefined;\n /** Runs before routing. Returning a Response short-circuits; use it for rate limiting. */\n // biome-ignore lint/suspicious/noConfusingVoidType: a hook that returns nothing is the common case\n onRequest?: ((request: Request) => Promisable<Response | undefined | void>) | undefined;\n maxBodyBytes?: number | undefined;\n};\n\n/** Every JSON body is capped here. Nothing legitimate the routes accept is bigger. */\nexport const MAX_BODY_BYTES = 64 * 1024;\n\nconst MAX_READ_IDS = 200;\n\n// The inbox is per-user data: never let a shared cache keep it, never let a\n// browser sniff it into something executable.\nconst PRIVATE_HEADERS: Record<string, string> = {\n \"cache-control\": \"private, no-store\",\n \"x-content-type-options\": \"nosniff\",\n};\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\", ...PRIVATE_HEADERS },\n });\n\nconst empty = (status: number) => new Response(null, { status, headers: PRIVATE_HEADERS });\n\n/** Plugin responses get the same headers, without touching their body. */\nfunction harden(response: Response): Response {\n const headers = new Headers(response.headers);\n for (const [name, value] of Object.entries(PRIVATE_HEADERS)) {\n if (!headers.has(name)) headers.set(name, value);\n }\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n\nconst encoder = new TextEncoder();\n\n/**\n * Constant-time comparison over SHA-256 digests. Node's timingSafeEqual is\n * unavailable on edge runtimes, `===` leaks through timing, and comparing the\n * raw strings would still reveal the secret's length.\n */\nasync function timingSafeEqual(a: string, b: string): Promise<boolean> {\n const [left, right] = await Promise.all(\n [a, b].map(\n async (value) => new Uint8Array(await crypto.subtle.digest(\"SHA-256\", encoder.encode(value))),\n ),\n );\n let diff = 0;\n for (let i = 0; i < 32; i += 1) diff |= (left?.[i] ?? 0) ^ (right?.[i] ?? 0);\n return diff === 0;\n}\n\n/** Anchored at a segment boundary: `/foo/api/notifications/cron` is not a cron request. */\nfunction routePath(url: URL, basePath: string): string {\n const { pathname } = url;\n if (pathname === basePath) return \"/\";\n if (pathname.startsWith(`${basePath}/`)) return pathname.slice(basePath.length);\n return pathname;\n}\n\nexport type JsonBody = { body: Record<string, unknown> } | { error: Response };\n\n/**\n * Reads a JSON object body with a byte cap enforced while streaming, so an\n * oversized body is refused before it is buffered. Malformed JSON is 400; an\n * empty body is `{}`. Plugin routes should use this rather than\n * `request.json()`, which has no limit.\n */\nexport async function readJsonBody(\n request: Request,\n maxBytes: number = MAX_BODY_BYTES,\n): Promise<JsonBody> {\n const tooLarge = () => ({ error: json({ error: `body exceeds ${maxBytes} bytes` }, 413) });\n\n const declared = Number(request.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > maxBytes) return tooLarge();\n if (!request.body) return { body: {} };\n\n const reader = request.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n return tooLarge();\n }\n chunks.push(value);\n }\n\n const bytes = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n const text = new TextDecoder().decode(bytes);\n if (text.trim() === \"\") return { body: {} };\n\n try {\n const parsed: unknown = JSON.parse(text);\n return {\n body:\n parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {},\n };\n } catch {\n return { error: json({ error: \"body must be a JSON object\" }, 400) };\n }\n}\n\nconst isJsonContentType = (request: Request) =>\n /^application\\/json\\s*(;|$)/i.test((request.headers.get(\"content-type\") ?? \"\").trim());\n\nfunction originAllowed(origin: string, url: URL, request: Request, trusted: readonly string[]) {\n let host: string;\n try {\n host = new URL(origin).host;\n } catch {\n return false;\n }\n\n if (host === url.host) return true;\n // Behind a proxy that rewrites Host, the public host arrives here instead.\n const forwarded = request.headers.get(\"x-forwarded-host\")?.split(\",\")[0]?.trim();\n if (forwarded && host === forwarded) return true;\n\n return trusted.some((entry) =>\n entry.startsWith(\"*.\") ? host.endsWith(entry.slice(1)) : entry === origin,\n );\n}\n\n/**\n * Two cheap CSRF defences for cookie-authenticated POSTs. A cross-site form\n * cannot send `application/json` without a preflight the browser will fail,\n * and it cannot forge `Origin`. Neither depends on the host app's SameSite\n * policy, which the library cannot see.\n */\nfunction csrfCheck(request: Request, url: URL, trusted: readonly string[]): Response | null {\n if (!isJsonContentType(request)) {\n return json({ error: \"content-type must be application/json\" }, 415);\n }\n\n const origin = request.headers.get(\"origin\");\n if (origin === null) return null;\n if (origin === \"null\" || !originAllowed(origin, url, request, trusted)) return empty(403);\n\n return null;\n}\n\nexport function createHandler(deps: HandlerDeps) {\n const trustedOrigins = deps.trustedOrigins ?? [];\n const maxBodyBytes = deps.maxBodyBytes ?? MAX_BODY_BYTES;\n\n async function resolveUserId(request: Request): Promise<string | null | \"error\"> {\n try {\n return await deps.session.getUserId(request);\n } catch (error) {\n // A broken session lookup is a 500, never a 401. Collapsing them makes\n // an outage look like a mass logout. RFC 0002 §1.\n deps.logger.error(\"session.getUserId threw\", { error });\n return \"error\";\n }\n }\n\n async function bearerMatches(request: Request, secret: string | undefined): Promise<boolean> {\n if (!secret) return false;\n const header = request.headers.get(\"authorization\") ?? \"\";\n const presented = header.startsWith(\"Bearer \") ? header.slice(7) : header;\n return timingSafeEqual(presented, secret);\n }\n\n async function handleCron(request: Request): Promise<Response> {\n if (!deps.cronSecret) return empty(404);\n if (!(await bearerMatches(request, deps.cronSecret))) return empty(401);\n\n // Bounded: an overlapping scheduler must not hold a request open for as\n // long as the backlog lasts.\n const result = await deps.runner.drain(\n deps.cronMaxSweeps === undefined ? {} : { maxSweeps: deps.cronMaxSweeps },\n );\n return json(result);\n }\n\n /**\n * Plugin routes are dispatched before core ones and carry their scope with\n * them. Authentication happens here rather than in the plugin handler, so a\n * plugin physically cannot forget to check a signature or scope a query.\n */\n async function handlePluginRoute(\n route: RouteDefinition,\n request: Request,\n url: URL,\n ): Promise<Response> {\n if (route.scope.type === \"user\") {\n if (request.method === \"POST\") {\n const rejected = csrfCheck(request, url, trustedOrigins);\n if (rejected) return rejected;\n }\n\n const userId = await resolveUserId(request);\n if (userId === \"error\") return empty(500);\n if (userId === null) return empty(401);\n return harden(await route.handler({ request, userId, claims: null, params: {} }));\n }\n\n if (route.scope.type === \"machine\") {\n if (!deps.machineSecret) return empty(404);\n if (!(await bearerMatches(request, deps.machineSecret))) return empty(401);\n return harden(await route.handler({ request, userId: null, claims: null, params: {} }));\n }\n\n if (route.scope.type === \"signed\") {\n const token =\n url.searchParams.get(\"token\") ??\n (request.headers.get(\"authorization\") ?? \"\").replace(/^Bearer /, \"\");\n\n const claims = await verifyToken(deps.secret, token, route.scope.purpose);\n // One response for every failure mode. Distinguishing expired from\n // forged tells an attacker which part to fix. RFC 0002 §5.\n if (!claims) return empty(400);\n\n return harden(await route.handler({ request, userId: claims.uid, claims, params: {} }));\n }\n\n return harden(await route.handler({ request, userId: null, claims: null, params: {} }));\n }\n\n async function route(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const path = routePath(url, deps.basePath);\n\n if (deps.onRequest) {\n const early = await deps.onRequest(request);\n if (early instanceof Response) return early;\n }\n\n if (deps.rateLimit) {\n const limited = deps.rateLimit(request);\n if (limited) return limited;\n }\n\n if (path === \"/cron\") {\n if (request.method !== \"POST\") return empty(405);\n return handleCron(request);\n }\n\n const pluginRoute = deps.pluginRoutes?.find(\n (candidate) => candidate.path === path && candidate.method === request.method,\n );\n if (pluginRoute) return handlePluginRoute(pluginRoute, request, url);\n\n if (request.method === \"POST\") {\n const rejected = csrfCheck(request, url, trustedOrigins);\n if (rejected) return rejected;\n }\n\n const userId = await resolveUserId(request);\n if (userId === \"error\") return empty(500);\n if (userId === null) return empty(401);\n\n if (request.method === \"GET\" && path === \"/\") {\n const limitParam = Number(url.searchParams.get(\"limit\") ?? 20);\n const limit = Number.isFinite(limitParam) ? Math.min(Math.max(limitParam, 1), 100) : 20;\n const cursor = url.searchParams.get(\"cursor\");\n\n // The badge count rides along with the first page, so a polling client\n // needs one request per tick instead of two. Paginating past the first\n // page omits it: a cursor request is scrollback, not a poll.\n const [page, unseenCount] = await Promise.all([\n deps.adapter.listNotifications({\n userId,\n limit,\n ...(cursor ? { cursor } : {}),\n ...(url.searchParams.get(\"unreadOnly\") === \"true\" ? { unreadOnly: true } : {}),\n }),\n cursor ? Promise.resolve(null) : deps.adapter.countUnseen(userId),\n ]);\n\n return json(unseenCount === null ? page : { ...page, unseenCount });\n }\n\n if (request.method === \"GET\" && path === \"/count\") {\n return json({ unseen: await deps.adapter.countUnseen(userId) });\n }\n\n if (request.method === \"POST\" && path === \"/seen\") {\n const parsed = await readJsonBody(request, maxBodyBytes);\n if (\"error\" in parsed) return parsed.error;\n const before = typeof parsed.body.before === \"string\" ? new Date(parsed.body.before) : null;\n\n // Defaults to now, but a client should send the timestamp of the newest\n // notification it has actually rendered. Otherwise anything that arrived\n // between the last poll and the click is marked seen without ever having\n // produced a badge — the user silently misses it.\n //\n // +1ms because Postgres keeps microseconds while an ISO string carries\n // only milliseconds. Without it the truncated cutoff falls *before* the\n // very row it was taken from, and markSeen matches nothing at all.\n const cutoff =\n before && !Number.isNaN(before.getTime()) ? new Date(before.getTime() + 1) : new Date();\n\n await deps.adapter.markSeen(userId, cutoff);\n return json({ ok: true });\n }\n\n if (request.method === \"POST\" && path === \"/read\") {\n const parsed = await readJsonBody(request, maxBodyBytes);\n if (\"error\" in parsed) return parsed.error;\n const ids = Array.isArray(parsed.body.ids)\n ? parsed.body.ids.filter((id): id is string => typeof id === \"string\")\n : [];\n if (ids.length === 0) return json({ error: \"ids must be a non-empty string array\" }, 400);\n\n // Unbounded, this builds a query with as many bind parameters as the\n // caller cares to send. Postgres caps at 65535 and degrades long before.\n if (ids.length > MAX_READ_IDS) {\n return json({ error: `at most ${MAX_READ_IDS} ids per request` }, 400);\n }\n\n // Scoped by userId in the adapter. A miss is 404, not 403 — a 403 would\n // confirm the row exists and belongs to someone else. RFC 0002 §2.\n const updated = await deps.adapter.markRead(userId, ids);\n return updated === 0 ? empty(404) : json({ updated });\n }\n\n if (request.method === \"POST\" && path === \"/read-all\") {\n return json({ updated: await deps.adapter.markAllRead(userId) });\n }\n\n return empty(404);\n }\n\n async function handle(request: Request): Promise<Response> {\n try {\n return await route(request);\n } catch (error) {\n // Never let a driver error escape: on Node it becomes an unhandled\n // rejection, on other hosts a stack trace in the response body.\n deps.logger.error(\"request failed\", { url: request.url, error });\n return empty(500);\n }\n }\n\n return {\n handle,\n GET: handle,\n POST: handle,\n };\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { MAX_BODY_BYTES } from \"../core/handler\";\n\n/** Adapts the web-standard handler to Node's http/Express signature. */\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport type NodeHandlerOptions = {\n /** Defaults to 64 KiB. Enforced here because this reads the raw stream, bypassing body-parser. */\n maxBodyBytes?: number;\n /** Called when the handler itself throws. Defaults to console.error; the response is already a 500. */\n onError?: (error: unknown) => void;\n};\n\nclass BodyTooLargeError extends Error {}\n\nfunction readBody(req: IncomingMessage, maxBytes: number): Promise<Buffer | undefined> {\n if (req.method === \"GET\" || req.method === \"HEAD\") return Promise.resolve(undefined);\n\n const declared = Number(req.headers[\"content-length\"]);\n if (Number.isFinite(declared) && declared > maxBytes) {\n return Promise.reject(new BodyTooLargeError());\n }\n\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let total = 0;\n req.on(\"data\", (chunk: Buffer) => {\n total += chunk.length;\n if (total > maxBytes) {\n req.destroy();\n reject(new BodyTooLargeError());\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(chunks.length ? Buffer.concat(chunks) : undefined));\n req.on(\"error\", reject);\n });\n}\n\nexport function toWebRequest(req: IncomingMessage, body?: Buffer): Request {\n // Behind a load balancer this is the only signal that the site is https.\n const protocol = (req.headers[\"x-forwarded-proto\"] as string | undefined) ?? \"http\";\n const host = req.headers.host ?? \"localhost\";\n\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) for (const item of value) headers.append(key, item);\n else headers.set(key, value);\n }\n\n return new Request(`${protocol}://${host}${req.url ?? \"/\"}`, {\n method: req.method ?? \"GET\",\n headers,\n ...(body ? { body: new Uint8Array(body) } : {}),\n });\n}\n\nexport function toNodeHandler(\n handler: (request: Request) => Promise<Response>,\n options: NodeHandlerOptions = {},\n): NodeHandler {\n const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;\n const onError = options.onError ?? ((error: unknown) => console.error(\"[easy-ping]\", error));\n\n return async (req, res) => {\n try {\n const response = await handler(toWebRequest(req, await readBody(req, maxBodyBytes)));\n\n res.statusCode = response.status;\n // forEach: Headers is only iterable with \"DOM.Iterable\" in lib.\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n\n const buffer = response.body ? Buffer.from(await response.arrayBuffer()) : null;\n res.end(buffer ?? undefined);\n } catch (error) {\n // Respond and swallow. Rethrowing here is an unhandled rejection under\n // Express, and Node terminates the process on those.\n if (!res.headersSent) res.statusCode = error instanceof BodyTooLargeError ? 413 : 500;\n res.end();\n if (!(error instanceof BodyTooLargeError)) onError(error);\n }\n };\n}\n"]}
|
package/dist/node.d.cts
CHANGED
|
@@ -2,7 +2,13 @@ import { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
2
2
|
|
|
3
3
|
/** Adapts the web-standard handler to Node's http/Express signature. */
|
|
4
4
|
type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
5
|
+
type NodeHandlerOptions = {
|
|
6
|
+
/** Defaults to 64 KiB. Enforced here because this reads the raw stream, bypassing body-parser. */
|
|
7
|
+
maxBodyBytes?: number;
|
|
8
|
+
/** Called when the handler itself throws. Defaults to console.error; the response is already a 500. */
|
|
9
|
+
onError?: (error: unknown) => void;
|
|
10
|
+
};
|
|
5
11
|
declare function toWebRequest(req: IncomingMessage, body?: Buffer): Request;
|
|
6
|
-
declare function toNodeHandler(handler: (request: Request) => Promise<Response
|
|
12
|
+
declare function toNodeHandler(handler: (request: Request) => Promise<Response>, options?: NodeHandlerOptions): NodeHandler;
|
|
7
13
|
|
|
8
|
-
export { type NodeHandler, toNodeHandler, toWebRequest };
|
|
14
|
+
export { type NodeHandler, type NodeHandlerOptions, toNodeHandler, toWebRequest };
|
package/dist/node.d.ts
CHANGED
|
@@ -2,7 +2,13 @@ import { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
2
2
|
|
|
3
3
|
/** Adapts the web-standard handler to Node's http/Express signature. */
|
|
4
4
|
type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
5
|
+
type NodeHandlerOptions = {
|
|
6
|
+
/** Defaults to 64 KiB. Enforced here because this reads the raw stream, bypassing body-parser. */
|
|
7
|
+
maxBodyBytes?: number;
|
|
8
|
+
/** Called when the handler itself throws. Defaults to console.error; the response is already a 500. */
|
|
9
|
+
onError?: (error: unknown) => void;
|
|
10
|
+
};
|
|
5
11
|
declare function toWebRequest(req: IncomingMessage, body?: Buffer): Request;
|
|
6
|
-
declare function toNodeHandler(handler: (request: Request) => Promise<Response
|
|
12
|
+
declare function toNodeHandler(handler: (request: Request) => Promise<Response>, options?: NodeHandlerOptions): NodeHandler;
|
|
7
13
|
|
|
8
|
-
export { type NodeHandler, toNodeHandler, toWebRequest };
|
|
14
|
+
export { type NodeHandler, type NodeHandlerOptions, toNodeHandler, toWebRequest };
|
package/dist/node.js
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
|
+
import { MAX_BODY_BYTES } from './chunk-DG3ZL5NP.js';
|
|
2
|
+
import './chunk-PTGHMCCG.js';
|
|
3
|
+
|
|
1
4
|
// src/node/index.ts
|
|
2
|
-
|
|
5
|
+
var BodyTooLargeError = class extends Error {
|
|
6
|
+
};
|
|
7
|
+
function readBody(req, maxBytes) {
|
|
3
8
|
if (req.method === "GET" || req.method === "HEAD") return Promise.resolve(void 0);
|
|
9
|
+
const declared = Number(req.headers["content-length"]);
|
|
10
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
11
|
+
return Promise.reject(new BodyTooLargeError());
|
|
12
|
+
}
|
|
4
13
|
return new Promise((resolve, reject) => {
|
|
5
14
|
const chunks = [];
|
|
6
|
-
|
|
15
|
+
let total = 0;
|
|
16
|
+
req.on("data", (chunk) => {
|
|
17
|
+
total += chunk.length;
|
|
18
|
+
if (total > maxBytes) {
|
|
19
|
+
req.destroy();
|
|
20
|
+
reject(new BodyTooLargeError());
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
chunks.push(chunk);
|
|
24
|
+
});
|
|
7
25
|
req.on("end", () => resolve(chunks.length ? Buffer.concat(chunks) : void 0));
|
|
8
26
|
req.on("error", reject);
|
|
9
27
|
});
|
|
@@ -23,10 +41,12 @@ function toWebRequest(req, body) {
|
|
|
23
41
|
...body ? { body: new Uint8Array(body) } : {}
|
|
24
42
|
});
|
|
25
43
|
}
|
|
26
|
-
function toNodeHandler(handler) {
|
|
44
|
+
function toNodeHandler(handler, options = {}) {
|
|
45
|
+
const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
46
|
+
const onError = options.onError ?? ((error) => console.error("[easy-ping]", error));
|
|
27
47
|
return async (req, res) => {
|
|
28
48
|
try {
|
|
29
|
-
const response = await handler(toWebRequest(req, await readBody(req)));
|
|
49
|
+
const response = await handler(toWebRequest(req, await readBody(req, maxBodyBytes)));
|
|
30
50
|
res.statusCode = response.status;
|
|
31
51
|
response.headers.forEach((value, key) => {
|
|
32
52
|
res.setHeader(key, value);
|
|
@@ -34,9 +54,9 @@ function toNodeHandler(handler) {
|
|
|
34
54
|
const buffer = response.body ? Buffer.from(await response.arrayBuffer()) : null;
|
|
35
55
|
res.end(buffer ?? void 0);
|
|
36
56
|
} catch (error) {
|
|
37
|
-
res.statusCode = 500;
|
|
57
|
+
if (!res.headersSent) res.statusCode = error instanceof BodyTooLargeError ? 413 : 500;
|
|
38
58
|
res.end();
|
|
39
|
-
|
|
59
|
+
if (!(error instanceof BodyTooLargeError)) onError(error);
|
|
40
60
|
}
|
|
41
61
|
};
|
|
42
62
|
}
|
package/dist/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/node/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../src/node/index.ts"],"names":[],"mappings":"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","file":"node.js","sourcesContent":["import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { MAX_BODY_BYTES } from \"../core/handler\";\n\n/** Adapts the web-standard handler to Node's http/Express signature. */\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport type NodeHandlerOptions = {\n /** Defaults to 64 KiB. Enforced here because this reads the raw stream, bypassing body-parser. */\n maxBodyBytes?: number;\n /** Called when the handler itself throws. Defaults to console.error; the response is already a 500. */\n onError?: (error: unknown) => void;\n};\n\nclass BodyTooLargeError extends Error {}\n\nfunction readBody(req: IncomingMessage, maxBytes: number): Promise<Buffer | undefined> {\n if (req.method === \"GET\" || req.method === \"HEAD\") return Promise.resolve(undefined);\n\n const declared = Number(req.headers[\"content-length\"]);\n if (Number.isFinite(declared) && declared > maxBytes) {\n return Promise.reject(new BodyTooLargeError());\n }\n\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let total = 0;\n req.on(\"data\", (chunk: Buffer) => {\n total += chunk.length;\n if (total > maxBytes) {\n req.destroy();\n reject(new BodyTooLargeError());\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(chunks.length ? Buffer.concat(chunks) : undefined));\n req.on(\"error\", reject);\n });\n}\n\nexport function toWebRequest(req: IncomingMessage, body?: Buffer): Request {\n // Behind a load balancer this is the only signal that the site is https.\n const protocol = (req.headers[\"x-forwarded-proto\"] as string | undefined) ?? \"http\";\n const host = req.headers.host ?? \"localhost\";\n\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) for (const item of value) headers.append(key, item);\n else headers.set(key, value);\n }\n\n return new Request(`${protocol}://${host}${req.url ?? \"/\"}`, {\n method: req.method ?? \"GET\",\n headers,\n ...(body ? { body: new Uint8Array(body) } : {}),\n });\n}\n\nexport function toNodeHandler(\n handler: (request: Request) => Promise<Response>,\n options: NodeHandlerOptions = {},\n): NodeHandler {\n const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;\n const onError = options.onError ?? ((error: unknown) => console.error(\"[easy-ping]\", error));\n\n return async (req, res) => {\n try {\n const response = await handler(toWebRequest(req, await readBody(req, maxBodyBytes)));\n\n res.statusCode = response.status;\n // forEach: Headers is only iterable with \"DOM.Iterable\" in lib.\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n\n const buffer = response.body ? Buffer.from(await response.arrayBuffer()) : null;\n res.end(buffer ?? undefined);\n } catch (error) {\n // Respond and swallow. Rethrowing here is an unhandled rejection under\n // Express, and Node terminates the process on those.\n if (!res.headersSent) res.statusCode = error instanceof BodyTooLargeError ? 413 : 500;\n res.end();\n if (!(error instanceof BodyTooLargeError)) onError(error);\n }\n };\n}\n"]}
|
|
@@ -24,6 +24,33 @@ type Logger = {
|
|
|
24
24
|
};
|
|
25
25
|
declare const consoleLogger: Logger;
|
|
26
26
|
|
|
27
|
+
type TokenClaims = {
|
|
28
|
+
/** Subject — the user the token acts for. */
|
|
29
|
+
uid: string;
|
|
30
|
+
/** What the token is allowed to do. Verification requires an exact match. */
|
|
31
|
+
purpose: string;
|
|
32
|
+
/** Expiry, epoch seconds. */
|
|
33
|
+
exp: number;
|
|
34
|
+
/** Issued at, epoch seconds. Set by signToken; lets a route refuse a token older than a later change. */
|
|
35
|
+
iat?: number;
|
|
36
|
+
/** Free-form, purpose-specific. */
|
|
37
|
+
data?: Record<string, string>;
|
|
38
|
+
};
|
|
39
|
+
declare function signToken(secret: string, claims: TokenClaims, now?: Date): Promise<string>;
|
|
40
|
+
/** null for every failure mode. Distinguishing them tells an attacker which part to fix. */
|
|
41
|
+
declare function verifyToken(secret: string, token: string, purpose: string, now?: Date): Promise<TokenClaims | null>;
|
|
42
|
+
declare const DEFAULT_TOKEN_TTL_SECONDS: number;
|
|
43
|
+
/** Ninety days. A link that must outlive this should be re-issued, not made immortal. */
|
|
44
|
+
declare const MAX_TOKEN_TTL_SECONDS: number;
|
|
45
|
+
declare const expiresIn: (seconds: number, now?: Date) => number;
|
|
46
|
+
type SignRequest = {
|
|
47
|
+
uid: string;
|
|
48
|
+
purpose: string;
|
|
49
|
+
data?: Record<string, string>;
|
|
50
|
+
/** Defaults to 30 days, capped at 90. */
|
|
51
|
+
ttlSeconds?: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
27
54
|
type Promisable<T> = T | Promise<T>;
|
|
28
55
|
type PrepareContext = {
|
|
29
56
|
type: string;
|
|
@@ -150,12 +177,7 @@ type RouteContext = {
|
|
|
150
177
|
/** Resolved for user-scoped routes, so a handler cannot forget to scope. */
|
|
151
178
|
userId: string | null;
|
|
152
179
|
/** Verified before dispatch, so a handler cannot forget to check. */
|
|
153
|
-
claims:
|
|
154
|
-
uid: string;
|
|
155
|
-
purpose: string;
|
|
156
|
-
exp: number;
|
|
157
|
-
data?: Record<string, string>;
|
|
158
|
-
} | null;
|
|
180
|
+
claims: TokenClaims | null;
|
|
159
181
|
params: Record<string, string>;
|
|
160
182
|
};
|
|
161
183
|
type RouteDefinition = {
|
|
@@ -168,9 +190,15 @@ type RouteDefinition = {
|
|
|
168
190
|
type PluginInitContext = {
|
|
169
191
|
/** Scoped to the tables this plugin declares in schema(). */
|
|
170
192
|
store: PluginStore;
|
|
171
|
-
/**
|
|
172
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Mints tokens for this plugin's own `signed` routes and nothing else. The
|
|
195
|
+
* master secret itself is never handed out: a plugin cannot forge another
|
|
196
|
+
* plugin's links, or anything a future signed flow relies on.
|
|
197
|
+
*/
|
|
198
|
+
sign: (request: SignRequest) => Promise<string>;
|
|
173
199
|
logger: Logger;
|
|
200
|
+
/** Configured notification names, so a route can refuse to store rows for types that do not exist. */
|
|
201
|
+
notificationTypes: readonly string[];
|
|
174
202
|
/** The app's resolver, for plugins that need timezones or addresses. */
|
|
175
203
|
getRecipients: (userIds: readonly string[]) => Promise<readonly Recipient[]>;
|
|
176
204
|
/**
|
|
@@ -214,4 +242,4 @@ type ExtractActions<TPlugin> = TPlugin extends {
|
|
|
214
242
|
/** Indexes the tuple rather than recursing: a fold blows the instantiation depth limit. */
|
|
215
243
|
type ActionsOf<TPlugins extends readonly AnyClientPlugin[]> = UnionToIntersection<ExtractActions<TPlugins[number]>>;
|
|
216
244
|
|
|
217
|
-
export { type AnyPlugin as A, type BeforeSendContext as B, type ChannelDecision as C, EasyPingError as E, type FieldDeclaration as F, type Logger as L, type
|
|
245
|
+
export { type AnyPlugin as A, type BeforeSendContext as B, type ChannelDecision as C, DEFAULT_TOKEN_TTL_SECONDS as D, EasyPingError as E, type FieldDeclaration as F, type Logger as L, MAX_TOKEN_TTL_SECONDS as M, type Promisable as P, type RouteScope as R, type SchemaDeclaration as S, type TableDeclaration as T, type UnionToIntersection as U, ValidationError as V, type ActionsOf as a, type AfterDeliverContext as b, type AnyClientPlugin as c, type BeforeSendResult as d, type ClientFetch as e, type ClientPlugin as f, ConfigError as g, type EasyPingPlugin as h, type FieldType as i, type PluginHooks as j, type PrepareContext as k, type ResolveChannelsContext as l, type RouteContext as m, type RouteDefinition as n, type SignRequest as o, type TokenClaims as p, consoleLogger as q, defineClientPlugin as r, definePlugin as s, expiresIn as t, signToken as u, verifyToken as v };
|