@rei-standard/amsg-server 2.6.0-next.2 → 2.6.0-next.21

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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +599 -24
  3. package/dist/adapters/d1.d.ts +416 -0
  4. package/dist/adapters/factory.d.ts +39 -0
  5. package/dist/adapters/interface.d.ts +306 -0
  6. package/dist/adapters/neon.d.ts +88 -0
  7. package/dist/adapters/pg-shared.d.ts +161 -0
  8. package/dist/adapters/pg.d.ts +84 -0
  9. package/dist/adapters/schema.d.ts +57 -0
  10. package/dist/adapters/schema.sqlite.d.ts +49 -0
  11. package/dist/chunk-7NMQFTDJ.cjs +148 -0
  12. package/dist/chunk-E7OWP3VL.cjs +135 -0
  13. package/dist/chunk-GN44PST5.mjs +148 -0
  14. package/dist/chunk-KOJQVOYS.mjs +5847 -0
  15. package/dist/chunk-QHVID3V2.cjs +5847 -0
  16. package/dist/chunk-ZGGF4GMA.mjs +135 -0
  17. package/dist/cloudflare/single-user-worker.d.ts +37 -0
  18. package/dist/cloudflare.cjs +39 -2
  19. package/dist/cloudflare.d.ts +10 -2
  20. package/dist/cloudflare.mjs +40 -3
  21. package/dist/handlers/cancel-message.d.ts +3 -0
  22. package/dist/handlers/capabilities.d.ts +4 -0
  23. package/dist/handlers/client-state.d.ts +7 -0
  24. package/dist/handlers/get-message.d.ts +3 -0
  25. package/dist/handlers/get-user-key.d.ts +3 -0
  26. package/dist/handlers/init-tenant.d.ts +23 -0
  27. package/dist/handlers/llm-credentials.d.ts +5 -0
  28. package/dist/handlers/messages.d.ts +3 -0
  29. package/dist/handlers/outbox.d.ts +7 -0
  30. package/dist/handlers/push-subscription.d.ts +5 -0
  31. package/dist/handlers/schedule-message.d.ts +3 -0
  32. package/dist/handlers/send-notifications.d.ts +3 -0
  33. package/dist/handlers/single-user-init.d.ts +13 -0
  34. package/dist/handlers/update-message.d.ts +3 -0
  35. package/dist/handlers/vapid-public-key.d.ts +19 -0
  36. package/dist/index.cjs +133 -44
  37. package/dist/index.d.cts +131 -766
  38. package/dist/index.d.ts +131 -766
  39. package/dist/index.mjs +125 -36
  40. package/dist/lib/agentic-fire.d.ts +108 -0
  41. package/dist/lib/client-state-store.d.ts +80 -0
  42. package/dist/lib/constant-time.d.ts +14 -0
  43. package/dist/lib/db-errors.d.ts +10 -0
  44. package/dist/lib/encryption.d.ts +48 -0
  45. package/dist/lib/errors.d.ts +215 -0
  46. package/dist/lib/llm-credentials-store.d.ts +147 -0
  47. package/dist/lib/llm.d.ts +1 -0
  48. package/dist/lib/message-processor.d.ts +73 -0
  49. package/dist/lib/outbox-store.d.ts +95 -0
  50. package/dist/lib/push-subscription-store.d.ts +94 -0
  51. package/dist/lib/recurrence.d.ts +51 -0
  52. package/dist/lib/request.d.ts +182 -0
  53. package/dist/lib/result-emitter.d.ts +54 -0
  54. package/dist/lib/run-tick.d.ts +115 -0
  55. package/dist/lib/schema-version.d.ts +50 -0
  56. package/dist/lib/state-accessors.d.ts +70 -0
  57. package/dist/lib/state-chunks.d.ts +55 -0
  58. package/dist/lib/task-projection.d.ts +92 -0
  59. package/dist/lib/validation.d.ts +97 -0
  60. package/dist/lib/version.d.ts +7 -0
  61. package/dist/lib/webcrypto-utils.d.ts +1 -0
  62. package/dist/lib/webpush-webcrypto.d.ts +78 -0
  63. package/dist/{neon-BFUS25UX.cjs → neon-KP2CPA57.cjs} +110 -10
  64. package/dist/{neon-CU5N3CSW.mjs → neon-ZIMHALYI.mjs} +106 -6
  65. package/dist/{pg-QO6NKTGL.mjs → pg-QGC2XHUT.mjs} +94 -5
  66. package/dist/{pg-IIH3M4OM.cjs → pg-RZPQGXR3.cjs} +98 -9
  67. package/dist/single-user.d.ts +95 -0
  68. package/dist/tenant/blob-store.d.ts +30 -0
  69. package/dist/tenant/context.d.ts +47 -0
  70. package/dist/tenant/single-user-context.d.ts +29 -0
  71. package/dist/tenant/token.d.ts +26 -0
  72. package/package.json +3 -3
  73. package/dist/chunk-5ENJGVZX.mjs +0 -72
  74. package/dist/chunk-GN5QTMCL.cjs +0 -1870
  75. package/dist/chunk-PVNLB6TF.mjs +0 -1870
  76. package/dist/chunk-RGECD4OH.cjs +0 -72
  77. package/dist/cloudflare-B-E51VjP.d.cts +0 -2848
  78. package/dist/cloudflare-B-E51VjP.d.ts +0 -2848
  79. package/dist/cloudflare.d.cts +0 -2
  80. package/dist/neon-CJl66EGy.d.cts +0 -251
  81. package/dist/neon-DYvGnCzx.d.ts +0 -251
  82. package/dist/pg-07S-u_H4.d.cts +0 -246
  83. package/dist/pg-Du-pN_UT.d.ts +0 -246
  84. package/dist/schema-C8OnYk6j.d.cts +0 -74
  85. package/dist/schema-C8OnYk6j.d.ts +0 -74
