easy-ping 0.3.0 → 0.5.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.
Files changed (127) hide show
  1. package/README.md +174 -82
  2. package/dist/adapter-B7R4rXtJ.d.cts +17 -0
  3. package/dist/adapter-B7R4rXtJ.d.ts +17 -0
  4. package/dist/{adapter-BfLe2J0t.d.cts → adapter-D3lTy-4E.d.cts} +6 -32
  5. package/dist/{adapter-BfLe2J0t.d.ts → adapter-D3lTy-4E.d.ts} +6 -32
  6. package/dist/adapters/drizzle.cjs +4 -4
  7. package/dist/adapters/drizzle.cjs.map +1 -1
  8. package/dist/adapters/drizzle.d.cts +35 -1
  9. package/dist/adapters/drizzle.d.ts +35 -1
  10. package/dist/adapters/drizzle.js +5 -5
  11. package/dist/adapters/drizzle.js.map +1 -1
  12. package/dist/adapters/mongodb.cjs +8 -7
  13. package/dist/adapters/mongodb.cjs.map +1 -1
  14. package/dist/adapters/mongodb.d.cts +4 -17
  15. package/dist/adapters/mongodb.d.ts +4 -17
  16. package/dist/adapters/mongodb.js +7 -7
  17. package/dist/adapters/mongodb.js.map +1 -1
  18. package/dist/adapters/mysql.cjs +605 -0
  19. package/dist/adapters/mysql.cjs.map +1 -0
  20. package/dist/adapters/mysql.d.cts +38 -0
  21. package/dist/adapters/mysql.d.ts +38 -0
  22. package/dist/adapters/mysql.js +51 -0
  23. package/dist/adapters/mysql.js.map +1 -0
  24. package/dist/adapters/postgres.cjs +19 -11
  25. package/dist/adapters/postgres.cjs.map +1 -1
  26. package/dist/adapters/postgres.d.cts +9 -54
  27. package/dist/adapters/postgres.d.ts +9 -54
  28. package/dist/adapters/postgres.js +6 -41
  29. package/dist/adapters/postgres.js.map +1 -1
  30. package/dist/adapters/sqlite.cjs +593 -0
  31. package/dist/adapters/sqlite.cjs.map +1 -0
  32. package/dist/adapters/sqlite.d.cts +37 -0
  33. package/dist/adapters/sqlite.d.ts +37 -0
  34. package/dist/adapters/sqlite.js +53 -0
  35. package/dist/adapters/sqlite.js.map +1 -0
  36. package/dist/browser.cjs.map +1 -1
  37. package/dist/browser.d.cts +2 -14
  38. package/dist/browser.d.ts +2 -14
  39. package/dist/browser.js.map +1 -1
  40. package/dist/chunk-2EYKPF5A.js +393 -0
  41. package/dist/chunk-2EYKPF5A.js.map +1 -0
  42. package/dist/{chunk-OZSOIX23.js → chunk-3VOQOGDR.js} +54 -12
  43. package/dist/chunk-3VOQOGDR.js.map +1 -0
  44. package/dist/{chunk-NYDVNMKG.js → chunk-AEYYTE5I.js} +4 -3
  45. package/dist/chunk-AEYYTE5I.js.map +1 -0
  46. package/dist/chunk-BECMTNK7.js +315 -0
  47. package/dist/chunk-BECMTNK7.js.map +1 -0
  48. package/dist/chunk-CK5UPVYP.js +48 -0
  49. package/dist/chunk-CK5UPVYP.js.map +1 -0
  50. package/dist/{chunk-BV3U542P.js → chunk-E4IZ2CQC.js} +2 -2
  51. package/dist/chunk-E4IZ2CQC.js.map +1 -0
  52. package/dist/chunk-GJNLOQJS.js +226 -0
  53. package/dist/chunk-GJNLOQJS.js.map +1 -0
  54. package/dist/chunk-HY47TQMY.js +52 -0
  55. package/dist/chunk-HY47TQMY.js.map +1 -0
  56. package/dist/chunk-M4R77BJD.js +64 -0
  57. package/dist/chunk-M4R77BJD.js.map +1 -0
  58. package/dist/{chunk-RBDW4S6F.js → chunk-XIDVLYNQ.js} +14 -4
  59. package/dist/chunk-XIDVLYNQ.js.map +1 -0
  60. package/dist/client.cjs.map +1 -1
  61. package/dist/client.js.map +1 -1
  62. package/dist/index.cjs +362 -64
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +56 -37
  65. package/dist/index.d.ts +56 -37
  66. package/dist/index.js +120 -359
  67. package/dist/index.js.map +1 -1
  68. package/dist/node.cjs +27 -6
  69. package/dist/node.cjs.map +1 -1
  70. package/dist/node.d.cts +8 -2
  71. package/dist/node.d.ts +8 -2
  72. package/dist/node.js +26 -6
  73. package/dist/node.js.map +1 -1
  74. package/dist/{plugin-CmPOFVmn.d.ts → plugin-CHd2ZiGC.d.cts} +37 -25
  75. package/dist/{plugin-f5KaFWSN.d.cts → plugin-Cg9usuUi.d.ts} +37 -25
  76. package/dist/plugins/digests.cjs +1 -4
  77. package/dist/plugins/digests.cjs.map +1 -1
  78. package/dist/plugins/digests.d.cts +6 -31
  79. package/dist/plugins/digests.d.ts +6 -31
  80. package/dist/plugins/digests.js +1 -4
  81. package/dist/plugins/digests.js.map +1 -1
  82. package/dist/plugins/preferences-client.cjs.map +1 -1
  83. package/dist/plugins/preferences-client.d.cts +2 -2
  84. package/dist/plugins/preferences-client.d.ts +2 -2
  85. package/dist/plugins/preferences-client.js +1 -1
  86. package/dist/plugins/preferences-react.cjs.map +1 -1
  87. package/dist/plugins/preferences-react.js.map +1 -1
  88. package/dist/plugins/preferences.cjs +101 -17
  89. package/dist/plugins/preferences.cjs.map +1 -1
  90. package/dist/plugins/preferences.d.cts +4 -2
  91. package/dist/plugins/preferences.d.ts +4 -2
  92. package/dist/plugins/preferences.js +36 -9
  93. package/dist/plugins/preferences.js.map +1 -1
  94. package/dist/plugins/push.cjs +198 -40
  95. package/dist/plugins/push.cjs.map +1 -1
  96. package/dist/plugins/push.d.cts +20 -18
  97. package/dist/plugins/push.d.ts +20 -18
  98. package/dist/plugins/push.js +141 -41
  99. package/dist/plugins/push.js.map +1 -1
  100. package/dist/providers/web-push.cjs +37 -11
  101. package/dist/providers/web-push.cjs.map +1 -1
  102. package/dist/providers/web-push.d.cts +10 -11
  103. package/dist/providers/web-push.d.ts +10 -11
  104. package/dist/providers/web-push.js +37 -12
  105. package/dist/providers/web-push.js.map +1 -1
  106. package/dist/react.cjs.map +1 -1
  107. package/dist/react.d.cts +1 -4
  108. package/dist/react.d.ts +1 -4
  109. package/dist/react.js.map +1 -1
  110. package/dist/schema.cjs +121 -2
  111. package/dist/schema.cjs.map +1 -1
  112. package/dist/schema.d.cts +25 -14
  113. package/dist/schema.d.ts +25 -14
  114. package/dist/schema.js +4 -2
  115. package/dist/testing.cjs +3 -19
  116. package/dist/testing.cjs.map +1 -1
  117. package/dist/testing.d.cts +4 -22
  118. package/dist/testing.d.ts +4 -22
  119. package/dist/testing.js +3 -19
  120. package/dist/testing.js.map +1 -1
  121. package/package.json +12 -1
  122. package/dist/chunk-BV3U542P.js.map +0 -1
  123. package/dist/chunk-NYDVNMKG.js.map +0 -1
  124. package/dist/chunk-OZSOIX23.js.map +0 -1
  125. package/dist/chunk-RBDW4S6F.js.map +0 -1
  126. package/dist/chunk-W3BMEPPQ.js +0 -50
  127. package/dist/chunk-W3BMEPPQ.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/errors.ts","../src/core/store.ts"],"names":[],"mappings":"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","file":"chunk-3VOQOGDR.js","sourcesContent":["export class EasyPingError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"EasyPingError\";\n }\n}\n\n/** Thrown when a payload fails its Standard Schema. Developer error. */\nexport class ValidationError extends EasyPingError {\n readonly issues: readonly { message: string; path?: string }[];\n\n constructor(type: string, issues: readonly { message: string; path?: string }[]) {\n super(`Payload for \"${type}\" failed validation: ${issues.map((i) => i.message).join(\"; \")}`);\n this.name = \"ValidationError\";\n this.issues = issues;\n }\n}\n\n/** Thrown at startup for a malformed config. Never at send time. */\nexport class ConfigError extends EasyPingError {\n constructor(message: string) {\n super(message);\n this.name = \"ConfigError\";\n }\n}\n\nexport type Logger = {\n warn: (message: string, meta?: Record<string, unknown>) => void;\n error: (message: string, meta?: Record<string, unknown>) => void;\n};\n\nexport const consoleLogger: Logger = {\n warn: (message, meta) => console.warn(`[easy-ping] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[easy-ping] ${message}`, meta ?? \"\"),\n};\n","import { toSnakeCase } from \"../schema/declaration\";\nimport { ConfigError } from \"./errors\";\nimport type { FieldDeclaration, SchemaDeclaration, TableDeclaration } from \"./plugin\";\n\nexport type Scalar = string | number | boolean | Date | null;\n\n/** Operators are tagged objects so a bare value is always equality; json columns cannot be filtered. */\nexport type WhereCondition =\n | Scalar\n | { in: readonly (string | number)[] }\n | { lt: Date | number }\n | { lte: Date | number }\n | { gt: Date | number }\n | { gte: Date | number }\n | { not: Scalar };\n\nexport type WhereClause = Record<string, WhereCondition>;\n\nexport type QueryOptions = {\n limit?: number;\n orderBy?: { field: string; direction: \"asc\" | \"desc\" };\n};\n\nexport type UpsertOptions = { onConflict: readonly string[] };\n\n/** Table access for plugins, scoped to the tables the plugin declared. */\nexport type PluginStore = {\n find<T = Record<string, unknown>>(\n table: string,\n where?: WhereClause,\n options?: QueryOptions,\n ): Promise<T[]>;\n insert(table: string, rows: readonly Record<string, unknown>[]): Promise<number>;\n upsert(\n table: string,\n rows: readonly Record<string, unknown>[],\n options: UpsertOptions,\n ): Promise<number>;\n update(table: string, where: WhereClause, set: Record<string, unknown>): Promise<number>;\n remove(table: string, where: WhereClause): Promise<number>;\n};\n\nexport type TableStorage = {\n readonly naming?: \"snake_case\" | \"preserve\";\n readonly serializesJson?: boolean;\n\n queryTable(\n table: string,\n where: WhereClause,\n options: QueryOptions,\n ): Promise<Record<string, unknown>[]>;\n insertRows(\n table: string,\n rows: readonly Record<string, unknown>[],\n onConflict?: readonly string[],\n ): Promise<number>;\n updateRows(table: string, where: WhereClause, set: Record<string, unknown>): Promise<number>;\n deleteRows(table: string, where: WhereClause): Promise<number>;\n};\n\nconst OPERATORS = [\"in\", \"lt\", \"lte\", \"gt\", \"gte\", \"not\"] as const;\n\nexport const isOperator = (value: unknown): boolean =>\n typeof value === \"object\" &&\n value !== null &&\n !(value instanceof Date) &&\n OPERATORS.some((op) => Object.hasOwn(value, op));\n\nconst isScalar = (value: unknown): value is Scalar =>\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n value instanceof Date;\n\n/** A forwarded JSON body can carry `{ $ne: … }`; refuse anything that is not a scalar or a known operator. */\nfunction assertCondition(pluginId: string, field: string, condition: unknown): void {\n if (isScalar(condition)) return;\n\n if (isOperator(condition)) {\n const operator = condition as Record<string, unknown>;\n const keys = Object.keys(operator);\n const [op] = keys;\n if (keys.length !== 1 || op === undefined) {\n throw new ConfigError(`plugin \"${pluginId}\" gave field \"${field}\" more than one operator.`);\n }\n const payload = operator[op];\n const valid =\n op === \"in\"\n ? Array.isArray(payload) &&\n payload.every((item) => typeof item === \"string\" || typeof item === \"number\")\n : isScalar(payload);\n if (valid) return;\n }\n\n throw new ConfigError(\n `plugin \"${pluginId}\" passed a non-scalar value for field \"${field}\"; ` +\n \"validate request bodies before querying with them.\",\n );\n}\n\nfunction assertValue(pluginId: string, field: string, spec: FieldDeclaration, value: unknown) {\n if (value === undefined || isScalar(value)) return;\n if (spec.type === \"json\" && typeof value === \"object\") return;\n throw new ConfigError(\n `plugin \"${pluginId}\" passed a non-scalar value for ${spec.type} field \"${field}\".`,\n );\n}\n\n/** Every table and column is checked against the plugin's own declaration. */\nexport function createPluginStore(\n pluginId: string,\n schema: SchemaDeclaration | undefined,\n storage: TableStorage,\n prefix: string,\n): PluginStore {\n const byTableName = new Map<string, TableDeclaration>();\n for (const declaration of Object.values(schema ?? {})) {\n byTableName.set(declaration.tableName, declaration);\n }\n\n function resolve(table: string): TableDeclaration {\n const declaration = byTableName.get(table);\n if (!declaration) {\n throw new ConfigError(\n `plugin \"${pluginId}\" accessed table \"${table}\", which it does not declare in schema().`,\n );\n }\n return declaration;\n }\n\n function checkFields(table: TableDeclaration, fields: Iterable<string>, context: string) {\n for (const field of fields) {\n // hasOwn, not a lookup: `constructor` and `__proto__` are not columns.\n const spec = Object.hasOwn(table.fields, field) ? table.fields[field] : undefined;\n if (!spec) {\n throw new ConfigError(\n `plugin \"${pluginId}\" used unknown field \"${field}\" on \"${table.tableName}\" (${context}).`,\n );\n }\n if (context === \"where\" && spec.type === \"json\") {\n throw new ConfigError(\n `plugin \"${pluginId}\" cannot filter on json field \"${field}\"; ` +\n \"a bare object value is indistinguishable from an operator.\",\n );\n }\n }\n }\n\n function checkWhere(table: TableDeclaration, where: WhereClause) {\n checkFields(table, Object.keys(where), \"where\");\n for (const [field, condition] of Object.entries(where)) {\n assertCondition(pluginId, field, condition);\n }\n }\n\n function checkRow(table: TableDeclaration, row: Record<string, unknown>, context: string) {\n checkFields(table, Object.keys(row), context);\n for (const [field, value] of Object.entries(row)) {\n const spec = table.fields[field];\n if (spec) assertValue(pluginId, field, spec, value);\n }\n }\n\n const qualified = (table: TableDeclaration) => prefix + table.tableName;\n\n // A document store keeps the declared names; a SQL adapter wants columns.\n const column = (field: string) => (storage.naming === \"preserve\" ? field : toSnakeCase(field));\n\n // json must reach postgres-js as text.\n const serialize = (table: TableDeclaration, row: Record<string, unknown>) => {\n if (storage.serializesJson === false) return row;\n\n const out: Record<string, unknown> = {};\n for (const [field, value] of Object.entries(row)) {\n out[field] =\n table.fields[field]?.type === \"json\" && value !== null && value !== undefined\n ? JSON.stringify(value)\n : value;\n }\n return out;\n };\n\n return {\n async find(table, where = {}, options = {}) {\n const declaration = resolve(table);\n checkWhere(declaration, where);\n if (options.orderBy) checkFields(declaration, [options.orderBy.field], \"orderBy\");\n\n const rows = await storage.queryTable(qualified(declaration), where, options);\n\n // SELECT * returns snake_case; plugins read camelCase.\n return rows.map((row) => {\n const mapped: Record<string, unknown> = {};\n for (const field of Object.keys(declaration.fields)) {\n const value = row[column(field)];\n const type = declaration.fields[field]?.type;\n // Drivers differ on jsonb (parsed or text) and timestamptz (Date or text).\n if (type === \"json\" && typeof value === \"string\") mapped[field] = JSON.parse(value);\n else if (type === \"date\" && typeof value === \"string\") mapped[field] = new Date(value);\n // MySQL and SQLite have no boolean column; they hand back 0 and 1.\n else if (type === \"boolean\" && typeof value === \"number\") mapped[field] = value !== 0;\n else mapped[field] = value;\n }\n return mapped;\n }) as never;\n },\n\n async insert(table, rows) {\n const declaration = resolve(table);\n for (const row of rows) checkRow(declaration, row, \"insert\");\n return storage.insertRows(\n qualified(declaration),\n rows.map((row) => serialize(declaration, row)),\n );\n },\n\n async upsert(table, rows, options) {\n const declaration = resolve(table);\n for (const row of rows) checkRow(declaration, row, \"upsert\");\n checkFields(declaration, options.onConflict, \"onConflict\");\n return storage.insertRows(\n qualified(declaration),\n rows.map((row) => serialize(declaration, row)),\n options.onConflict,\n );\n },\n\n async update(table, where, set) {\n const declaration = resolve(table);\n checkWhere(declaration, where);\n checkRow(declaration, set, \"update\");\n return storage.updateRows(qualified(declaration), where, serialize(declaration, set));\n },\n\n async remove(table, where) {\n const declaration = resolve(table);\n checkWhere(declaration, where);\n return storage.deleteRows(qualified(declaration), where);\n },\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-NYDVNMKG.js.map
