@rei-standard/amsg-server 2.6.0-next.13 → 2.6.0-next.16

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 (38) hide show
  1. package/README.md +109 -1
  2. package/dist/adapters/d1.d.ts +130 -0
  3. package/dist/adapters/interface.d.ts +67 -0
  4. package/dist/adapters/neon.d.ts +20 -58
  5. package/dist/adapters/pg-shared.d.ts +114 -0
  6. package/dist/adapters/pg.d.ts +20 -58
  7. package/dist/adapters/schema.d.ts +1 -1
  8. package/dist/adapters/schema.sqlite.d.ts +15 -1
  9. package/dist/{chunk-B2FDV7YK.mjs → chunk-34I2YSWE.mjs} +10 -0
  10. package/dist/{chunk-5IOYCCFD.cjs → chunk-H4V7OXHE.cjs} +1326 -384
  11. package/dist/{chunk-TV7N3RTH.mjs → chunk-KVHR3RCU.mjs} +1324 -382
  12. package/dist/{chunk-V2SDWGUB.cjs → chunk-OUPM4HAV.cjs} +10 -0
  13. package/dist/chunk-TFFBMYA2.cjs +95 -0
  14. package/dist/chunk-UYROU4L3.mjs +95 -0
  15. package/dist/cloudflare/single-user-worker.d.ts +35 -2
  16. package/dist/cloudflare.cjs +17 -2
  17. package/dist/cloudflare.d.ts +3 -1
  18. package/dist/cloudflare.mjs +18 -3
  19. package/dist/handlers/outbox.d.ts +7 -0
  20. package/dist/index.cjs +72 -36
  21. package/dist/index.d.cts +5 -2
  22. package/dist/index.d.ts +5 -2
  23. package/dist/index.mjs +53 -17
  24. package/dist/lib/client-state-store.d.ts +18 -3
  25. package/dist/lib/errors.d.ts +93 -0
  26. package/dist/lib/message-processor.d.ts +8 -2
  27. package/dist/lib/outbox-store.d.ts +33 -0
  28. package/dist/lib/push-subscription-store.d.ts +10 -2
  29. package/dist/lib/request.d.ts +36 -0
  30. package/dist/lib/run-tick.d.ts +38 -0
  31. package/dist/lib/schema-version.d.ts +50 -0
  32. package/dist/lib/validation.d.ts +1 -7
  33. package/dist/{neon-3ASBLLCM.cjs → neon-G3G4TDAY.cjs} +52 -112
  34. package/dist/{neon-CUIGAB2T.mjs → neon-GBUF4G76.mjs} +44 -104
  35. package/dist/{pg-IEZTAR43.cjs → pg-C3JZKBUM.cjs} +49 -112
  36. package/dist/{pg-KMJPL6QV.mjs → pg-MRXP5MHJ.mjs} +41 -104
  37. package/dist/single-user.d.ts +4 -0
  38. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -1,6 +1,17 @@