package/dist/index.mjs CHANGED
@@ -1,8 +1,36 @@
1
1
  import {
2
+ CORS_ALLOW_HEADERS,
3
+ CORS_ALLOW_METHODS,
4
+ CRED_ID_MAX_LENGTH,
5
+ CRED_PUT_BATCH_MAX,
6
+ CRED_REFS_MAX_ENTRIES,
7
+ CRED_ROWS_PER_USER_MAX,
8
+ DEFAULT_CLAIM_LEASE_MS,
9
+ DEFAULT_HEARTBEAT_LEASE_TTL_MS,
10
+ DEFAULT_LEASE_HEARTBEAT_MS,
11
+ DEFAULT_MAX_REQUEST_BODY_BYTES,
12
+ DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE,
13
+ DEFAULT_MAX_TOOL_ITERATIONS,
14
+ DEFAULT_TOTAL_TIMEOUT_MS,
15
+ DeploymentConfigError,
16
+ MAX_PUSH_PAYLOAD_BYTES,
17
+ MAX_TASK_PAYLOAD_BYTES,
18
+ MIN_SCHEDULE_LEAD_MS,
19
+ NonRetryableError,
20
+ PUSH_ENVELOPE_RESERVED_BYTES,
21
+ SCHEMA_VERSION,
22
+ WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
23
+ WEB_PUSH_MAX_BODY_BYTES,
24
+ advanceOccurrence,
25
+ base64UrlToBytes,
26
+ bytesToBase64Url,
2
27
  createCancelMessageHandler,
3
28
  createD1Adapter,
29
+ createGetMessageHandler,
4
30
  createGetUserKeyHandler,
31
+ createLlmCredentialsHandler,
5
32
  createMessagesHandler,
33
+ createPushSubscriptionHandler,
6
34
  createScheduleMessageHandler,
7
35
  createSingleUserCloudflareWorker,
8
36
  createSingleUserServer,
@@ -12,18 +40,39 @@ import {
12
40
  decryptPayload,
13
41
  deriveUserEncryptionKey,
14
42
  encryptForStorage,
43
+ ensureSchema,
15
44
  getHeader,
45
+ getSchemaVersion,
46
+ hasChatCredRef,
47
+ isNonRetryableError,
48
+ isValidCredId,
16
49
  isValidISO8601,
50
+ isValidTimeZoneId,
17
51
  isValidUUID,
18
52
  isValidUUIDv4,
19
53
  isValidUrl,
54
+ measurePushPayload,
55
+ nextFutureOccurrence,
20
56
  parseJsonBody,
57
+ planNextOccurrence,
58
+ readRequestBody,
59
+ resolveLlmCredential,
21
60
  runScheduledTick,
61
+ runTask,
62
+ sanitizeErrorSummary,
63
+ summarizeErrorCause,
64
+ supportsLlmCredentialsStore,
65
+ timingSafeEqualBytes,
66
+ utf8,
67
+ utf8Decode,
22
68
  validateAvatarUrl,
69
+ validateCredRefs,
70
+ validateCredValue,
23
71
  validateLlmMessagesArray,
24
72
  validateScheduleMessagePayload,
25
73
  validateSplitPattern
26
- } from "./chunk-PVNLB6TF.mjs";
74
+ } from "./chunk-KOJQVOYS.mjs";
75
+ import "./chunk-GN44PST5.mjs";
27
76
 