62
- //# sourceMappingURL=chunk-NYDVNMKG.js.map
62
+ //# sourceMappingURL=chunk-AEYYTE5I.js.map
63
+ //# sourceMappingURL=chunk-AEYYTE5I.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema/declaration.ts"],"names":[],"mappings":"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","file":"chunk-AEYYTE5I.js","sourcesContent":["import type { SchemaDeclaration } from \"../core/plugin\";\n\nexport const toSnakeCase = (value: string) => value.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\n\n/** Core tables in the plugin format. A drift test asserts it agrees with the Drizzle schema. */\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"]}
@@ -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-BECMTNK7.js.map
315
+ //# sourceMappingURL=chunk-BECMTNK7.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-BECMTNK7.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/** One HMAC key per purpose, derived with HKDF, so an unsubscribe key signs nothing else. */\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 expected purpose's key before the body is parsed. 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/** What a plugin gets instead of the secret: signs only its own declared purposes. */\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// Per-user data: no shared caching, no sniffing.\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/** Constant time over SHA-256 digests, so neither content nor length leaks. Works on edge runtimes. */\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/** Reads a JSON object body with a streaming byte cap. Malformed JSON is 400; an empty body is `{}`. */\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/** CSRF: a cross-site form can neither send `application/json` without a preflight nor forge `Origin`. */\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. 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 so an overlapping scheduler does not hold a request open for the whole backlog.\n const result = await deps.runner.drain(\n deps.cronMaxSweeps === undefined ? {} : { maxSweeps: deps.cronMaxSweeps },\n );\n return json(result);\n }\n\n /** Plugin routes first; auth is enforced here so a plugin cannot forget it. */\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. 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 // unseenCount rides on the first page only: 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 // +1ms: Postgres keeps microseconds, an ISO string milliseconds; without it markSeen misses the newest row.\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 if (ids.length > MAX_READ_IDS) {\n return json({ error: `at most ${MAX_READ_IDS} ids per request` }, 400);\n }\n\n // 404, not 403: a 403 confirms the row exists. 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 // A thrown driver error must not escape: unhandled rejection on Node, stack trace elsewhere.\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"]}
@@ -0,0 +1,48 @@
1
+ // src/adapters/sql/statement.ts
2
+ var defaultBind = (value) => value instanceof Date ? value.toISOString() : value;
3
+ var Statement = class {
4
+ #text = "";
5
+ params = [];
6
+ #placeholder;
7
+ #bind;
8
+ constructor(options = {}) {
9
+ this.#placeholder = options.placeholder ?? "dollar";
10
+ this.#bind = options.bind ?? defaultBind;
11
+ }
12
+ /** Raw SQL. Never user input; use `value`. */
13
+ raw(fragment) {
14
+ this.#text += fragment;
15
+ return this;
16
+ }
17
+ value(value) {
18
+ this.params.push(this.#bind(value));
19
+ this.#text += this.#placeholder === "dollar" ? `$${this.params.length}` : "?";
20
+ return this;
21
+ }
22
+ list(values, separator = ", ") {
23
+ values.forEach((value, index) => {
24
+ if (index > 0) this.raw(separator);
25
+ this.value(value);
26
+ });
27
+ return this;
28
+ }
29
+ /** `($1::text, $2::timestamptz)`. Casts are Postgres-only and dropped elsewhere. */
30
+ tuple(cells) {
31
+ this.raw("(");
32
+ cells.forEach(([value, cast], index) => {
33
+ if (index > 0) this.raw(", ");
34
+ this.value(value);
35
+ if (this.#placeholder === "dollar") this.raw(`::${cast}`);
36
+ });
37
+ return this.raw(")");
38
+ }
39
+ get text() {
40
+ return this.#text;
41
+ }
42
+ };
43
+ var quoteWith = (quoteChar) => (identifier) => `${quoteChar}${identifier.split(quoteChar).join(quoteChar + quoteChar)}${quoteChar}`;
44
+ var quote = quoteWith('"');
45
+
46
+ export { Statement, quote, quoteWith };
47
+ //# sourceMappingURL=chunk-CK5UPVYP.js.map
48
+ //# sourceMappingURL=chunk-CK5UPVYP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/sql/statement.ts"],"names":[],"mappings":";AASA,IAAM,cAAc,CAAC,KAAA,KAAoB,iBAAiB,IAAA,GAAO,KAAA,CAAM,aAAY,GAAI,KAAA;AAGhF,IAAM,YAAN,MAAgB;AAAA,EACrB,KAAA,GAAQ,EAAA;AAAA,EACC,SAAoB,EAAC;AAAA,EACrB,YAAA;AAAA,EACA,KAAA;AAAA,EAET,WAAA,CAAY,OAAA,GAA4B,EAAC,EAAG;AAC1C,IAAA,IAAA,CAAK,YAAA,GAAe,QAAQ,WAAA,IAAe,QAAA;AAC3C,IAAA,IAAA,CAAK,KAAA,GAAQ,QAAQ,IAAA,IAAQ,WAAA;AAAA,EAC/B;AAAA;AAAA,EAGA,IAAI,QAAA,EAAwB;AAC1B,IAAA,IAAA,CAAK,KAAA,IAAS,QAAA;AACd,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,MAAM,KAAA,EAAsB;AAC1B,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,KAAK,CAAC,CAAA;AAClC,IAAA,IAAA,CAAK,KAAA,IAAS,KAAK,YAAA,KAAiB,QAAA,GAAW,IAAI,IAAA,CAAK,MAAA,CAAO,MAAM,CAAA,CAAA,GAAK,GAAA;AAC1E,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,IAAA,CAAK,MAAA,EAA4B,SAAA,GAAY,IAAA,EAAY;AACvD,IAAA,MAAA,CAAO,OAAA,CAAQ,CAAC,KAAA,EAAO,KAAA,KAAU;AAC/B,MAAA,IAAI,KAAA,GAAQ,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,SAAS,CAAA;AACjC,MAAA,IAAA,CAAK,MAAM,KAAK,CAAA;AAAA,IAClB,CAAC,CAAA;AACD,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,KAAA,EAAsD;AAC1D,IAAA,IAAA,CAAK,IAAI,GAAG,CAAA;AACZ,IAAA,KAAA,CAAM,QAAQ,CAAC,CAAC,KAAA,EAAO,IAAI,GAAG,KAAA,KAAU;AACtC,MAAA,IAAI,KAAA,GAAQ,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,IAAI,CAAA;AAC5B,MAAA,IAAA,CAAK,MAAM,KAAK,CAAA;AAChB,MAAA,IAAI,KAAK,YAAA,KAAiB,QAAA,OAAe,GAAA,CAAI,CAAA,EAAA,EAAK,IAAI,CAAA,CAAE,CAAA;AAAA,IAC1D,CAAC,CAAA;AACD,IAAA,OAAO,IAAA,CAAK,IAAI,GAAG,CAAA;AAAA,EACrB;AAAA,EAEA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AACF;AAEO,IAAM,YACX,CAAC,SAAA,KACD,CAAC,UAAA,KACC,GAAG,SAAS,CAAA,EAAG,UAAA,CAAW,KAAA,CAAM,SAAS,CAAA,CAAE,IAAA,CAAK,YAAY,SAAS,CAAC,GAAG,SAAS,CAAA;AAG/E,IAAM,KAAA,GAAQ,UAAU,GAAG","file":"chunk-CK5UPVYP.js","sourcesContent":["export type Placeholder = \"dollar\" | \"question\";\n\nexport type StatementOptions = {\n /** `$1, $2` for Postgres; `?` for MySQL and SQLite. */\n placeholder?: Placeholder;\n /** What the driver is handed for a JS value. Dates become ISO strings by default. */\n bind?: (value: unknown) => unknown;\n};\n\nconst defaultBind = (value: unknown) => (value instanceof Date ? value.toISOString() : value);\n\n/** SQL text plus bound values. Values are never interpolated; identifiers arrive pre-validated. */\nexport class Statement {\n #text = \"\";\n readonly params: unknown[] = [];\n readonly #placeholder: Placeholder;\n readonly #bind: (value: unknown) => unknown;\n\n constructor(options: StatementOptions = {}) {\n this.#placeholder = options.placeholder ?? \"dollar\";\n this.#bind = options.bind ?? defaultBind;\n }\n\n /** Raw SQL. Never user input; use `value`. */\n raw(fragment: string): this {\n this.#text += fragment;\n return this;\n }\n\n value(value: unknown): this {\n this.params.push(this.#bind(value));\n this.#text += this.#placeholder === \"dollar\" ? `$${this.params.length}` : \"?\";\n return this;\n }\n\n list(values: readonly unknown[], separator = \", \"): this {\n values.forEach((value, index) => {\n if (index > 0) this.raw(separator);\n this.value(value);\n });\n return this;\n }\n\n /** `($1::text, $2::timestamptz)`. Casts are Postgres-only and dropped elsewhere. */\n tuple(cells: readonly (readonly [unknown, string])[]): this {\n this.raw(\"(\");\n cells.forEach(([value, cast], index) => {\n if (index > 0) this.raw(\", \");\n this.value(value);\n if (this.#placeholder === \"dollar\") this.raw(`::${cast}`);\n });\n return this.raw(\")\");\n }\n\n get text(): string {\n return this.#text;\n }\n}\n\nexport const quoteWith =\n (quoteChar: '\"' | \"`\") =>\n (identifier: string): string =>\n `${quoteChar}${identifier.split(quoteChar).join(quoteChar + quoteChar)}${quoteChar}`;\n\n/** Double-quoted: Postgres and SQLite. */\nexport const quote = quoteWith('\"');\n"]}
@@ -7,5 +7,5 @@ function defineClientPlugin(plugin) {
7
7
  }
8
8
 
9
9
  export { defineClientPlugin, definePlugin };
10
- //# sourceMappingURL=chunk-BV3U542P.js.map
11
- //# sourceMappingURL=chunk-BV3U542P.js.map
10
+ //# sourceMappingURL=chunk-E4IZ2CQC.js.map
11
+ //# sourceMappingURL=chunk-E4IZ2CQC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/plugin.ts"],"names":[],"mappings":";AAuKO,SAAS,aACd,MAAA,EAC8B;AAC9B,EAAA,OAAO,MAAA;AACT;AAgBO,SAAS,mBACd,MAAA,EAC6B;AAC7B,EAAA,OAAO,MAAA;AACT","file":"chunk-E4IZ2CQC.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 /** Once per send, after commit. The only hook that sees notification ids. Fails open. */\n afterSend?: (ctx: AfterSendContext) => Promisable<void>;\n /** Delivers one of the plugin's declared `channels`. Core carries inApp and email. */\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 /** Mints tokens for this plugin's own `signed` routes only. The secret itself is never handed out. */\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 /** The instance's own send(), so a composed message inherits retry, backoff and idempotency. */\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"]}