easy-ping 0.2.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/{adapter-f0OR2DhY.d.cts → adapter-BfLe2J0t.d.cts} +10 -0
- package/dist/{adapter-f0OR2DhY.d.ts → adapter-BfLe2J0t.d.ts} +10 -0
- package/dist/adapters/drizzle.cjs +9 -4
- package/dist/adapters/drizzle.cjs.map +1 -1
- package/dist/adapters/drizzle.d.cts +35 -1
- package/dist/adapters/drizzle.d.ts +35 -1
- package/dist/adapters/drizzle.js +10 -5
- package/dist/adapters/drizzle.js.map +1 -1
- package/dist/adapters/mongodb.cjs +9 -4
- package/dist/adapters/mongodb.cjs.map +1 -1
- package/dist/adapters/mongodb.d.cts +2 -2
- package/dist/adapters/mongodb.d.ts +2 -2
- package/dist/adapters/mongodb.js +8 -4
- package/dist/adapters/mongodb.js.map +1 -1
- package/dist/adapters/postgres.cjs +136 -3
- package/dist/adapters/postgres.cjs.map +1 -1
- package/dist/adapters/postgres.d.cts +45 -2
- package/dist/adapters/postgres.d.ts +45 -2
- package/dist/adapters/postgres.js +38 -5
- package/dist/adapters/postgres.js.map +1 -1
- 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-YEMRRMJQ.js +199 -0
- package/dist/chunk-YEMRRMJQ.js.map +1 -0
- package/dist/client.cjs +3 -5
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +3 -5
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +377 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -22
- package/dist/index.d.ts +88 -22
- package/dist/index.js +119 -351
- 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-Bqj9xAOS.d.cts → plugin-C9Hoxus2.d.ts} +38 -10
- package/dist/{plugin-bTJKmnlq.d.ts → plugin-CiMsGlQe.d.cts} +38 -10
- package/dist/plugins/digests.d.cts +2 -2
- package/dist/plugins/digests.d.ts +2 -2
- package/dist/plugins/preferences-client.cjs.map +1 -1
- package/dist/plugins/preferences-client.d.cts +2 -2
- package/dist/plugins/preferences-client.d.ts +2 -2
- 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 +4 -2
- package/dist/plugins/preferences.d.ts +4 -2
- package/dist/plugins/preferences.js +35 -7
- package/dist/plugins/preferences.js.map +1 -1
- package/dist/plugins/push.cjs +222 -61
- package/dist/plugins/push.cjs.map +1 -1
- package/dist/plugins/push.d.cts +81 -3
- package/dist/plugins/push.d.ts +81 -3
- package/dist/plugins/push.js +164 -62
- 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 +9 -3
- package/dist/providers/web-push.d.ts +9 -3
- package/dist/providers/web-push.js +37 -12
- package/dist/providers/web-push.js.map +1 -1
- package/dist/react.cjs +3 -5
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +3 -5
- package/dist/react.js.map +1 -1
- package/dist/schema.cjs +2 -1
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +7 -2
- package/dist/schema.d.ts +7 -2
- package/dist/schema.js +2 -198
- package/dist/schema.js.map +1 -1
- package/dist/testing.cjs +29 -0
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +29 -0
- package/dist/testing.js.map +1 -1
- 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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/backoff.ts","../src/core/runner.ts"],"names":[],"mappings":"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","file":"chunk-CYKAM7LE.js","sourcesContent":["/**\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\n/**\n * What gets persisted to `last_error`. Provider responses quote the offending\n * field, which for a bad recipient is the address itself; that column outlives\n * the notification and is read by admin tooling, so addresses do not go in.\n */\nexport const redactErrorMessage = (text: string) =>\n text.replace(/[^\\s\"'<>()[\\]{},;]+@[^\\s\"'<>()[\\]{},;]+/g, \"[redacted-email]\");\n\nconst message = (error: unknown) =>\n redactErrorMessage(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"]}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { encodeBase64Url, decodeBase64UrlBytes, decodeBase64Url } from './chunk-PTGHMCCG.js';
|
|
2
|
+
|
|
3
|
+
// src/core/tokens.ts
|
|
4
|
+
var encoder = new TextEncoder();
|
|
5
|
+
var KEY_SALT = encoder.encode("easy-ping/tokens/v1");
|
|
6
|
+
async function hmacKey(secret, purpose) {
|
|
7
|
+
const master = await crypto.subtle.importKey("raw", encoder.encode(secret), "HKDF", false, [
|
|
8
|
+
"deriveKey"
|
|
9
|
+
]);
|
|
10
|
+
return crypto.subtle.deriveKey(
|
|
11
|
+
{ name: "HKDF", hash: "SHA-256", salt: KEY_SALT, info: encoder.encode(purpose) },
|
|
12
|
+
master,
|
|
13
|
+
{ name: "HMAC", hash: "SHA-256", length: 256 },
|
|
14
|
+
false,
|
|
15
|
+
["sign", "verify"]
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
async function signToken(secret, claims, now = /* @__PURE__ */ new Date()) {
|
|
19
|
+
const stamped = { ...claims, iat: claims.iat ?? Math.floor(now.getTime() / 1e3) };
|
|
20
|
+
const body = encodeBase64Url(JSON.stringify(stamped));
|
|
21
|
+
const signature = await crypto.subtle.sign(
|
|
22
|
+
"HMAC",
|
|
23
|
+
await hmacKey(secret, claims.purpose),
|
|
24
|
+
encoder.encode(body)
|
|
25
|
+
);
|
|
26
|
+
return `${body}.${encodeBase64Url(new Uint8Array(signature))}`;
|
|
27
|
+
}
|
|
28
|
+
async function verifyToken(secret, token, purpose, now = /* @__PURE__ */ new Date()) {
|
|
29
|
+
const separator = token.lastIndexOf(".");
|
|
30
|
+
if (separator <= 0) return null;
|
|
31
|
+
const body = token.slice(0, separator);
|
|
32
|
+
const signature = decodeBase64UrlBytes(token.slice(separator + 1));
|
|
33
|
+
if (!signature) return null;
|
|
34
|
+
const valid = await crypto.subtle.verify(
|
|
35
|
+
"HMAC",
|
|
36
|
+
await hmacKey(secret, purpose),
|
|
37
|
+
signature,
|
|
38
|
+
encoder.encode(body)
|
|
39
|
+
);
|
|
40
|
+
if (!valid) return null;
|
|
41
|
+
const decoded = decodeBase64Url(body);
|
|
42
|
+
if (!decoded) return null;
|
|
43
|
+
let claims;
|
|
44
|
+
try {
|
|
45
|
+
claims = JSON.parse(decoded);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (typeof claims?.uid !== "string" || typeof claims.exp !== "number") return null;
|
|
50
|
+
if (claims.purpose !== purpose) return null;
|
|
51
|
+
if (claims.exp * 1e3 <= now.getTime()) return null;
|
|
52
|
+
if (claims.iat !== void 0 && typeof claims.iat !== "number") return null;
|
|
53
|
+
if (claims.data !== void 0) {
|
|
54
|
+
if (typeof claims.data !== "object" || claims.data === null || Array.isArray(claims.data)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
for (const value of Object.values(claims.data)) {
|
|
58
|
+
if (typeof value !== "string") return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return claims;
|
|
62
|
+
}
|
|
63
|
+
var DEFAULT_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
64
|
+
var MAX_TOKEN_TTL_SECONDS = 90 * 24 * 60 * 60;
|
|
65
|
+
var expiresIn = (seconds, now = /* @__PURE__ */ new Date()) => Math.floor(now.getTime() / 1e3) + Math.min(Math.max(seconds, 0), MAX_TOKEN_TTL_SECONDS);
|
|
66
|
+
function createScopedSigner(secret, pluginId, purposes, onViolation) {
|
|
67
|
+
return (request) => {
|
|
68
|
+
if (!purposes.has(request.purpose)) {
|
|
69
|
+
throw onViolation(
|
|
70
|
+
`plugin "${pluginId}" tried to sign a token for purpose "${request.purpose}", which none of its signed routes declare.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return signToken(secret, {
|
|
74
|
+
uid: request.uid,
|
|
75
|
+
purpose: request.purpose,
|
|
76
|
+
exp: expiresIn(request.ttlSeconds ?? DEFAULT_TOKEN_TTL_SECONDS),
|
|
77
|
+
...request.data ? { data: request.data } : {}
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/core/handler.ts
|
|
83
|
+
var MAX_BODY_BYTES = 64 * 1024;
|
|
84
|
+
var MAX_READ_IDS = 200;
|
|
85
|
+
var PRIVATE_HEADERS = {
|
|
86
|
+
"cache-control": "private, no-store",
|
|
87
|
+
"x-content-type-options": "nosniff"
|
|
88
|
+
};
|
|
89
|
+
var json = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
90
|
+
status,
|
|
91
|
+
headers: { "content-type": "application/json", ...PRIVATE_HEADERS }
|
|
92
|
+
});
|
|
93
|
+
var empty = (status) => new Response(null, { status, headers: PRIVATE_HEADERS });
|
|
94
|
+
function harden(response) {
|
|
95
|
+
const headers = new Headers(response.headers);
|
|
96
|
+
for (const [name, value] of Object.entries(PRIVATE_HEADERS)) {
|
|
97
|
+
if (!headers.has(name)) headers.set(name, value);
|
|
98
|
+
}
|
|
99
|
+
return new Response(response.body, {
|
|
100
|
+
status: response.status,
|
|
101
|
+
statusText: response.statusText,
|
|
102
|
+
headers
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
var encoder2 = new TextEncoder();
|
|
106
|
+
async function timingSafeEqual(a, b) {
|
|
107
|
+
const [left, right] = await Promise.all(
|
|
108
|
+
[a, b].map(
|
|
109
|
+
async (value) => new Uint8Array(await crypto.subtle.digest("SHA-256", encoder2.encode(value)))
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
let diff = 0;
|
|
113
|
+
for (let i = 0; i < 32; i += 1) diff |= (left?.[i] ?? 0) ^ (right?.[i] ?? 0);
|
|
114
|
+
return diff === 0;
|
|
115
|
+
}
|
|
116
|
+
function routePath(url, basePath) {
|
|
117
|
+
const { pathname } = url;
|
|
118
|
+
if (pathname === basePath) return "/";
|
|
119
|
+
if (pathname.startsWith(`${basePath}/`)) return pathname.slice(basePath.length);
|
|
120
|
+
return pathname;
|
|
121
|
+
}
|
|
122
|
+
async function readJsonBody(request, maxBytes = MAX_BODY_BYTES) {
|
|
123
|
+
const tooLarge = () => ({ error: json({ error: `body exceeds ${maxBytes} bytes` }, 413) });
|
|
124
|
+
const declared = Number(request.headers.get("content-length"));
|
|
125
|
+
if (Number.isFinite(declared) && declared > maxBytes) return tooLarge();
|
|
126
|
+
if (!request.body) return { body: {} };
|
|
127
|
+
const reader = request.body.getReader();
|
|
128
|
+
const chunks = [];
|
|
129
|
+
let total = 0;
|
|
130
|
+
for (; ; ) {
|
|
131
|
+
const { done, value } = await reader.read();
|
|
132
|
+
if (done) break;
|
|
133
|
+
total += value.byteLength;
|
|
134
|
+
if (total > maxBytes) {
|
|
135
|
+
await reader.cancel();
|
|
136
|
+
return tooLarge();
|
|
137
|
+
}
|
|
138
|
+
chunks.push(value);
|
|
139
|
+
}
|
|
140
|
+
const bytes = new Uint8Array(total);
|
|
141
|
+
let offset = 0;
|
|
142
|
+
for (const chunk of chunks) {
|
|
143
|
+
bytes.set(chunk, offset);
|
|
144
|
+
offset += chunk.byteLength;
|
|
145
|
+
}
|
|
146
|
+
const text = new TextDecoder().decode(bytes);
|
|
147
|
+
if (text.trim() === "") return { body: {} };
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(text);
|
|
150
|
+
return {
|
|
151
|
+
body: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
|
|
152
|
+
};
|
|
153
|
+
} catch {
|
|
154
|
+
return { error: json({ error: "body must be a JSON object" }, 400) };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
var isJsonContentType = (request) => /^application\/json\s*(;|$)/i.test((request.headers.get("content-type") ?? "").trim());
|
|
158
|
+
function originAllowed(origin, url, request, trusted) {
|
|
159
|
+
let host;
|
|
160
|
+
try {
|
|
161
|
+
host = new URL(origin).host;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (host === url.host) return true;
|
|
166
|
+
const forwarded = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
|
|
167
|
+
if (forwarded && host === forwarded) return true;
|
|
168
|
+
return trusted.some(
|
|
169
|
+
(entry) => entry.startsWith("*.") ? host.endsWith(entry.slice(1)) : entry === origin
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
function csrfCheck(request, url, trusted) {
|
|
173
|
+
if (!isJsonContentType(request)) {
|
|
174
|
+
return json({ error: "content-type must be application/json" }, 415);
|
|
175
|
+
}
|
|
176
|
+
const origin = request.headers.get("origin");
|
|
177
|
+
if (origin === null) return null;
|
|
178
|
+
if (origin === "null" || !originAllowed(origin, url, request, trusted)) return empty(403);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function createHandler(deps) {
|
|
182
|
+
const trustedOrigins = deps.trustedOrigins ?? [];
|
|
183
|
+
const maxBodyBytes = deps.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
184
|
+
async function resolveUserId(request) {
|
|
185
|
+
try {
|
|
186
|
+
return await deps.session.getUserId(request);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
deps.logger.error("session.getUserId threw", { error });
|
|
189
|
+
return "error";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function bearerMatches(request, secret) {
|
|
193
|
+
if (!secret) return false;
|
|
194
|
+
const header = request.headers.get("authorization") ?? "";
|
|
195
|
+
const presented = header.startsWith("Bearer ") ? header.slice(7) : header;
|
|
196
|
+
return timingSafeEqual(presented, secret);
|
|
197
|
+
}
|
|
198
|
+
async function handleCron(request) {
|
|
199
|
+
if (!deps.cronSecret) return empty(404);
|
|
200
|
+
if (!await bearerMatches(request, deps.cronSecret)) return empty(401);
|
|
201
|
+
const result = await deps.runner.drain(
|
|
202
|
+
deps.cronMaxSweeps === void 0 ? {} : { maxSweeps: deps.cronMaxSweeps }
|
|
203
|
+
);
|
|
204
|
+
return json(result);
|
|
205
|
+
}
|
|
206
|
+
async function handlePluginRoute(route2, request, url) {
|
|
207
|
+
if (route2.scope.type === "user") {
|
|
208
|
+
if (request.method === "POST") {
|
|
209
|
+
const rejected = csrfCheck(request, url, trustedOrigins);
|
|
210
|
+
if (rejected) return rejected;
|
|
211
|
+
}
|
|
212
|
+
const userId = await resolveUserId(request);
|
|
213
|
+
if (userId === "error") return empty(500);
|
|
214
|
+
if (userId === null) return empty(401);
|
|
215
|
+
return harden(await route2.handler({ request, userId, claims: null, params: {} }));
|
|
216
|
+
}
|
|
217
|
+
if (route2.scope.type === "machine") {
|
|
218
|
+
if (!deps.machineSecret) return empty(404);
|
|
219
|
+
if (!await bearerMatches(request, deps.machineSecret)) return empty(401);
|
|
220
|
+
return harden(await route2.handler({ request, userId: null, claims: null, params: {} }));
|
|
221
|
+
}
|
|
222
|
+
if (route2.scope.type === "signed") {
|
|
223
|
+
const token = url.searchParams.get("token") ?? (request.headers.get("authorization") ?? "").replace(/^Bearer /, "");
|
|
224
|
+
const claims = await verifyToken(deps.secret, token, route2.scope.purpose);
|
|
225
|
+
if (!claims) return empty(400);
|
|
226
|
+
return harden(await route2.handler({ request, userId: claims.uid, claims, params: {} }));
|
|
227
|
+
}
|
|
228
|
+
return harden(await route2.handler({ request, userId: null, claims: null, params: {} }));
|
|
229
|
+
}
|
|
230
|
+
async function route(request) {
|
|
231
|
+
const url = new URL(request.url);
|
|
232
|
+
const path = routePath(url, deps.basePath);
|
|
233
|
+
if (deps.onRequest) {
|
|
234
|
+
const early = await deps.onRequest(request);
|
|
235
|
+
if (early instanceof Response) return early;
|
|
236
|
+
}
|
|
237
|
+
if (deps.rateLimit) {
|
|
238
|
+
const limited = deps.rateLimit(request);
|
|
239
|
+
if (limited) return limited;
|
|
240
|
+
}
|
|
241
|
+
if (path === "/cron") {
|
|
242
|
+
if (request.method !== "POST") return empty(405);
|
|
243
|
+
return handleCron(request);
|
|
244
|
+
}
|
|
245
|
+
const pluginRoute = deps.pluginRoutes?.find(
|
|
246
|
+
(candidate) => candidate.path === path && candidate.method === request.method
|
|
247
|
+
);
|
|
248
|
+
if (pluginRoute) return handlePluginRoute(pluginRoute, request, url);
|
|
249
|
+
if (request.method === "POST") {
|
|
250
|
+
const rejected = csrfCheck(request, url, trustedOrigins);
|
|
251
|
+
if (rejected) return rejected;
|
|
252
|
+
}
|
|
253
|
+
const userId = await resolveUserId(request);
|
|
254
|
+
if (userId === "error") return empty(500);
|
|
255
|
+
if (userId === null) return empty(401);
|
|
256
|
+
if (request.method === "GET" && path === "/") {
|
|
257
|
+
const limitParam = Number(url.searchParams.get("limit") ?? 20);
|
|
258
|
+
const limit = Number.isFinite(limitParam) ? Math.min(Math.max(limitParam, 1), 100) : 20;
|
|
259
|
+
const cursor = url.searchParams.get("cursor");
|
|
260
|
+
const [page, unseenCount] = await Promise.all([
|
|
261
|
+
deps.adapter.listNotifications({
|
|
262
|
+
userId,
|
|
263
|
+
limit,
|
|
264
|
+
...cursor ? { cursor } : {},
|
|
265
|
+
...url.searchParams.get("unreadOnly") === "true" ? { unreadOnly: true } : {}
|
|
266
|
+
}),
|
|
267
|
+
cursor ? Promise.resolve(null) : deps.adapter.countUnseen(userId)
|
|
268
|
+
]);
|
|
269
|
+
return json(unseenCount === null ? page : { ...page, unseenCount });
|
|
270
|
+
}
|
|
271
|
+
if (request.method === "GET" && path === "/count") {
|
|
272
|
+
return json({ unseen: await deps.adapter.countUnseen(userId) });
|
|
273
|
+
}
|
|
274
|
+
if (request.method === "POST" && path === "/seen") {
|
|
275
|
+
const parsed = await readJsonBody(request, maxBodyBytes);
|
|
276
|
+
if ("error" in parsed) return parsed.error;
|
|
277
|
+
const before = typeof parsed.body.before === "string" ? new Date(parsed.body.before) : null;
|
|
278
|
+
const cutoff = before && !Number.isNaN(before.getTime()) ? new Date(before.getTime() + 1) : /* @__PURE__ */ new Date();
|
|
279
|
+
await deps.adapter.markSeen(userId, cutoff);
|
|
280
|
+
return json({ ok: true });
|
|
281
|
+
}
|
|
282
|
+
if (request.method === "POST" && path === "/read") {
|
|
283
|
+
const parsed = await readJsonBody(request, maxBodyBytes);
|
|
284
|
+
if ("error" in parsed) return parsed.error;
|
|
285
|
+
const ids = Array.isArray(parsed.body.ids) ? parsed.body.ids.filter((id) => typeof id === "string") : [];
|
|
286
|
+
if (ids.length === 0) return json({ error: "ids must be a non-empty string array" }, 400);
|
|
287
|
+
if (ids.length > MAX_READ_IDS) {
|
|
288
|
+
return json({ error: `at most ${MAX_READ_IDS} ids per request` }, 400);
|
|
289
|
+
}
|
|
290
|
+
const updated = await deps.adapter.markRead(userId, ids);
|
|
291
|
+
return updated === 0 ? empty(404) : json({ updated });
|
|
292
|
+
}
|
|
293
|
+
if (request.method === "POST" && path === "/read-all") {
|
|
294
|
+
return json({ updated: await deps.adapter.markAllRead(userId) });
|
|
295
|
+
}
|
|
296
|
+
return empty(404);
|
|
297
|
+
}
|
|
298
|
+
async function handle(request) {
|
|
299
|
+
try {
|
|
300
|
+
return await route(request);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
deps.logger.error("request failed", { url: request.url, error });
|
|
303
|
+
return empty(500);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
handle,
|
|
308
|
+
GET: handle,
|
|
309
|
+
POST: handle
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export { DEFAULT_TOKEN_TTL_SECONDS, MAX_BODY_BYTES, MAX_TOKEN_TTL_SECONDS, createHandler, createScopedSigner, expiresIn, readJsonBody, signToken, verifyToken };
|
|
314
|
+
//# sourceMappingURL=chunk-DG3ZL5NP.js.map
|
|
315
|
+
//# sourceMappingURL=chunk-DG3ZL5NP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/tokens.ts","../src/core/handler.ts"],"names":["encoder","route"],"mappings":"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","file":"chunk-DG3ZL5NP.js","sourcesContent":["import { decodeBase64Url, decodeBase64UrlBytes, encodeBase64Url } from \"./base64url\";\n\nexport type TokenClaims = {\n /** Subject — the user the token acts for. */\n uid: string;\n /** What the token is allowed to do. Verification requires an exact match. */\n purpose: string;\n /** Expiry, epoch seconds. */\n exp: number;\n /** Issued at, epoch seconds. Set by signToken; lets a route refuse a token older than a later change. */\n iat?: number;\n /** Free-form, purpose-specific. */\n data?: Record<string, string>;\n};\n\nconst encoder = new TextEncoder();\n\n// Changing this invalidates every outstanding token. Bump it on purpose only.\nconst KEY_SALT = encoder.encode(\"easy-ping/tokens/v1\");\n\n/**\n * One HMAC key per purpose, derived from the master secret with HKDF. A key\n * that can sign unsubscribe links cannot sign anything else, so a plugin\n * handed the unsubscribe key gets exactly that and no more.\n */\nasync function hmacKey(secret: string, purpose: string): Promise<CryptoKey> {\n const master = await crypto.subtle.importKey(\"raw\", encoder.encode(secret), \"HKDF\", false, [\n \"deriveKey\",\n ]);\n return crypto.subtle.deriveKey(\n { name: \"HKDF\", hash: \"SHA-256\", salt: KEY_SALT, info: encoder.encode(purpose) },\n master,\n { name: \"HMAC\", hash: \"SHA-256\", length: 256 },\n false,\n [\"sign\", \"verify\"],\n );\n}\n\nexport async function signToken(\n secret: string,\n claims: TokenClaims,\n now: Date = new Date(),\n): Promise<string> {\n const stamped: TokenClaims = { ...claims, iat: claims.iat ?? Math.floor(now.getTime() / 1000) };\n const body = encodeBase64Url(JSON.stringify(stamped));\n const signature = await crypto.subtle.sign(\n \"HMAC\",\n await hmacKey(secret, claims.purpose),\n encoder.encode(body),\n );\n return `${body}.${encodeBase64Url(new Uint8Array(signature))}`;\n}\n\n/** null for every failure mode. Distinguishing them tells an attacker which part to fix. */\nexport async function verifyToken(\n secret: string,\n token: string,\n purpose: string,\n now: Date = new Date(),\n): Promise<TokenClaims | null> {\n const separator = token.lastIndexOf(\".\");\n if (separator <= 0) return null;\n\n const body = token.slice(0, separator);\n const signature = decodeBase64UrlBytes(token.slice(separator + 1));\n if (!signature) return null;\n\n // Verified under the key for the purpose this route expects, before the\n // body is even parsed. A token minted for another purpose fails here.\n // crypto.subtle.verify is constant-time.\n const valid = await crypto.subtle.verify(\n \"HMAC\",\n await hmacKey(secret, purpose),\n signature,\n encoder.encode(body),\n );\n if (!valid) return null;\n\n const decoded = decodeBase64Url(body);\n if (!decoded) return null;\n\n let claims: TokenClaims;\n try {\n claims = JSON.parse(decoded) as TokenClaims;\n } catch {\n return null;\n }\n\n if (typeof claims?.uid !== \"string\" || typeof claims.exp !== \"number\") return null;\n if (claims.purpose !== purpose) return null;\n if (claims.exp * 1000 <= now.getTime()) return null;\n if (claims.iat !== undefined && typeof claims.iat !== \"number\") return null;\n\n if (claims.data !== undefined) {\n if (typeof claims.data !== \"object\" || claims.data === null || Array.isArray(claims.data)) {\n return null;\n }\n for (const value of Object.values(claims.data)) {\n if (typeof value !== \"string\") return null;\n }\n }\n\n return claims;\n}\n\nexport const DEFAULT_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;\n/** Ninety days. A link that must outlive this should be re-issued, not made immortal. */\nexport const MAX_TOKEN_TTL_SECONDS = 90 * 24 * 60 * 60;\n\nexport const expiresIn = (seconds: number, now: Date = new Date()): number =>\n Math.floor(now.getTime() / 1000) + Math.min(Math.max(seconds, 0), MAX_TOKEN_TTL_SECONDS);\n\nexport type SignRequest = {\n uid: string;\n purpose: string;\n data?: Record<string, string>;\n /** Defaults to 30 days, capped at 90. */\n ttlSeconds?: number;\n};\n\n/**\n * What a plugin gets instead of the secret: a signer that only mints tokens\n * for the purposes its own `signed` routes declared.\n */\nexport function createScopedSigner(\n secret: string,\n pluginId: string,\n purposes: ReadonlySet<string>,\n onViolation: (message: string) => Error,\n): (request: SignRequest) => Promise<string> {\n return (request) => {\n if (!purposes.has(request.purpose)) {\n throw onViolation(\n `plugin \"${pluginId}\" tried to sign a token for purpose \"${request.purpose}\", ` +\n \"which none of its signed routes declare.\",\n );\n }\n return signToken(secret, {\n uid: request.uid,\n purpose: request.purpose,\n exp: expiresIn(request.ttlSeconds ?? DEFAULT_TOKEN_TTL_SECONDS),\n ...(request.data ? { data: request.data } : {}),\n });\n };\n}\n","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"]}
|
|
@@ -51,12 +51,13 @@ var coreSchema = {
|
|
|
51
51
|
type: { type: "string", required: true },
|
|
52
52
|
channel: { type: "string", required: true },
|
|
53
53
|
enabled: { type: "boolean", required: true, default: true },
|
|
54
|
-
frequency: { type: "string", required: true, default: "instant" }
|
|
54
|
+
frequency: { type: "string", required: true, default: "instant" },
|
|
55
|
+
updatedAt: { type: "date", required: true, defaultNow: true }
|
|
55
56
|
},
|
|
56
57
|
primaryKey: ["userId", "type", "channel"]
|
|
57
58
|
}
|
|
58
59
|
};
|
|
59
60
|
|
|
60
61
|
export { coreSchema, toSnakeCase };
|
|
61
|
-
//# sourceMappingURL=chunk-
|
|
62
|
-
//# sourceMappingURL=chunk-
|
|
62
|
+
//# sourceMappingURL=chunk-PSJHXZPR.js.map
|
|
63
|
+
//# sourceMappingURL=chunk-PSJHXZPR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema/declaration.ts"],"names":[],"mappings":";AAEO,IAAM,WAAA,GAAc,CAAC,KAAA,KAAkB,KAAA,CAAM,OAAA,CAAQ,QAAA,EAAU,CAAC,CAAA,KAAM,CAAA,CAAA,EAAI,CAAA,CAAE,WAAA,EAAa,CAAA,CAAE;AAO3F,IAAM,UAAA,GAAa;AAAA,EACxB,YAAA,EAAc;AAAA,IACZ,SAAA,EAAW,cAAA;AAAA,IACX,MAAA,EAAQ;AAAA,MACN,EAAA,EAAI,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACrC,MAAA,EAAQ,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACzC,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACvC,OAAA,EAAS,EAAE,IAAA,EAAM,MAAA,EAAQ,UAAU,IAAA,EAAK;AAAA,MACxC,OAAA,EAAS,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MAC1B,QAAA,EAAU,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MAC3B,SAAA,EAAW,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MAC5B,MAAA,EAAQ,EAAE,IAAA,EAAM,MAAA,EAAO;AAAA,MACvB,MAAA,EAAQ,EAAE,IAAA,EAAM,MAAA,EAAO;AAAA,MACvB,UAAA,EAAY,EAAE,IAAA,EAAM,MAAA,EAAO;AAAA,MAC3B,WAAW,EAAE,IAAA,EAAM,QAAQ,QAAA,EAAU,IAAA,EAAM,YAAY,IAAA;AAAK,KAC9D;AAAA,IACA,UAAA,EAAY,CAAC,IAAI,CAAA;AAAA,IACjB,OAAA,EAAS;AAAA,MACP,EAAE,EAAA,EAAI,CAAC,UAAU,WAAW,CAAA,EAAG,MAAM,uBAAA,EAAwB;AAAA,MAC7D,EAAE,EAAA,EAAI,CAAC,UAAU,UAAU,CAAA,EAAG,MAAM,wBAAA,EAAyB;AAAA,MAC7D,EAAE,IAAI,CAAC,QAAA,EAAU,WAAW,CAAA,EAAG,MAAA,EAAQ,IAAA,EAAM,IAAA,EAAM,yBAAA;AAA0B;AAC/E,GACF;AAAA,EAEA,oBAAA,EAAsB;AAAA,IACpB,SAAA,EAAW,uBAAA;AAAA,IACX,MAAA,EAAQ;AAAA,MACN,EAAA,EAAI,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACrC,cAAA,EAAgB,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACjD,OAAA,EAAS,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MAC1C,QAAQ,EAAE,IAAA,EAAM,UAAU,QAAA,EAAU,IAAA,EAAM,SAAS,SAAA,EAAU;AAAA,MAC7D,UAAU,EAAE,IAAA,EAAM,UAAU,QAAA,EAAU,IAAA,EAAM,SAAS,CAAA,EAAE;AAAA,MACvD,aAAa,EAAE,IAAA,EAAM,UAAU,QAAA,EAAU,IAAA,EAAM,SAAS,CAAA,EAAE;AAAA,MAC1D,WAAW,EAAE,IAAA,EAAM,QAAQ,QAAA,EAAU,IAAA,EAAM,YAAY,IAAA,EAAK;AAAA,MAC5D,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAO;AAAA,MAC1B,SAAA,EAAW,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MAC5B,SAAA,EAAW,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MAC5B,WAAW,EAAE,IAAA,EAAM,QAAQ,QAAA,EAAU,IAAA,EAAM,YAAY,IAAA;AAAK,KAC9D;AAAA,IACA,UAAA,EAAY,CAAC,IAAI,CAAA;AAAA,IACjB,OAAA,EAAS;AAAA,MACP,EAAE,EAAA,EAAI,CAAC,UAAU,WAAW,CAAA,EAAG,MAAM,oBAAA,EAAqB;AAAA,MAC1D,EAAE,EAAA,EAAI,CAAC,gBAAgB,CAAA,EAAG,MAAM,2BAAA;AAA4B;AAC9D,GACF;AAAA,EAEA,sBAAA,EAAwB;AAAA,IACtB,SAAA,EAAW,yBAAA;AAAA,IACX,MAAA,EAAQ;AAAA,MACN,MAAA,EAAQ,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACzC,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACvC,OAAA,EAAS,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MAC1C,SAAS,EAAE,IAAA,EAAM,WAAW,QAAA,EAAU,IAAA,EAAM,SAAS,IAAA,EAAK;AAAA,MAC1D,WAAW,EAAE,IAAA,EAAM,UAAU,QAAA,EAAU,IAAA,EAAM,SAAS,SAAA,EAAU;AAAA,MAChE,WAAW,EAAE,IAAA,EAAM,QAAQ,QAAA,EAAU,IAAA,EAAM,YAAY,IAAA;AAAK,KAC9D;AAAA,IACA,UAAA,EAAY,CAAC,QAAA,EAAU,MAAA,EAAQ,SAAS;AAAA;AAE5C","file":"chunk-PSJHXZPR.js","sourcesContent":["import type { SchemaDeclaration } from \"../core/plugin\";\n\nexport const toSnakeCase = (value: string) => value.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\n\n/**\n * Core tables in the format plugins use. Less expressive than the hand-written\n * Drizzle schema (no partial indexes, ordering or cascade) — a drift test\n * asserts the two agree on the fields they share.\n */\nexport const coreSchema = {\n notification: {\n tableName: \"notification\",\n fields: {\n id: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n type: { type: \"string\", required: true },\n payload: { type: \"json\", required: true },\n actorId: { type: \"string\" },\n groupKey: { type: \"string\" },\n dedupeKey: { type: \"string\" },\n seenAt: { type: \"date\" },\n readAt: { type: \"date\" },\n archivedAt: { type: \"date\" },\n createdAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"userId\", \"createdAt\"], name: \"notification_feed_idx\" },\n { on: [\"userId\", \"groupKey\"], name: \"notification_group_idx\" },\n { on: [\"userId\", \"dedupeKey\"], unique: true, name: \"notification_dedupe_idx\" },\n ],\n },\n\n notificationDelivery: {\n tableName: \"notification_delivery\",\n fields: {\n id: { type: \"string\", required: true },\n notificationId: { type: \"string\", required: true },\n channel: { type: \"string\", required: true },\n status: { type: \"string\", required: true, default: \"pending\" },\n attempts: { type: \"number\", required: true, default: 0 },\n maxAttempts: { type: \"number\", required: true, default: 5 },\n notBefore: { type: \"date\", required: true, defaultNow: true },\n claimedAt: { type: \"date\" },\n claimedBy: { type: \"string\" },\n lastError: { type: \"string\" },\n updatedAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"status\", \"notBefore\"], name: \"delivery_claim_idx\" },\n { on: [\"notificationId\"], name: \"delivery_notification_idx\" },\n ],\n },\n\n notificationPreference: {\n tableName: \"notification_preference\",\n fields: {\n userId: { type: \"string\", required: true },\n type: { type: \"string\", required: true },\n channel: { type: \"string\", required: true },\n enabled: { type: \"boolean\", required: true, default: true },\n frequency: { type: \"string\", required: true, default: \"instant\" },\n updatedAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"userId\", \"type\", \"channel\"],\n },\n} satisfies SchemaDeclaration;\n"]}
|
|
@@ -7,5 +7,5 @@ function defineClientPlugin(plugin) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export { defineClientPlugin, definePlugin };
|
|
10
|
-
//# sourceMappingURL=chunk-
|
|
11
|
-
//# sourceMappingURL=chunk-
|
|
10
|
+
//# sourceMappingURL=chunk-W6I3B2CI.js.map
|
|
11
|
+
//# sourceMappingURL=chunk-W6I3B2CI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/plugin.ts"],"names":[],"mappings":";AAuLO,SAAS,aACd,MAAA,EAC8B;AAC9B,EAAA,OAAO,MAAA;AACT;AAgBO,SAAS,mBACd,MAAA,EAC6B;AAC7B,EAAA,OAAO,MAAA;AACT","file":"chunk-W6I3B2CI.js","sourcesContent":["import type { DeliveryOutcome } from \"./adapter\";\nimport type { Logger } from \"./errors\";\nimport type { PluginStore } from \"./store\";\nimport type { SignRequest, TokenClaims } from \"./tokens\";\nimport type { Channel, Recipient } from \"./types\";\n\nexport type Promisable<T> = T | Promise<T>;\n\nexport type PrepareContext = {\n type: string;\n recipients: readonly Recipient[];\n};\n\nexport type BeforeSendContext = {\n type: string;\n recipient: Recipient;\n payload: unknown;\n actorId: string | null;\n dedupeKey: string | null;\n /** Whatever this plugin's own prepare() returned for this send. */\n prepared: unknown;\n};\n\nexport type BeforeSendResult =\n | { action: \"continue\"; payload?: unknown; dedupeKey?: string }\n | { action: \"cancel\"; reason: string };\n\nexport type ChannelDecision = {\n channel: Channel;\n action: \"send\" | \"skip\" | \"defer\";\n /** defer only. Written to notification_delivery.not_before. */\n notBefore?: Date | undefined;\n};\n\nexport type ResolveChannelsContext = {\n type: string;\n recipient: Recipient;\n payload: unknown;\n decisions: readonly ChannelDecision[];\n /** Whatever this plugin's own prepare() returned for this send. */\n prepared: unknown;\n};\n\nexport type DeliverContext = {\n channel: Channel;\n deliveryId: string;\n attempt: number;\n recipient: Recipient;\n notification: { id: string; userId: string; type: string; payload: unknown };\n /** Aborted on the runner's timeout. */\n signal: AbortSignal;\n};\n\nexport type AfterSendContext = {\n type: string;\n payload: unknown;\n /** Committed rows. Ids exist only at this point. */\n notifications: readonly { id: string; userId: string }[];\n /** Whatever this plugin's own prepare() returned for this send. */\n prepared: unknown;\n};\n\nexport type AfterDeliverContext = {\n deliveryId: string;\n notificationId: string;\n userId: string;\n channel: Channel;\n outcome: DeliveryOutcome;\n attempt: number;\n isTerminal: boolean;\n};\n\n/** beforeSend and resolveChannels fail closed; afterDeliver fails open. */\nexport type PluginHooks = {\n /** Once per send, for bulk loading — the per-recipient hooks would be an N+1. */\n prepare?: (ctx: PrepareContext) => Promisable<unknown>;\n beforeSend?: (ctx: BeforeSendContext) => Promisable<BeforeSendResult>;\n resolveChannels?: (ctx: ResolveChannelsContext) => Promisable<readonly ChannelDecision[]>;\n /**\n * Once per send, after the rows are committed. The only hook that sees\n * notification ids, which digests needs to bucket an entry against one.\n * Fails open: the notification is already written.\n */\n afterSend?: (ctx: AfterSendContext) => Promisable<void>;\n /**\n * Delivers one of the plugin's declared `channels`. Core carries inApp and\n * email; everything else arrives here, which is how push, sms and slack are\n * added without core learning about them.\n */\n deliver?: (ctx: DeliverContext) => Promisable<DeliveryOutcome>;\n afterDeliver?: (ctx: AfterDeliverContext) => Promisable<void>;\n};\n\nexport type FieldType = \"string\" | \"number\" | \"boolean\" | \"date\" | \"json\";\n\nexport type FieldDeclaration = {\n type: FieldType;\n required?: boolean;\n default?: string | number | boolean;\n /** date fields only. Renders as defaultNow() / CURRENT_TIMESTAMP. */\n defaultNow?: boolean;\n};\n\nexport type IndexDeclaration = {\n on: readonly string[];\n unique?: boolean;\n name?: string;\n};\n\nexport type TableDeclaration = {\n tableName: string;\n fields: Record<string, FieldDeclaration>;\n primaryKey?: readonly string[];\n indexes?: readonly IndexDeclaration[];\n};\n\nexport type SchemaDeclaration = Record<string, TableDeclaration>;\n\nexport type RouteScope =\n | { type: \"user\" }\n | { type: \"machine\" }\n | { type: \"signed\"; purpose: string }\n | { type: \"custom\"; justification: string };\n\nexport type RouteContext = {\n request: Request;\n /** Resolved for user-scoped routes, so a handler cannot forget to scope. */\n userId: string | null;\n /** Verified before dispatch, so a handler cannot forget to check. */\n claims: TokenClaims | null;\n params: Record<string, string>;\n};\n\nexport type RouteDefinition = {\n path: string;\n method: \"GET\" | \"POST\";\n scope: RouteScope;\n handler: (ctx: RouteContext) => Promise<Response>;\n};\n\n/** Handed to a plugin at startup so it need not be given the database twice. */\nexport type PluginInitContext = {\n /** Scoped to the tables this plugin declares in schema(). */\n store: PluginStore;\n /**\n * Mints tokens for this plugin's own `signed` routes and nothing else. The\n * master secret itself is never handed out: a plugin cannot forge another\n * plugin's links, or anything a future signed flow relies on.\n */\n sign: (request: SignRequest) => Promise<string>;\n logger: Logger;\n /** Configured notification names, so a route can refuse to store rows for types that do not exist. */\n notificationTypes: readonly string[];\n /** The app's resolver, for plugins that need timezones or addresses. */\n getRecipients: (userIds: readonly string[]) => Promise<readonly Recipient[]>;\n /**\n * The instance's own send(). A plugin composing a message (a digest, say)\n * routes it back through the pipeline rather than reaching for a provider,\n * so it inherits retry, backoff and idempotency for free.\n */\n send: (\n type: string,\n args: { to: string | readonly string[]; payload: unknown; dedupeKey?: string },\n ) => Promise<unknown>;\n};\n\nexport type EasyPingPlugin<\n TId extends string = string,\n TSchema extends SchemaDeclaration = SchemaDeclaration,\n> = {\n id: TId;\n dependsOn?: readonly string[];\n schema?: TSchema;\n routes?: readonly RouteDefinition[];\n hooks?: PluginHooks;\n /** Channels this plugin delivers. Makes them usable in a notification. */\n channels?: readonly Channel[];\n /** Called once by easyPing() before any hook or route can run. */\n init?: (ctx: PluginInitContext) => void;\n};\n\nexport type AnyPlugin = EasyPingPlugin<string, SchemaDeclaration>;\n\nexport function definePlugin<TId extends string, TSchema extends SchemaDeclaration>(\n plugin: EasyPingPlugin<TId, TSchema>,\n): EasyPingPlugin<TId, TSchema> {\n return plugin;\n}\n\nexport type ClientFetch = (path: string, init?: RequestInit) => Promise<unknown>;\n\nexport type ClientPlugin<\n TId extends string = string,\n TActions extends Record<string, unknown> = Record<string, unknown>,\n> = {\n id: TId;\n /** Phantom. Links client types to the server plugin so a mismatch is a type error. */\n $InferServerPlugin?: AnyPlugin;\n actions: (fetch: ClientFetch) => TActions;\n};\n\nexport type AnyClientPlugin = ClientPlugin<string, Record<string, unknown>>;\n\nexport function defineClientPlugin<TId extends string, TActions extends Record<string, unknown>>(\n plugin: ClientPlugin<TId, TActions>,\n): ClientPlugin<TId, TActions> {\n return plugin;\n}\n\nexport type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (\n k: infer I,\n) => void\n ? I\n : never;\n\n/** Naked type parameter, so the conditional distributes over the union. */\ntype ExtractActions<TPlugin> = TPlugin extends { actions: (fetch: never) => infer TActions }\n ? TActions\n : never;\n\n/** Indexes the tuple rather than recursing: a fold blows the instantiation depth limit. */\nexport type ActionsOf<TPlugins extends readonly AnyClientPlugin[]> = UnionToIntersection<\n ExtractActions<TPlugins[number]>\n>;\n"]}
|