1
1
  import {
2
+ CORS_ALLOW_HEADERS,
3
+ CORS_ALLOW_METHODS,
4
+ DEFAULT_CLAIM_LEASE_MS,
5
+ DEFAULT_HEARTBEAT_LEASE_TTL_MS,
6
+ DEFAULT_LEASE_HEARTBEAT_MS,
7
+ DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE,
8
+ DEFAULT_MAX_TOOL_ITERATIONS,
9
+ DEFAULT_TOTAL_TIMEOUT_MS,
2
10
  MAX_PUSH_PAYLOAD_BYTES,
11
+ MIN_SCHEDULE_LEAD_MS,
12
+ NonRetryableError,
3
13
  PUSH_ENVELOPE_RESERVED_BYTES,
14
+ SCHEMA_VERSION,
4
15
  WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
5
16
  WEB_PUSH_MAX_BODY_BYTES,
6
17
  advanceOccurrence,
@@ -21,7 +32,10 @@ import {
21
32
  decryptPayload,
22
33
  deriveUserEncryptionKey,
23
34
  encryptForStorage,
35
+ ensureSchema,
24
36
  getHeader,
37
+ getSchemaVersion,
38
+ isNonRetryableError,
25
39
  isValidISO8601,
26
40
  isValidTimeZoneId,
27
41
  isValidUUID,
@@ -32,6 +46,9 @@ import {
32
46
  parseJsonBody,
33
47
  planNextOccurrence,
34
48
  runScheduledTick,
49
+ runTask,
50
+ sanitizeErrorSummary,
51
+ summarizeErrorCause,
35
52
  timingSafeEqualBytes,
36
53
  utf8,
37
54
  utf8Decode,
@@ -39,7 +56,8 @@ import {
39
56
  validateLlmMessagesArray,
40
57
  validateScheduleMessagePayload,
41
58
  validateSplitPattern
42
- } from "./chunk-TV7N3RTH.mjs";
59
+ } from "./chunk-KVHR3RCU.mjs";
60
+ import "./chunk-34I2YSWE.mjs";
43
61
 
44
62
  // src/server/adapters/factory.js
45
63
  async function createAdapter(config) {
@@ -55,11 +73,11 @@ async function createAdapter(config) {
55
73
  }
56
74
  switch (config.driver) {
57
75
  case "neon": {
58
- const { NeonAdapter } = await import("./neon-CUIGAB2T.mjs");
76
+ const { NeonAdapter } = await import("./neon-GBUF4G76.mjs");
59
77
  return new NeonAdapter(config.connectionString);
60
78
  }
61
79
  case "pg": {
62
- const { PgAdapter } = await import("./pg-KMJPL6QV.mjs");
80
+ const { PgAdapter } = await import("./pg-MRXP5MHJ.mjs");
63
81
  return new PgAdapter(config.connectionString);
64
82
  }
65
83
  default:
@@ -228,14 +246,6 @@ function createSendNotificationsHandler(ctx) {
228
246
  // src/server/tenant/blob-store.js
229
247
  import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
230
248
  var inMemoryNamespaces = /* @__PURE__ */ new Map();
231
- function base64UrlEncode(input) {
232
- return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
233
- }
234
- function base64UrlDecode(input) {
235
- const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
236
- const padLength = (4 - normalized.length % 4) % 4;
237
- return Buffer.from(normalized + "=".repeat(padLength), "base64");
238
- }
239
249
  function getKekBuffer(kek) {
240
250
  const value = String(kek || "").trim();
241
251
  if (!value) {
@@ -249,16 +259,16 @@ function encryptConfig(config, kekBuffer) {
249
259
  const plaintext = Buffer.from(JSON.stringify(config), "utf8");
250
260
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
251
261
  const authTag = cipher.getAuthTag();
252
- return `v1.${base64UrlEncode(iv)}.${base64UrlEncode(authTag)}.${base64UrlEncode(ciphertext)}`;
262
+ return `v1.${bytesToBase64Url(iv)}.${bytesToBase64Url(authTag)}.${bytesToBase64Url(ciphertext)}`;
253
263
  }
254
264
  function decryptConfig(encrypted, kekBuffer) {
255
265
  const parts = String(encrypted || "").split(".");
256
266
  if (parts.length !== 4 || parts[0] !== "v1") {
257
267
  throw new Error("INVALID_TENANT_CONFIG");
258
268
  }
259
- const iv = base64UrlDecode(parts[1]);
260
- const authTag = base64UrlDecode(parts[2]);
261
- const ciphertext = base64UrlDecode(parts[3]);
269
+ const iv = base64UrlToBytes(parts[1]);
270
+ const authTag = base64UrlToBytes(parts[2]);
271
+ const ciphertext = base64UrlToBytes(parts[3]);
262
272
  const decipher = createDecipheriv("aes-256-gcm", kekBuffer, iv);
263
273
  decipher.setAuthTag(authTag);
264
274
  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
@@ -446,8 +456,17 @@ function createTenantContextManager(options) {
446
456
  async function getOrCreateAdapter(dbConfig) {
447
457
  const cacheKey = `${dbConfig.driver}:${dbConfig.connectionString}`;
448
458
  if (!adapterCache.has(cacheKey)) {
449
- const adapter = await adapterFactory(dbConfig);
450
- adapterCache.set(cacheKey, adapter);
459
+ const created = (async () => {
460
+ const adapter = await adapterFactory(dbConfig);
461
+ if (typeof adapter.initSchema === "function") {
462
+ await adapter.initSchema();
463
+ }
464
+ return adapter;
465
+ })().catch((error) => {
466
+ adapterCache.delete(cacheKey);
467
+ throw error;
468
+ });
469
+ adapterCache.set(cacheKey, created);
451
470
  }
452
471
  return adapterCache.get(cacheKey);
453
472
  }
@@ -595,8 +614,19 @@ async function createReiServer(config) {
595
614
  };
596
615
  }
597
616
  export {
617
+ CORS_ALLOW_HEADERS,
618
+ CORS_ALLOW_METHODS,
619
+ DEFAULT_CLAIM_LEASE_MS,
620
+ DEFAULT_HEARTBEAT_LEASE_TTL_MS,
621
+ DEFAULT_LEASE_HEARTBEAT_MS,
622
+ DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE,
623
+ DEFAULT_MAX_TOOL_ITERATIONS,
624
+ DEFAULT_TOTAL_TIMEOUT_MS,
598
625
  MAX_PUSH_PAYLOAD_BYTES,
626
+ MIN_SCHEDULE_LEAD_MS,
627
+ NonRetryableError,
599
628
  PUSH_ENVELOPE_RESERVED_BYTES,
629
+ SCHEMA_VERSION,
600
630
  WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
601
631
  WEB_PUSH_MAX_BODY_BYTES,
602
632
  advanceOccurrence,
@@ -611,6 +641,9 @@ export {
611
641
  decryptPayload,
612
642
  deriveUserEncryptionKey,
613
643
  encryptForStorage,
644
+ ensureSchema,
645
+ getSchemaVersion,
646
+ isNonRetryableError,
614
647
  isValidISO8601,
615
648
  isValidTimeZoneId,
616
649
  isValidUUID,
@@ -620,6 +653,9 @@ export {
620
653
  nextFutureOccurrence,
621
654
  planNextOccurrence,
622
655
  runScheduledTick,
656
+ runTask,
657
+ sanitizeErrorSummary,
658
+ summarizeErrorCause,
623
659
  validateAvatarUrl,
624
660
  validateLlmMessagesArray,
625
661
  validateScheduleMessagePayload,
@@ -12,6 +12,13 @@ export function stateValueBytes(value: any): number;
12
12
  * 两者都受同一套 last-write-wins 约束:`updatedAt` 比库里已有值旧的写入
13
13
  * (或删除)不生效,陈旧批次盖不掉新数据。
14
14
  *
15
+ * 条件写护栏:条目可带可选的 `version`(毫秒时间戳或单调递增整数)。带了它,
16
+ * 比较用的就是这个值而不是 `updatedAt`——同一个 key 有多个写入方时(例如
17
+ * fire_pack 由常规 flush 和 instant-chat 两条路径写),谁的**内容**新谁赢,
18
+ * 而不是谁的请求后到谁赢:慢网下晚到的旧包带着旧 `version`,盖不掉先到的新
19
+ * 包。护栏值落在行的 updated_at 列上(client_state 的比较列本来就是它),
20
+ * 没带 `version` 的写入照旧按 `updatedAt` 比。
21
+ *
15
22
  * 条目本身的合法性(namespace / key 字符、value 大小)由调用方先校验好:
16
23
  * HTTP handler 逐条拒绝并把原因回给客户端,`writeState()` 直接抛错给 hook。
17
24
  *
@@ -19,10 +26,13 @@ export function stateValueBytes(value: any): number;
19
26
  * @param {{ upsertClientState: Function }} args.db
20
27
  * @param {string} args.userId
21
28
  * @param {CryptoKey|string} args.userKey - 该用户的存储密钥(value 用它加密)
22
- * @param {Array<{ namespace: string, key: string, value: string|null, updatedAt: number }>} args.entries
23
- * @returns {Promise<{ upserted: number, skipped: number, deleted: number }>}
29
+ * @param {Array<{ namespace: string, key: string, value: string|null, updatedAt: number, version?: number }>} args.entries
30
+ * `version`(可选):条件写护栏值,见文件头。带了它,这条的 last-write-wins
31
+ * 比较用它(写进行的 updated_at 列);没带照旧用 `updatedAt`。
32
+ * @returns {Promise<{ upserted: number, skipped: number, deleted: number, skippedEntries: Array<{ namespace: string, key: string }> }>}
24
33
  * `upserted` / `skipped` 按逻辑条目计(切片行不计);`deleted` 是请求删除的
25
- * key 数,不代表这些 key 原本一定存在。
34
+ * key 数,不代表这些 key 原本一定存在。`skippedEntries` 逐条列出被
35
+ * last-write-wins 拦下的 key(适配器不回 outcomes 时为空数组——分不清是哪条)。
26
36
  */
27
37
  export function writeClientStateEntries({ db, userId, userKey, entries }: {
28
38
  db: {
@@ -35,11 +45,16 @@ export function writeClientStateEntries({ db, userId, userKey, entries }: {
35
45
  key: string;
36
46
  value: string | null;
37
47
  updatedAt: number;
48
+ version?: number;
38
49
  }>;
39
50
  }): Promise<{
40
51
  upserted: number;
41
52
  skipped: number;
42
53
  deleted: number;
54
+ skippedEntries: Array<{
55
+ namespace: string;
56
+ key: string;
57
+ }>;
43
58
  }>;
44
59
  export const MAX_STATE_ENTRIES_PER_BATCH: 200;
45
60
  export const MAX_NAMESPACE_CHARS: 128;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 这个错误是不是「重试也好不了」。认 NonRetryableError,也认任何自带
3
+ * `permanent: true` 的错误对象(跨包/跨版本安全)。
4
+ *
5
+ * @param {unknown} error
6
+ * @returns {boolean}
7
+ */
8
+ export function isNonRetryableError(error: unknown): boolean;
9
+ /**
10
+ * 把错误原因脱敏成能明文落库(任务行 last_error 列)的摘要。
11
+ *
12
+ * 任务内容一律密文落库,last_error 是唯一一列明文的「为什么没发出去」——
13
+ * 错误消息偶尔会回显请求细节(上游 API 的报错带 URL、header 片段),所以
14
+ * 长得像凭据的 token 一律遮掉,再截断到 500 字符。
15
+ *
16
+ * @param {unknown} reason
17
+ * @returns {string}
18
+ */
19
+ export function sanitizeErrorSummary(reason: unknown): string;
20
+ /**
21
+ * @typedef {Object} ErrorCause
22
+ * @property {'config'|'request'|'tick'} stage - 在哪一段炸的
23
+ * @property {string} name - 错误类型(`error.name`,认不出来时是 'Error')
24
+ * @property {string} message - 脱敏后的错误消息
25
+ * @property {string} [code] - 错误自带的 `code` 字符串(有才带)
26
+ */
27
+ /**
28
+ * 把一个异常整理成能随响应体一起回给调用方的机读原因。
29
+ *
30
+ * 500 只回一句「服务器内部错误」的话,真因(`D1_ERROR: no such table:
31
+ * message_outbox`、存储层写超时……)就只剩 console.error 里那一行,调用方拿不
32
+ * 到,用户看到的也只是「服务器内部错误」——不知道哪儿坏了,也不知道该点哪里。
33
+ * 这个函数把异常压成几个固定字段,让调用方能机读、能展示。
34
+ *
35
+ * 只带错误类型和消息文本。消息先过 `sanitizeErrorSummary`(遮掉长得像凭据的
36
+ * 串、截断到 500 字符);密钥、用户数据、任务正文都不在 `error.message` 上,
37
+ * 也不往这里放。
38
+ *
39
+ * @param {unknown} error - 捕获到的异常,也收 `{ name, message }` 这样的普通对象
40
+ * @param {'config'|'request'|'tick'} stage - 'config' = 构建配置时抛的(少了
41
+ * binding / 环境变量);'request' = 路由或处理器抛的;'tick' = cron 那一跳抛的
42
+ * @returns {ErrorCause}
43
+ */
44
+ export function summarizeErrorCause(error: unknown, stage: "config" | "request" | "tick"): ErrorCause;
45
+ /**
46
+ * 错误类型:宿主 hook 与库内部共用的失败语义标注。
47
+ */
48
+ /**
49
+ * 确定性失败——重试必然同败,别再排退避阶梯。
50
+ *
51
+ * 典型场景全在 fire-time hook 里:onBeforeFire 发现 fire_pack 缺失、解析失
52
+ * 败、缺必要的段……这类错误不会因为过两分钟再跑一次就好,按普通投递失败重
53
+ * 试三轮只是让用户多白等十几分钟,还把 hook 里的计费调用(情绪评估之类)
54
+ * 重复烧三遍。hook 抛这个类(或任何带 `permanent: true` 的错误),run-tick
55
+ * 收到后跳过退避:一次性任务直接标 failed(原因进 last_error / payload 的
56
+ * lastError),循环任务直接作废本次 occurrence。
57
+ *
58
+ * `code`(可选)会透传到 processSingleMessage 的 errorCode,宿主想按错误类
59
+ * 别分流时用它。
60
+ *
61
+ * 用 `permanent` 属性而不是 instanceof 做判定:宿主的 worker 里可能打包了另
62
+ * 一份本库(版本错开、bundler 双实例),跨 realm 的 instanceof 靠不住。
63
+ */
64
+ export class NonRetryableError extends Error {
65
+ /**
66
+ * @param {string} message
67
+ * @param {{ code?: string, cause?: unknown }} [options]
68
+ */
69
+ constructor(message: string, options?: {
70
+ code?: string;
71
+ cause?: unknown;
72
+ });
73
+ permanent: boolean;
74
+ code: string;
75
+ }
76
+ export type ErrorCause = {
77
+ /**
78
+ * - 在哪一段炸的
79
+ */
80
+ stage: "config" | "request" | "tick";
81
+ /**
82
+ * - 错误类型(`error.name`,认不出来时是 'Error')
83
+ */
84
+ name: string;
85
+ /**
86
+ * - 脱敏后的错误消息
87
+ */
88
+ message: string;
89
+ /**
90
+ * - 错误自带的 `code` 字符串(有才带)
91
+ */
92
+ code?: string;
93
+ };
@@ -10,12 +10,18 @@
10
10
  * @param {import('../adapters/interface.js').TaskRow} task
11
11
  * @param {ProcessorContext} ctx
12
12
  * @param {string} [providedMasterKey]
13
- * @returns {Promise<{ success: boolean, messagesSent: number, error?: string }>}
13
+ * @param {{ userKey: string, payload: Object } | null} [predecrypted] - 调用方
14
+ * (run-tick 的预扫描)已经解好的 payload;传了就不再解第二遍。
15
+ * @returns {Promise<{ success: boolean, messagesSent: number, error?: string, errorCode?: string|null }>}
14
16
  */
15
- export function processSingleMessage(task: import("../adapters/interface.js").TaskRow, ctx: ProcessorContext, providedMasterKey?: string): Promise<{
17
+ export function processSingleMessage(task: import("../adapters/interface.js").TaskRow, ctx: ProcessorContext, providedMasterKey?: string, predecrypted?: {
18
+ userKey: string;
19
+ payload: any;
20
+ } | null): Promise<{
16
21
  success: boolean;
17
22
  messagesSent: number;
18
23
  error?: string;
24
+ errorCode?: string | null;
19
25
  }>;
20
26
  /**
21
27
  * Process a single message identified by UUID (used for instant type).
@@ -0,0 +1,33 @@
1
+ /** 适配器支持 outbox 吗(写入侧要用的两个方法都在才算)。 */
2
+ export function supportsOutbox(db: any): boolean;
3
+ /**
4
+ * 发送前把这一批 push 落进 outbox。push 对象须已定稿(messageId / sessionId /
5
+ * messageIndex / totalMessages / 任务身份都已补齐)——落进去的密文就是客户端
6
+ * 补收时拿到的那一份。
7
+ *
8
+ * @param {Object} args
9
+ * @param {Object} args.db
10
+ * @param {string} args.userId
11
+ * @param {CryptoKey|string} args.userKey - per-user 存储密钥
12
+ * @param {Object[]} args.pushes - 定稿后的 push 对象
13
+ * @returns {Promise<boolean>} 是否真的落了行(适配器不支持 / 落行失败 → false)
14
+ */
15
+ export function appendPushesToOutbox({ db, userId, userKey, pushes }: {
16
+ db: any;
17
+ userId: string;
18
+ userKey: CryptoKey | string;
19
+ pushes: any[];
20
+ }): Promise<boolean>;
21
+ /**
22
+ * 把发出去的那部分标成 delivered(发送半途失败时只标已发出的段)。
23
+ *
24
+ * @param {Object} args
25
+ * @param {Object} args.db
26
+ * @param {string} args.userId
27
+ * @param {string[]} args.messageIds
28
+ */
29
+ export function markPushesDelivered({ db, userId, messageIds }: {
30
+ db: any;
31
+ userId: string;
32
+ messageIds: string[];
33
+ }): Promise<void>;
@@ -48,16 +48,24 @@ export function loadPushSubscription({ db, userId, userKey }: {
48
48
  /**
49
49
  * 投递前取订阅。取不到就抛——静默不发会让任务「成功」地什么都没做,用户
50
50
  * 只看到消息凭空消失。抛出去走既有的重试 / 标记逻辑,原因也会记进 payload
51
- * 的 lastError,`GET /messages` 上看得见。
51
+ * 的 lastError,`GET /messages` 上看得见。抛出的错误带稳定的 `code` 属性
52
+ * ('PUSH_SUBSCRIPTION_MISSING' / 'PUSH_SUBSCRIPTION_STORE_UNSUPPORTED'),
53
+ * 按类别分支请用它,别匹配 message 文案。
54
+ *
55
+ * `legacyFallback`:用户级存储里没有订阅时的兜底(升级前创建的任务把订阅
56
+ * 冻结在自己的 payload 里,这份订阅仍然有效)。存储里有订阅时永远用存储的
57
+ * 那份——它是用户最近一次登记的。
52
58
  *
53
59
  * @param {Object} args
54
60
  * @param {import('../adapters/interface.js').DbAdapter} args.db
55
61
  * @param {string} args.userId
56
62
  * @param {string} args.userKey
63
+ * @param {unknown} [args.legacyFallback] - 旧任务 payload 里内嵌的订阅(可选)
57
64
  * @returns {Promise<Object>} 明文订阅对象
58
65
  */
59
- export function resolvePushSubscription({ db, userId, userKey }: {
66
+ export function resolvePushSubscription({ db, userId, userKey, legacyFallback }: {
60
67
  db: import("../adapters/interface.js").DbAdapter;
61
68
  userId: string;
62
69
  userKey: string;
70
+ legacyFallback?: unknown;
63
71
  }): Promise<any>;
@@ -56,6 +56,42 @@ export function isEncryptedEnvelope(payload: unknown): payload is {
56
56
  * @returns {ParseBodyResult}
57
57
  */
58
58
  export function parseEncryptedBody(body: unknown): ParseBodyResult;
59
+ /**
60
+ * 标准错误信封:{ status, body: { success: false, error: { code, message, details? } } }。
61
+ *
62
+ * @param {number} status
63
+ * @param {string} code
64
+ * @param {string} message
65
+ * @param {Object} [details]
66
+ */
67
+ export function errorResponse(status: number, code: string, message: string, details?: any): {
68
+ status: number;
69
+ body: {
70
+ success: boolean;
71
+ error: {
72
+ code: string;
73
+ message: string;
74
+ details?: undefined;
75
+ } | {
76
+ code: string;
77
+ message: string;
78
+ details: any;
79
+ };
80
+ };
81
+ };
82
+ /**
83
+ * X-User-Id 门禁。所有按用户读写的端点共用这一份:规则(必填 + UUID v4)和
84
+ * 文案只此一处,改口径不用挨个 handler 找复制粘贴的副本。
85
+ *
86
+ * @param {Record<string, any>} headers
87
+ * @returns {{ userId: string, error?: undefined } | { error: ReturnType<typeof errorResponse> }}
88
+ */
89
+ export function requireUserId(headers: Record<string, any>): {
90
+ userId: string;
91
+ error?: undefined;
92
+ } | {
93
+ error: ReturnType<typeof errorResponse>;
94
+ };
59
95
  /**
60
96
  * Read a header value case-insensitively.
61
97
  *
@@ -27,8 +27,45 @@ export function runScheduledTick(ctx: any): Promise<{
27
27
  failedTasks: any[];
28
28
  };
29
29
  }>;
30
+ /**
31
+ * 只跑一条任务的官方入口,给「fetch 里只来得及 enqueue、真正的 fire 交给
32
+ * CF Queue 消费者(15 分钟预算)跑」这类宿主用——不用再依赖 cron 恰好捞到。
33
+ *
34
+ * 与 cron tick 走完全同一条投递链:占位(含心跳续租)、过期守卫、分组串行
35
+ * (单任务场景下退化为占位时的跨 tick 分组门)、失败重试/终态、hook 全套。
36
+ * 行没到点(next_send_at 在未来)或正处在退避窗口(retry_after 未到点)时不
37
+ * 跑——这个入口只是换了个触发器,不是绕过排期的后门。
38
+ *
39
+ * 四种不跑的情形分开回报,调用方不用猜:
40
+ * - `not_found`:没有这条 uuid。一次性任务发完即删,所以「发完了」的那条也
41
+ * 落在这里;行还在但已经是终态的走下面那条。
42
+ * - `already_settled`:行还在,但已经是 sent / failed(`status` 带上是哪
43
+ * 个)。适配器没实现 `getTaskStatusByUuidOnly` 时这种情况并进 `not_found`。
44
+ * - `not_due`:还没到 `next_send_at`(`nextSendAt` 带上是什么时候)。
45
+ * - `retry_pending`:上次投递失败,还在退避窗口里(`retryAfter` 带上什么时
46
+ * 候到点)。
47
+ *
48
+ * @param {Object} ctx - 与 runScheduledTick 同一份 ctx
49
+ * @param {string} uuid - 任务 uuid(pending 行)
50
+ * @returns {Promise<{ ran: false, reason: 'not_found'|'already_settled'|'not_due'|'retry_pending', status?: string, nextSendAt?: string, retryAfter?: string }
51
+ * | { ran: true, summary: Object }>} summary 与 runScheduledTick 的返回同构
52
+ * (totalTasks 恒为 1);任务被别的执行者占着时体现在 summary.details.claimSkippedTasks。
53
+ */
54
+ export function runTask(ctx: any, uuid: string): Promise<{
55
+ ran: false;
56
+ reason: "not_found" | "already_settled" | "not_due" | "retry_pending";
57
+ status?: string;
58
+ nextSendAt?: string;
59
+ retryAfter?: string;
60
+ } | {
61
+ ran: true;
62
+ summary: any;
63
+ }>;
30
64
  export const DEFAULT_CLAIM_LEASE_MS: number;
65
+ export const DEFAULT_LEASE_HEARTBEAT_MS: number;
66
+ export const DEFAULT_HEARTBEAT_LEASE_TTL_MS: number;
31
67
  export const STALE_AFTER_MS: number;
68
+ export { sanitizeErrorSummary };
32
69
  export type StaleSkipInfo = {
33
70
  reason: "stale";
34
71
  /**
@@ -66,3 +103,4 @@ export type StaleSkipInfo = {
66
103
  readState: (namespace: string) => Promise<any[]>;
67
104
  writeState: (namespace: string, entries: any[]) => Promise<any>;
68
105
  };
106
+ import { sanitizeErrorSummary } from './errors.js';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 活库的表结构够不够这一版代码用。只读,不改任何东西。
3
+ *
4
+ * @param {import('../adapters/interface.js').DbAdapter} db - 数据库适配器
5
+ * (要实现 `describeSchema()`;内置的 D1 适配器实现了)
6
+ * @returns {Promise<SchemaVersionResult>}
7
+ */
8
+ export function getSchemaVersion(db: import("../adapters/interface.js").DbAdapter): Promise<SchemaVersionResult>;
9
+ /**
10
+ * 缺什么补什么:自查一遍,不够用就跑一次 `initSchema()`(建表 + 补列 + 建索
11
+ * 引,重复跑没事),再自查一遍把结果回报出去。
12
+ *
13
+ * 本来就够用时不跑——`initSchema()` 是好几个来回,能省则省。
14
+ *
15
+ * @param {import('../adapters/interface.js').DbAdapter} db - 数据库适配器
16
+ * @returns {Promise<EnsureSchemaResult>} 补齐之后的自查结果;`migrated` 说明这
17
+ * 次有没有真的动手。补完仍然 `ok: false`(例如 ALTER 被库拒了)时 `missing`
18
+ * 里还留着没补上的那几项
19
+ */
20
+ export function ensureSchema(db: import("../adapters/interface.js").DbAdapter): Promise<EnsureSchemaResult>;
21
+ /**
22
+ * 表结构自己的版本号,只在表 / 列 / 关键索引变化时抬,与包版本各走各的。
23
+ * 数值取自引入当前这套表结构的那条发布线。
24
+ */
25
+ export const SCHEMA_VERSION: "2.6.0";
26
+ export type SchemaVersionResult = {
27
+ /**
28
+ * - 活库当前满足的版本:够用就是 `SCHEMA_VERSION`,
29
+ * 缺东西就是 `null`(只知道不够用,不知道它停在哪一版)
30
+ */
31
+ current: string | null;
32
+ /**
33
+ * - 这一版代码需要的表结构版本
34
+ */
35
+ required: string;
36
+ /**
37
+ * - 需要的表 / 列 / 关键索引是不是都在
38
+ */
39
+ ok: boolean;
40
+ /**
41
+ * - 缺什么,形如 `'table:message_outbox'` /
42
+ * `'column:scheduled_messages.last_error'` / `'index:uidx_uuid'`。整张表缺席
43
+ * 时只报这一张表,不再逐列展开。`ok` 为 true 时是空数组
44
+ */
45
+ missing: string[];
46
+ };
47
+ export type EnsureSchemaResult = SchemaVersionResult & {
48
+ migrated: boolean;
49
+ schema: any | null;
50
+ };
@@ -4,12 +4,6 @@
4
4
  * @returns {boolean}
5
5
  */
6
6
  export function isValidISO8601(dateString: string): boolean;
7
- /**
8
- * Validate URL format.
9
- * @param {string} urlString
10
- * @returns {boolean}
11
- */
12
- export function isValidUrl(urlString: string): boolean;
13
7
  /**
14
8
  * Validate UUID format.
15
9
  * @param {string} uuid
@@ -65,4 +59,4 @@ export function validateScheduleMessagePayload(payload: any): {
65
59
  details?: any;
66
60
  };
67
61
  import { isValidTimeZoneId } from './recurrence.js';
68
- export { isValidTimeZoneId, validateAvatarUrl };
62
+ export { isValidTimeZoneId, isValidUrl, validateAvatarUrl };