28
77
  // src/server/adapters/factory.js
29
78
  async function createAdapter(config) {
@@ -39,11 +88,11 @@ async function createAdapter(config) {
39
88
  }
40
89
  switch (config.driver) {
41
90
  case "neon": {
42
- const { NeonAdapter } = await import("./neon-CU5N3CSW.mjs");
91
+ const { NeonAdapter } = await import("./neon-ZIMHALYI.mjs");
43
92
  return new NeonAdapter(config.connectionString);
44
93
  }
45
94
  case "pg": {
46
- const { PgAdapter } = await import("./pg-QO6NKTGL.mjs");
95
+ const { PgAdapter } = await import("./pg-QGC2XHUT.mjs");
47
96
  return new PgAdapter(config.connectionString);
48
97
  }
49
98
  default:
@@ -212,14 +261,6 @@ function createSendNotificationsHandler(ctx) {
212
261
  // src/server/tenant/blob-store.js
213
262
  import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
214
263
  var inMemoryNamespaces = /* @__PURE__ */ new Map();
215
- function base64UrlEncode(input) {
216
- return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
217
- }
218
- function base64UrlDecode(input) {
219
- const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
220
- const padLength = (4 - normalized.length % 4) % 4;
221
- return Buffer.from(normalized + "=".repeat(padLength), "base64");
222
- }
223
264
  function getKekBuffer(kek) {
224
265
  const value = String(kek || "").trim();
225
266
  if (!value) {
@@ -233,16 +274,16 @@ function encryptConfig(config, kekBuffer) {
233
274
  const plaintext = Buffer.from(JSON.stringify(config), "utf8");
234
275
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
235
276
  const authTag = cipher.getAuthTag();
236
- return `v1.${base64UrlEncode(iv)}.${base64UrlEncode(authTag)}.${base64UrlEncode(ciphertext)}`;
277
+ return `v1.${bytesToBase64Url(iv)}.${bytesToBase64Url(authTag)}.${bytesToBase64Url(ciphertext)}`;
237
278
  }
238
279
  function decryptConfig(encrypted, kekBuffer) {
239
280
  const parts = String(encrypted || "").split(".");
240
281
  if (parts.length !== 4 || parts[0] !== "v1") {
241
282
  throw new Error("INVALID_TENANT_CONFIG");
242
283
  }
243
- const iv = base64UrlDecode(parts[1]);
244
- const authTag = base64UrlDecode(parts[2]);
245
- const ciphertext = base64UrlDecode(parts[3]);
284
+ const iv = base64UrlToBytes(parts[1]);
285
+ const authTag = base64UrlToBytes(parts[2]);
286
+ const ciphertext = base64UrlToBytes(parts[3]);
246
287
  const decipher = createDecipheriv("aes-256-gcm", kekBuffer, iv);
247
288
  decipher.setAuthTag(authTag);
248
289
  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
@@ -308,17 +349,9 @@ function createTenantBlobStore(options) {
308
349
  import { createHash as createHash2, randomBytes as randomBytes2, randomUUID } from "crypto";
309
350
 
310
351
  // src/server/tenant/token.js
311
- import { createHmac, timingSafeEqual } from "crypto";
312
- function base64UrlEncode2(input) {
313
- return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
314
- }
315
- function base64UrlDecode2(input) {
316
- const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
317
- const padLength = (4 - normalized.length % 4) % 4;
318
- return Buffer.from(normalized + "=".repeat(padLength), "base64");
319
- }
352
+ import { createHmac } from "crypto";
320
353
  function sign(input, secret) {
321
- return base64UrlEncode2(createHmac("sha256", secret).update(input).digest());
354
+ return bytesToBase64Url(createHmac("sha256", secret).update(input).digest());
322
355
  }
323
356
  function nowEpochSeconds() {
324
357
  return Math.floor(Date.now() / 1e3);
@@ -334,8 +367,8 @@ function createTenantToken(params, secret) {
334
367
  v: 1
335
368
  };
336
369
  const header = { alg: "HS256", typ: "JWT" };
337
- const encodedHeader = base64UrlEncode2(JSON.stringify(header));
338
- const encodedPayload = base64UrlEncode2(JSON.stringify(payload));
370
+ const encodedHeader = bytesToBase64Url(utf8(JSON.stringify(header)));
371
+ const encodedPayload = bytesToBase64Url(utf8(JSON.stringify(payload)));
339
372
  const signingInput = `${encodedHeader}.${encodedPayload}`;
340
373
  const signature = sign(signingInput, secret);
341
374
  return `${signingInput}.${signature}`;
@@ -351,20 +384,20 @@ function verifyTenantToken(token, secret, options = {}) {
351
384
  const [encodedHeader, encodedPayload, receivedSignature] = parts;
352
385
  const signingInput = `${encodedHeader}.${encodedPayload}`;
353
386
  const expectedSignature = sign(signingInput, secret);
354
- let receivedBuffer;
355
- let expectedBuffer;
387
+ let receivedBytes;
388
+ let expectedBytes;
356
389
  try {
357
- receivedBuffer = base64UrlDecode2(receivedSignature);
358
- expectedBuffer = base64UrlDecode2(expectedSignature);
390
+ receivedBytes = base64UrlToBytes(receivedSignature);
391
+ expectedBytes = base64UrlToBytes(expectedSignature);
359
392
  } catch {
360
393
  throw new Error("INVALID_TENANT_AUTH");
361
394
  }
362
- if (receivedBuffer.length !== expectedBuffer.length || !timingSafeEqual(receivedBuffer, expectedBuffer)) {
395
+ if (!timingSafeEqualBytes(receivedBytes, expectedBytes)) {
363
396
  throw new Error("INVALID_TENANT_AUTH");
364
397
  }
365
398
  let payload;
366
399
  try {
367
- payload = JSON.parse(base64UrlDecode2(encodedPayload).toString("utf8"));
400
+ payload = JSON.parse(utf8Decode(base64UrlToBytes(encodedPayload)));
368
401
  } catch {
369
402
  throw new Error("INVALID_TENANT_AUTH");
370
403
  }
@@ -438,8 +471,17 @@ function createTenantContextManager(options) {
438
471
  async function getOrCreateAdapter(dbConfig) {
439
472
  const cacheKey = `${dbConfig.driver}:${dbConfig.connectionString}`;
440
473
  if (!adapterCache.has(cacheKey)) {
441
- const adapter = await adapterFactory(dbConfig);
442
- adapterCache.set(cacheKey, adapter);
474
+ const created = (async () => {
475
+ const adapter = await adapterFactory(dbConfig);
476
+ if (typeof adapter.initSchema === "function") {
477
+ await adapter.initSchema();
478
+ }
479
+ return adapter;
480
+ })().catch((error) => {
481
+ adapterCache.delete(cacheKey);
482
+ throw error;
483
+ });
484
+ adapterCache.set(cacheKey, created);
443
485
  }
444
486
  return adapterCache.get(cacheKey);
445
487
  }
@@ -567,6 +609,10 @@ async function createReiServer(config) {
567
609
  privateKey: vapid.privateKey || ""
568
610
  },
569
611
  webpush: webpushModule,
612
+ // 分片传输的限额(与 installReiSW 的 multipart 同一份)。instant 消息在
613
+ // schedule-message 里就地投递、定时消息走 send-notifications 的 tick,两条路
614
+ // 都从这个 ctx 展开,所以配一次两边都认。
615
+ multipart: config.multipart || null,
570
616
  tenant: {
571
617
  initSecret
572
618
  },
@@ -580,11 +626,37 @@ async function createReiServer(config) {
580
626
  sendNotifications: createSendNotificationsHandler(ctx),
581
627
  updateMessage: createUpdateMessageHandler(ctx),
582
628
  cancelMessage: createCancelMessageHandler(ctx),
583
- messages: createMessagesHandler(ctx)
629
+ messages: createMessagesHandler(ctx),
630
+ getMessage: createGetMessageHandler(ctx),
631
+ pushSubscription: createPushSubscriptionHandler(ctx),
632
+ llmCredentials: createLlmCredentialsHandler(ctx)
584
633
  }
585
634
  };
586
635
  }
587
636
  export {
637
+ CORS_ALLOW_HEADERS,
638
+ CORS_ALLOW_METHODS,
639
+ CRED_ID_MAX_LENGTH,
640
+ CRED_PUT_BATCH_MAX,
641
+ CRED_REFS_MAX_ENTRIES,
642
+ CRED_ROWS_PER_USER_MAX,
643
+ DEFAULT_CLAIM_LEASE_MS,
644
+ DEFAULT_HEARTBEAT_LEASE_TTL_MS,
645
+ DEFAULT_LEASE_HEARTBEAT_MS,
646
+ DEFAULT_MAX_REQUEST_BODY_BYTES,
647
+ DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE,
648
+ DEFAULT_MAX_TOOL_ITERATIONS,
649
+ DEFAULT_TOTAL_TIMEOUT_MS,
650
+ DeploymentConfigError,
651
+ MAX_PUSH_PAYLOAD_BYTES,
652
+ MAX_TASK_PAYLOAD_BYTES,
653
+ MIN_SCHEDULE_LEAD_MS,
654
+ NonRetryableError,
655
+ PUSH_ENVELOPE_RESERVED_BYTES,
656
+ SCHEMA_VERSION,
657
+ WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
658
+ WEB_PUSH_MAX_BODY_BYTES,
659
+ advanceOccurrence,
588
660
  createAdapter,
589
661
  createD1Adapter,
590
662
  createReiServer,
@@ -596,12 +668,29 @@ export {
596
668
  decryptPayload,
597
669
  deriveUserEncryptionKey,
598
670
  encryptForStorage,
671
+ ensureSchema,
672
+ getSchemaVersion,
673
+ hasChatCredRef,
674
+ isNonRetryableError,
675
+ isValidCredId,
599
676
  isValidISO8601,
677
+ isValidTimeZoneId,
600
678
  isValidUUID,
601
679
  isValidUUIDv4,
602
680
  isValidUrl,
681
+ measurePushPayload,
682
+ nextFutureOccurrence,
683
+ planNextOccurrence,
684
+ readRequestBody,
685
+ resolveLlmCredential,
603
686
  runScheduledTick,
687
+ runTask,
688
+ sanitizeErrorSummary,
689
+ summarizeErrorCause,
690
+ supportsLlmCredentialsStore,
604
691
  validateAvatarUrl,
692
+ validateCredRefs,
693
+ validateCredValue,
605
694
  validateLlmMessagesArray,
606
695
  validateScheduleMessagePayload,
607
696
  validateSplitPattern,
@@ -0,0 +1,108 @@
1
+ /**
2
+ * 默认 messageId / sessionId 里掺的名义触发时刻后缀(`@<epoch-ms>`)。
3
+ *
4
+ * 循环任务跨天复用同一条任务行,id 只用 task.id 的话,离线设备一次性收到
5
+ * 多天积压的推送(push TTL 有四周)时会在 service worker 端互相去重、在
6
+ * 收件箱按 messageId 覆盖,几天的消息只剩一条。掺入名义触发时刻后每个
7
+ * occurrence 一套 id;同一 occurrence 的重试仍是同一套——重投已送达的段
8
+ * 会被去重,这正是想要的。行上没有可解析的 next_send_at(比如直接喂进来
9
+ * 的内存任务对象)时返回 '',等价于旧格式。
10
+ *
11
+ * @param {{ next_send_at?: string }} task
12
+ * @returns {string}
13
+ */
14
+ export function occurrenceSuffix(task: {
15
+ next_send_at?: string;
16
+ }): string;
17
+ /**
18
+ * 本次触发的名义时刻(epoch 毫秒)。行上没有可解析的 next_send_at(比如直接
19
+ * 喂进来的内存任务对象)时返回 null。
20
+ *
21
+ * @param {{ next_send_at?: string }} task
22
+ * @returns {number|null}
23
+ */
24
+ export function occurrenceMsOf(task: {
25
+ next_send_at?: string;
26
+ }): number | null;
27
+ /**
28
+ * Does this task need the LLM at fire time? Fixed text never does, so it
29
+ * always stays on the legacy path regardless of hooks.
30
+ *
31
+ * @param {Object} decryptedPayload
32
+ * @returns {boolean}
33
+ */
34
+ export function taskNeedsLlm(decryptedPayload: any): boolean;
35
+ /**
36
+ * Frozen, credential-free view of the task for hook authors.
37
+ *
38
+ * nextSendAt 是这条任务原本的触发时刻。run-tick 领取任务时写的是 lease_until,
39
+ * next_send_at 那一列不动,所以这里给出去的和库里的是同一个值——宿主拿它当
40
+ * 时间锚点(窗口判断、缓存键)时对得上。
41
+ *
42
+ * 导出给 run-tick 用:`serializeBy` 要从任务内容里取分组 key,拿到的就该是
43
+ * 这一份(凭据剔掉、和 onBeforeFire 的 ctx.task 同一个形状),宿主不用为了
44
+ * 分组再学第二套字段。
45
+ */
46
+ export function buildHookTask(task: any, decryptedPayload: any): Readonly<{
47
+ id: any;
48
+ uuid: any;
49
+ nextSendAt: any;
50
+ retryCount: any;
51
+ }>;
52
+ /**
53
+ * Try the hook-driven fire path for one task.
54
+ *
55
+ * @param {Object} args
56
+ * @param {import('../adapters/interface.js').TaskRow} args.task
57
+ * @param {Object} args.decryptedPayload - decrypted task payload (has credentials; they stop here)
58
+ * @param {string} args.userKey - per-user storage key (for readState decryption)
59
+ * @param {Object} args.ctx - processor ctx ({ db, webpush, vapid, hooks, maxToolIterations, totalTimeoutMs, maxScheduledTasksPerFire })
60
+ * @returns {Promise<{ handled: false } | { handled: true, result: { success: true, messagesSent: number, status: 'finished'|'skipped', iterations: number } }>}
61
+ * `handled: false` → caller falls back to the legacy frozen-prompt path
62
+ * (onBeforeFire returned null).
63
+ * `onBeforeFire` may also return `{ skip: true }` to complete the fire
64
+ * before the first LLM call → `status: 'skipped', iterations: 0`, same
65
+ * success handling as the post-LLM skip-push path.
66
+ * Failures (timeout / loop exceeded / config errors) throw — the caller's
67
+ * existing error handling turns them into task retry/failure.
68
+ *
69
+ * 收尾回执:onBeforeFire 一旦被调用,上面每一种结局(含抛错)都会调一次
70
+ * `ctx.onFireSettled`,见 notifyFireSettled。
71
+ */
72
+ export function runAgenticFire({ task, decryptedPayload, userKey, ctx }: {
73
+ task: import("../adapters/interface.js").TaskRow;
74
+ decryptedPayload: any;
75
+ userKey: string;
76
+ ctx: any;
77
+ }): Promise<{
78
+ handled: false;
79
+ } | {
80
+ handled: true;
81
+ result: {
82
+ success: true;
83
+ messagesSent: number;
84
+ status: "finished" | "skipped";
85
+ iterations: number;
86
+ };
87
+ }>;
88
+ export const DEFAULT_MAX_TOOL_ITERATIONS: 5;
89
+ export const DEFAULT_TOTAL_TIMEOUT_MS: 240000;
90
+ export const DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE: 2;
91
+ export const MIN_SCHEDULE_LEAD_MS: 60000;
92
+ /**
93
+ * 把任务的调度身份投影进一条 push。
94
+ *
95
+ * 客户端收到推送时要知道「这是哪条任务、哪一次触发、它还会不会再来」——不然
96
+ * 角色在 fire 里自排的任务客户端从没见过,只能靠猜。这三个字段由库统一补,
97
+ * 宿主不用再往 metadata 里抄一遍(抄漏一个就够坏事了)。
98
+ *
99
+ * 放在 push 顶层,和 messageId / sessionId / timestamp / messageIndex /
100
+ * totalMessages 一样,不塞进 metadata:metadata 是宿主自己的地盘,库往里写
101
+ * 会和宿主的键打架。
102
+ *
103
+ * @param {Object} push - 待发送的 push(原地补字段)
104
+ * @param {Object} task - 数据库任务行
105
+ * @param {Object} decryptedPayload
106
+ * @param {number|null} occurrenceMs
107
+ */
108
+ export function stampTaskIdentity(push: any, task: any, decryptedPayload: any, occurrenceMs: number | null): void;
@@ -0,0 +1,80 @@
1
+ /** value 的 UTF-8 字节数——各处限额判断用的都是这个口径。 */
2
+ export function stateValueBytes(value: any): number;
3
+ /**
4
+ * 把 `clientStateTtl` 配置(`{ 命名空间: 天数 }`)算成清理指令。
5
+ *
6
+ * 一个命名空间出两条:它自己,加上它的切片命名空间(大值分块存在那儿,见
7
+ * lib/state-chunks.js)。两边共用同一个截止时刻——同一次写入的根行和切片行
8
+ * `updated_at` 相同,所以要么一起留、要么一起走,不会留下半截数据。
9
+ *
10
+ * 天数不是正数的条目跳过并告警(每个键只说一次):配错了就该看得见,但不能
11
+ * 让 cron 每分钟刷一条。
12
+ *
13
+ * @param {Record<string, number>|null|undefined} ttl - `{ 命名空间: 天数 }`;天数可带小数
14
+ * @param {number} now - 当前时刻(epoch 毫秒)
15
+ * @returns {Array<{ namespace: string, updatedBefore: number }>}
16
+ * `updatedBefore` 是 epoch 毫秒:这个命名空间下 `updated_at` 早于它的条目该清掉。
17
+ */
18
+ export function planClientStateCleanup(ttl: Record<string, number> | null | undefined, now: number): Array<{
19
+ namespace: string;
20
+ updatedBefore: number;
21
+ }>;
22
+ /**
23
+ * 把逻辑条目写进 client_state。条目要么是覆盖写(`value` 是字符串),要么是
24
+ * 删除(`value` 为 `null`):
25
+ *
26
+ * - 覆盖写:≤ 200KB 单行存;超过的切片跨行存(见 lib/state-chunks.js)。
27
+ * 同一批里先清掉这个 key 上次写入留下的切片行,所以覆盖成更小的值、或者
28
+ * 块数变少,都不会留下尾巴。
29
+ * - 删除:根行按精确 key 删,切片行按 key 前缀删,一个 key 的数据清干净。
30
+ *
31
+ * 两者都受同一套 last-write-wins 约束:`updatedAt` 比库里已有值旧的写入
32
+ * (或删除)不生效,陈旧批次盖不掉新数据。
33
+ *
34
+ * 条件写护栏:条目可带可选的 `version`(毫秒时间戳或单调递增整数)。带了它,
35
+ * 比较用的就是这个值而不是 `updatedAt`——同一个 key 有多个写入方时(例如
36
+ * fire_pack 由常规 flush 和 instant-chat 两条路径写),谁的**内容**新谁赢,
37
+ * 而不是谁的请求后到谁赢:慢网下晚到的旧包带着旧 `version`,盖不掉先到的新
38
+ * 包。护栏值落在行的 updated_at 列上(client_state 的比较列本来就是它),
39
+ * 没带 `version` 的写入照旧按 `updatedAt` 比。
40
+ *
41
+ * 条目本身的合法性(namespace / key 字符、value 大小)由调用方先校验好:
42
+ * HTTP handler 逐条拒绝并把原因回给客户端,`writeState()` 直接抛错给 hook。
43
+ *
44
+ * @param {Object} args
45
+ * @param {{ upsertClientState: Function }} args.db
46
+ * @param {string} args.userId
47
+ * @param {CryptoKey|string} args.userKey - 该用户的存储密钥(value 用它加密)
48
+ * @param {Array<{ namespace: string, key: string, value: string|null, updatedAt: number, version?: number }>} args.entries
49
+ * `version`(可选):条件写护栏值,见文件头。带了它,这条的 last-write-wins
50
+ * 比较用它(写进行的 updated_at 列);没带照旧用 `updatedAt`。
51
+ * @returns {Promise<{ upserted: number, skipped: number, deleted: number, skippedEntries: Array<{ namespace: string, key: string }> }>}
52
+ * `upserted` / `skipped` 按逻辑条目计(切片行不计);`deleted` 是请求删除的
53
+ * key 数,不代表这些 key 原本一定存在。`skippedEntries` 逐条列出被
54
+ * last-write-wins 拦下的 key(适配器不回 outcomes 时为空数组——分不清是哪条)。
55
+ */
56
+ export function writeClientStateEntries({ db, userId, userKey, entries }: {
57
+ db: {
58
+ upsertClientState: Function;
59
+ };
60
+ userId: string;
61
+ userKey: CryptoKey | string;
62
+ entries: Array<{
63
+ namespace: string;
64
+ key: string;
65
+ value: string | null;
66
+ updatedAt: number;
67
+ version?: number;
68
+ }>;
69
+ }): Promise<{
70
+ upserted: number;
71
+ skipped: number;
72
+ deleted: number;
73
+ skippedEntries: Array<{
74
+ namespace: string;
75
+ key: string;
76
+ }>;
77
+ }>;
78
+ export const MAX_STATE_ENTRIES_PER_BATCH: 200;
79
+ export const MAX_NAMESPACE_CHARS: 128;
80
+ export const MAX_KEY_CHARS: 256;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Portable constant-time string comparison.
3
+ *
4
+ * Runs identically on Node (tests) and Cloudflare Workers (prod). We avoid
5
+ * both node:crypto's timingSafeEqual (undefined on Workers historically) and
6
+ * crypto.subtle.timingSafeEqual (absent on Node). Instead we HMAC-SHA256 both
7
+ * inputs under one fresh random key, then compare the two digests. Because the
8
+ * key is random per call, an attacker can't precompute or replay a stable
9
+ * timing oracle; digests are a fixed 32 bytes, so the XOR-accumulate compare
10
+ * runs the same number of steps regardless of input length, with no early-out.
11
+ *
12
+ * globalThis.crypto (Web Crypto) is available on Node >= 20 and on Workers.
13
+ */
14
+ export function constantTimeEqual(a: any, b: any): Promise<boolean>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Database error helpers.
3
+ */
4
+ /**
5
+ * Check whether an error is caused by a unique-constraint violation.
6
+ *
7
+ * @param {unknown} error
8
+ * @returns {boolean}
9
+ */
10
+ export function isUniqueViolation(error: unknown): boolean;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Derive a user-specific encryption key from the master key.
3
+ *
4
+ * @param {string} userId - Unique user identifier.
5
+ * @param {string} masterKey - 64-char hex master key.
6
+ * @returns {Promise<string>} 64-char hex key.
7
+ */
8
+ export function deriveUserEncryptionKey(userId: string, masterKey: string): Promise<string>;
9
+ /**
10
+ * Decrypt a client-encrypted request body (AES-256-GCM, base64 encoded).
11
+ *
12
+ * @param {{ iv: string, authTag: string, encryptedData: string }} encryptedPayload
13
+ * @param {string} encryptionKey - 64-char hex key.
14
+ * @returns {Promise<Object>} Decrypted JSON object.
15
+ */
16
+ export function decryptPayload(encryptedPayload: {
17
+ iv: string;
18
+ authTag: string;
19
+ encryptedData: string;
20
+ }, encryptionKey: string): Promise<any>;
21
+ /**
22
+ * Encrypt a JSON payload for API transfer (AES-256-GCM, base64 encoded).
23
+ *
24
+ * @param {string|Object} payload
25
+ * @param {string} encryptionKey - 64-char hex key.
26
+ * @returns {Promise<{ iv: string, authTag: string, encryptedData: string }>}
27
+ */
28
+ export function encryptPayload(payload: string | any, encryptionKey: string): Promise<{
29
+ iv: string;
30
+ authTag: string;
31
+ encryptedData: string;
32
+ }>;
33
+ /**
34
+ * Encrypt data for database storage (hex encoded, colon-separated).
35
+ *
36
+ * @param {string} text - Plaintext string.
37
+ * @param {string} encryptionKey - 64-char hex key.
38
+ * @returns {Promise<string>} Format: iv:authTag:encryptedData
39
+ */
40
+ export function encryptForStorage(text: string, encryptionKey: string): Promise<string>;
41
+ /**
42
+ * Decrypt data from database storage format.
43
+ *
44
+ * @param {string} encryptedText - Format: iv:authTag:encryptedData
45
+ * @param {string} encryptionKey - 64-char hex key.
46
+ * @returns {Promise<string>} Plaintext string.
47
+ */
48
+ export function decryptFromStorage(encryptedText: string, encryptionKey: string): Promise<string>;