@rei-standard/amsg-server 2.6.0-next.9 → 3.0.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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +822 -41
  3. package/dist/adapters/d1.d.ts +693 -0
  4. package/dist/adapters/factory.d.ts +39 -0
  5. package/dist/adapters/interface.d.ts +384 -0
  6. package/dist/adapters/neon.d.ts +105 -0
  7. package/dist/adapters/pg-shared.d.ts +194 -0
  8. package/dist/adapters/pg.d.ts +101 -0
  9. package/dist/adapters/schema.d.ts +57 -0
  10. package/dist/adapters/schema.sqlite.d.ts +80 -0
  11. package/dist/chunk-7NMQFTDJ.cjs +148 -0
  12. package/dist/{chunk-GOBCWCJP.mjs → chunk-GN44PST5.mjs} +68 -1
  13. package/dist/chunk-RL7V7VSS.mjs +8784 -0
  14. package/dist/chunk-TKDISKCF.cjs +163 -0
  15. package/dist/chunk-XKUWMYVF.mjs +163 -0
  16. package/dist/chunk-YBDQJVFQ.cjs +8784 -0
  17. package/dist/cloudflare/single-user-worker.d.ts +39 -0
  18. package/dist/cloudflare.cjs +35 -2
  19. package/dist/cloudflare.d.ts +11 -2
  20. package/dist/cloudflare.mjs +36 -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-namespaces.d.ts +5 -0
  24. package/dist/handlers/client-state.d.ts +7 -0
  25. package/dist/handlers/cloud-data.d.ts +4 -0
  26. package/dist/handlers/get-message.d.ts +3 -0
  27. package/dist/handlers/get-user-key.d.ts +3 -0
  28. package/dist/handlers/init-tenant.d.ts +23 -0
  29. package/dist/handlers/llm-credentials.d.ts +5 -0
  30. package/dist/handlers/messages.d.ts +3 -0
  31. package/dist/handlers/outbox.d.ts +9 -0
  32. package/dist/handlers/push-subscription.d.ts +5 -0
  33. package/dist/handlers/schedule-message.d.ts +3 -0
  34. package/dist/handlers/send-notifications.d.ts +3 -0
  35. package/dist/handlers/single-user-init.d.ts +13 -0
  36. package/dist/handlers/update-message.d.ts +3 -0
  37. package/dist/handlers/vapid-public-key.d.ts +19 -0
  38. package/dist/index.cjs +133 -44
  39. package/dist/index.d.cts +145 -766
  40. package/dist/index.d.ts +145 -766
  41. package/dist/index.mjs +125 -36
  42. package/dist/lib/agentic-fire.d.ts +177 -0
  43. package/dist/lib/client-state-store.d.ts +88 -0
  44. package/dist/lib/cloud-data-cleanup.d.ts +36 -0
  45. package/dist/lib/cloud-data-guard.d.ts +14 -0
  46. package/dist/lib/cloud-data-inventory.d.ts +15 -0
  47. package/dist/lib/constant-time.d.ts +14 -0
  48. package/dist/lib/db-errors.d.ts +10 -0
  49. package/dist/lib/encryption.d.ts +48 -0
  50. package/dist/lib/errors.d.ts +273 -0
  51. package/dist/lib/llm-credentials-store.d.ts +147 -0
  52. package/dist/lib/llm.d.ts +12 -0
  53. package/dist/lib/message-processor.d.ts +82 -0
  54. package/dist/lib/outbox-store.d.ts +162 -0
  55. package/dist/lib/push-policy.d.ts +11 -0
  56. package/dist/lib/push-subscription-store.d.ts +94 -0
  57. package/dist/lib/recurrence.d.ts +51 -0
  58. package/dist/lib/request.d.ts +182 -0
  59. package/dist/lib/result-emitter.d.ts +60 -0
  60. package/dist/lib/retry-policy.d.ts +11 -0
  61. package/dist/lib/run-tick.d.ts +121 -0
  62. package/dist/lib/schema-version.d.ts +50 -0
  63. package/dist/lib/state-accessors.d.ts +70 -0
  64. package/dist/lib/state-chunks.d.ts +65 -0
  65. package/dist/lib/task-projection.d.ts +92 -0
  66. package/dist/lib/validation.d.ts +97 -0
  67. package/dist/lib/version.d.ts +7 -0
  68. package/dist/lib/webcrypto-utils.d.ts +1 -0
  69. package/dist/lib/webpush-webcrypto.d.ts +78 -0
  70. package/dist/{neon-QJ7KYHEX.mjs → neon-GKLT4UY3.mjs} +116 -44
  71. package/dist/{neon-V4VOH55N.cjs → neon-XHPJZESV.cjs} +121 -49
  72. package/dist/{pg-6X74HKMX.cjs → pg-FHXQ6BJP.cjs} +109 -48
  73. package/dist/{pg-DFJSFOJQ.mjs → pg-J4YXRQP5.mjs} +104 -43
  74. package/dist/single-user.d.ts +106 -0
  75. package/dist/tenant/blob-store.d.ts +30 -0
  76. package/dist/tenant/context.d.ts +47 -0
  77. package/dist/tenant/single-user-context.d.ts +29 -0
  78. package/dist/tenant/token.d.ts +26 -0
  79. package/package.json +3 -3
  80. package/dist/chunk-3QRVP4JE.cjs +0 -2834
  81. package/dist/chunk-DCKCAGFV.cjs +0 -81
  82. package/dist/chunk-S6O3MXQO.mjs +0 -2834
  83. package/dist/cloudflare-kUY6z-k5.d.cts +0 -4460
  84. package/dist/cloudflare-kUY6z-k5.d.ts +0 -4460
  85. package/dist/cloudflare.d.cts +0 -2
  86. package/dist/neon-D8Z2Nn97.d.cts +0 -295
  87. package/dist/neon-PqNFJIz2.d.ts +0 -295
  88. package/dist/pg-C48FwCWW.d.cts +0 -289
  89. package/dist/pg-C5gtWUfU.d.ts +0 -289
  90. package/dist/schema-BHuyH_6Z.d.cts +0 -88
  91. package/dist/schema-BHuyH_6Z.d.ts +0 -88
@@ -0,0 +1,121 @@
1
+ /**
2
+ * 把宿主给的分组 key 变成落库用的分组标识。
3
+ *
4
+ * 不直接存 key 本身:它多半是角色 id / 联系人名这类宿主数据,而任务内容一律
5
+ * 加密落库,多开一列明文出口不合适。这里用该用户的存储密钥做 HMAC——同一个
6
+ * 用户的同一个 key 每次都得到同一个值(分组判定要的就是相等性),换个用户
7
+ * 得到的值必然不同(分组天然按用户隔开,SQL 那边不用再比 user_id),拿到数
8
+ * 据库也反推不回原 key。
9
+ *
10
+ * @param {string} userKey - 该用户的存储密钥(deriveUserEncryptionKey 的结果)
11
+ * @param {string} rawKey - serializeBy 返回的分组 key
12
+ * @returns {Promise<string>} 43 字符的 base64url 串(列宽 64 够放)
13
+ */
14
+ export function deriveSerializeGroup(userKey: string, rawKey: string): Promise<string>;
15
+ export function runScheduledTick(ctx: any): Promise<{
16
+ totalTasks: any;
17
+ successCount: number;
18
+ failedCount: number;
19
+ processedAt: string;
20
+ executionTime: number;
21
+ details: {
22
+ claimSkippedTasks: number;
23
+ serializeSkippedTasks: number;
24
+ deletedOnceOffTasks: number;
25
+ updatedRecurringTasks: number;
26
+ staleTasks: any[];
27
+ deferredTasks: any[];
28
+ cancelledTasks: any[];
29
+ reasoningSkippedTasks: any[];
30
+ redeliveredTasks: any[];
31
+ failedTasks: any[];
32
+ };
33
+ }>;
34
+ /**
35
+ * 只跑一条任务的官方入口,给「fetch 里只来得及 enqueue、真正的 fire 交给
36
+ * CF Queue 消费者(15 分钟预算)跑」这类宿主用——不用再依赖 cron 恰好捞到。
37
+ *
38
+ * 与 cron tick 走完全同一条投递链:占位(含心跳续租)、过期守卫、分组串行
39
+ * (单任务场景下退化为占位时的跨 tick 分组门)、失败重试/终态、hook 全套。
40
+ * 行没到点(next_send_at 在未来)或正处在退避窗口(retry_after 未到点)时不
41
+ * 跑——这个入口只是换了个触发器,不是绕过排期的后门。
42
+ *
43
+ * 四种不跑的情形分开回报,调用方不用猜:
44
+ * - `not_found`:没有这条 uuid。一次性任务发完即删,所以「发完了」的那条也
45
+ * 落在这里;行还在但已经是终态的走下面那条。
46
+ * - `already_settled`:行还在,但已经是 sent / failed(`status` 带上是哪
47
+ * 个)。适配器没实现 `getTaskStatusByUuidOnly` 时这种情况并进 `not_found`。
48
+ * - `not_due`:还没到 `next_send_at`(`nextSendAt` 带上是什么时候)。
49
+ * - `retry_pending`:上次投递失败还在退避窗口里,或者上次 onBeforeFire 把
50
+ * 它推迟了(`{ defer }`)还没到点(`retryAfter` 带上什么时候到点)。
51
+ *
52
+ * 这一次跑的时候 onBeforeFire 返回 `{ defer }` 的话,结果是 `ran: true`,任务
53
+ * 列在 summary.details.deferredTasks 里;到点之前再调都回 `retry_pending`。
54
+ *
55
+ * @param {Object} ctx - 与 runScheduledTick 同一份 ctx
56
+ * @param {string} uuid - 任务 uuid(pending 行)
57
+ * @returns {Promise<{ ran: false, reason: 'not_found'|'already_settled'|'not_due'|'retry_pending', status?: string, nextSendAt?: string, retryAfter?: string }
58
+ * | { ran: true, summary: Object }>} summary 与 runScheduledTick 的返回同构
59
+ * (totalTasks 恒为 1);任务被别的执行者占着时体现在 summary.details.claimSkippedTasks。
60
+ */
61
+ export function runTask(ctx: any, uuid: string): Promise<{
62
+ ran: false;
63
+ reason: "not_found" | "already_settled" | "not_due" | "retry_pending";
64
+ status?: string;
65
+ nextSendAt?: string;
66
+ retryAfter?: string;
67
+ } | {
68
+ ran: true;
69
+ summary: any;
70
+ }>;
71
+ export { DEFAULT_MAX_DELIVERY_RETRIES } from "./retry-policy.js";
72
+ export const DEFAULT_CLAIM_LEASE_MS: number;
73
+ export const DEFAULT_LEASE_HEARTBEAT_MS: number;
74
+ export const DEFAULT_HEARTBEAT_LEASE_TTL_MS: number;
75
+ export const STALE_AFTER_MS: number;
76
+ export { sanitizeErrorSummary };
77
+ export type StaleSkipInfo = {
78
+ reason: "stale";
79
+ /**
80
+ * `expired` = 一次性任务,这一次永远不会补发了,行已标 failed;
81
+ * `fast_forwarded` = 循环任务,攒下的这几次都跳过,排期已快进到未来第一个
82
+ * 名义时刻,行仍是 pending,下一次照常触发。
83
+ */
84
+ action: "expired" | "fast_forwarded";
85
+ /**
86
+ * - 解密 payload 里的 metadata 子字段
87
+ */
88
+ metadata: any | null;
89
+ /**
90
+ * - 'none' / 'daily' / 'weekly'
91
+ */
92
+ recurrenceType: string;
93
+ /**
94
+ * - 被跳过的第一个名义时刻(epoch 毫秒)
95
+ */
96
+ occurrenceMs: number | null;
97
+ /**
98
+ * - 一共跳过几次(一次性任务恒为 1)
99
+ */
100
+ skippedCount: number;
101
+ /**
102
+ * - 被跳过的名义时刻列表(epoch 毫秒);
103
+ * 超过 32 次时只给首末两个,并把 skippedTruncated 置 true
104
+ */
105
+ skippedOccurrences: number[];
106
+ skippedTruncated: boolean;
107
+ /**
108
+ * - 循环任务快进到的下一次触发时刻;一次性任务为 null
109
+ */
110
+ nextSendAt: string | null;
111
+ readState: (namespace: string) => Promise<any[]>;
112
+ writeState: (namespace: string, entries: any[]) => Promise<any>;
113
+ /**
114
+ * 往客户端补一条自定义结果(落收件箱 + 推送,见 lib/result-emitter.js)。
115
+ */
116
+ emitResult: (payload: any) => Promise<{
117
+ messageId: string;
118
+ pushed: boolean;
119
+ }>;
120
+ };
121
+ 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-cloud-data.2";
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
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @typedef {Object} StateAccessors
3
+ * @property {(namespace: string) => Promise<Array<{ namespace: string, key: string, value: string, updatedAt: number }>>} readState
4
+ * 一个 namespace 下的全部条目(值已解密、分块的已拼回原文)。适配器不支持
5
+ * client_state 时返回空数组——读不到状态,hook 走自己的兜底就行。
6
+ * @property {(namespace: string, entries: Array<{ key: string, value: string|null, updatedAt?: number }>) => Promise<{ upserted: number, skipped: number, deleted: number }>} writeState
7
+ * 批量写。`value` 是字符串 → 整条覆盖(不是追加,宿主自己序列化);
8
+ * `value` 为 `null` → 删掉这个 key(连它的分块切片行一起)。`updatedAt`
9
+ * 默认取当前时刻,语义与客户端同步一致:比库里已有值旧的写入(或删除)
10
+ * 不生效,客户端后写的数据不会被这次调用盖回去。
11
+ * 适配器不支持 client_state 时抛错(静默成功会让 push 带上一个指向不存在
12
+ * 数据的引用键)。
13
+ */
14
+ /**
15
+ * 造一份作用于某个用户的状态读写口。
16
+ *
17
+ * 典型用法是「大内容旁路」:一条 Web Push 的正文只有 4KB 出头(见
18
+ * lib/webpush-webcrypto.js 的 MAX_PUSH_PAYLOAD_BYTES),塞不下的内容先写进
19
+ * client_state,push 里只带一个引用键,客户端上线后按键取回。
20
+ *
21
+ * 谁清、什么时候清:库不做 TTL 也不自动回收,写进去的东西一直在,直到有人
22
+ * 覆盖或删除它。旁路内容建议放在固定的少量 key 上(例如每个角色一个),下次
23
+ * 写同一个 key 直接覆盖,存量天然有上限;一次性的大内容在确认客户端取走后,
24
+ * 用 `{ key, value: null }` 删掉。`DELETE /client-state` 仍然是清空这个用户
25
+ * 全部状态的兜底。
26
+ *
27
+ * @param {Object} args
28
+ * @param {import('../adapters/interface.js').DbAdapter} args.db
29
+ * @param {string} args.userId
30
+ * @param {string} args.userKey - per-user 存储密钥
31
+ * @param {number} [args.maxStateValueBytes] - 单条 value 的字节上限(默认 5MB)
32
+ * @param {() => number} [args.now] - 取当前时刻(测试可注入假时钟)
33
+ * @returns {StateAccessors}
34
+ */
35
+ export function createStateAccessors({ db, userId, userKey, maxStateValueBytes, now }: {
36
+ db: import("../adapters/interface.js").DbAdapter;
37
+ userId: string;
38
+ userKey: string;
39
+ maxStateValueBytes?: number;
40
+ now?: () => number;
41
+ }): StateAccessors;
42
+ export type StateAccessors = {
43
+ /**
44
+ * 一个 namespace 下的全部条目(值已解密、分块的已拼回原文)。适配器不支持
45
+ * client_state 时返回空数组——读不到状态,hook 走自己的兜底就行。
46
+ */
47
+ readState: (namespace: string) => Promise<Array<{
48
+ namespace: string;
49
+ key: string;
50
+ value: string;
51
+ updatedAt: number;
52
+ }>>;
53
+ /**
54
+ * 批量写。`value` 是字符串 → 整条覆盖(不是追加,宿主自己序列化);
55
+ * `value` 为 `null` → 删掉这个 key(连它的分块切片行一起)。`updatedAt`
56
+ * 默认取当前时刻,语义与客户端同步一致:比库里已有值旧的写入(或删除)
57
+ * 不生效,客户端后写的数据不会被这次调用盖回去。
58
+ * 适配器不支持 client_state 时抛错(静默成功会让 push 带上一个指向不存在
59
+ * 数据的引用键)。
60
+ */
61
+ writeState: (namespace: string, entries: Array<{
62
+ key: string;
63
+ value: string | null;
64
+ updatedAt?: number;
65
+ }>) => Promise<{
66
+ upserted: number;
67
+ skipped: number;
68
+ deleted: number;
69
+ }>;
70
+ };
@@ -0,0 +1,65 @@
1
+ /** 某个用户 namespace 的切片行所在的保留 namespace。 */
2
+ export function chunkNamespaceFor(namespace: any): string;
3
+ /** 这个 namespace 是不是库内部的切片保留 namespace。 */
4
+ export function isChunkNamespace(namespace: any): boolean;
5
+ /** 第 index 片的存储 key。 */
6
+ export function chunkKeyFor(key: any, index: any): string;
7
+ /** 清理某 key 全部切片行用的 key 前缀。 */
8
+ export function chunkKeyPrefixFor(key: any): string;
9
+ /** 分块根行的 value(纯文本 marker,不加密——不含用户数据)。 */
10
+ export function buildChunkedRootValue(chunkCount: any): string;
11
+ /**
12
+ * 严格解析根行 marker。不是 marker(普通密文 / 任意文本 / 计数非正整数)
13
+ * → null,调用方按普通单行处理。
14
+ *
15
+ * @param {unknown} value
16
+ * @returns {number | null}
17
+ */
18
+ export function parseChunkedRootCount(value: unknown): number | null;
19
+ /**
20
+ * 把超限 value 切成 ≤ STATE_CHUNK_SLICE_BYTES 的切片。切点在码点边界
21
+ * (chunkReasoningByUtf8Bytes 保证多字节字符 / emoji 代理对不被劈开,
22
+ * join('') === 原文)。
23
+ *
24
+ * @param {string} value
25
+ * @returns {string[]}
26
+ */
27
+ export function splitStateValue(value: string): string[];
28
+ /**
29
+ * 把一个 namespace 的存储行解析成逻辑条目(GET /client-state 与 readState
30
+ * 共用)。普通行解密直读;分块根行按 marker 拼回。切片行查询是惰性的:
31
+ * 整个 namespace 没有分块根行时一次都不发。
32
+ *
33
+ * @param {Array<{ namespace: string, key: string, value: string, updated_at: number }>} rows
34
+ * 用户 namespace 的存储行(getClientState 返回值)。
35
+ * @param {() => Promise<Array<{ key: string, value: string, updated_at: number }>>} fetchChunkRows
36
+ * 取该 namespace 对应保留 namespace 的全部切片行(最多调用一次)。
37
+ * @param {(value: string) => Promise<string>} decryptValue
38
+ * @returns {Promise<Array<{ namespace: string, key: string, value: string, updatedAt: number }>>}
39
+ */
40
+ export function resolveClientStateEntries(rows: Array<{
41
+ namespace: string;
42
+ key: string;
43
+ value: string;
44
+ updated_at: number;
45
+ }>, fetchChunkRows: () => Promise<Array<{
46
+ key: string;
47
+ value: string;
48
+ updated_at: number;
49
+ }>>, decryptValue: (value: string) => Promise<string>): Promise<Array<{
50
+ namespace: string;
51
+ key: string;
52
+ value: string;
53
+ updatedAt: number;
54
+ }>>;
55
+ export const STATE_CHUNK_SLICE_BYTES: number;
56
+ export const DEFAULT_MAX_STATE_VALUE_BYTES: number;
57
+ export const INTERNAL_STATE_CHAR_RE: RegExp;
58
+ /**
59
+ * 切片行所在保留 namespace 的固定前缀(`\u001famsg-chunks\u001f`)。
60
+ *
61
+ * 露出来是给「按前缀折算统计」那类调用方用的(见 handlers/client-state-namespaces.js):
62
+ * 保留 namespace 对宿主是不存在的东西,统计要把它折回原 namespace。适配器不
63
+ * import 这一份——分块是 lib 层的事,前缀由调用方传下去。
64
+ */
65
+ export const CHUNK_NAMESPACE_PREFIX: "\u001Famsg-chunks\u001F";
@@ -0,0 +1,92 @@
1
+ /**
2
+ * 任务行的对外投影:`GET /messages` 列出来的那一份形状。
3
+ *
4
+ * 一份实现两处用:HTTP 列表接口,以及 fire-time hook 的
5
+ * `ctx.scheduleTask()` 撞 uuid 时回给宿主的那条已存在任务。两边给出的字段
6
+ * 一样,宿主拿到哪一份都能直接进自己的任务面板。
7
+ *
8
+ * 白名单式取字段:解密后的 payload 里还躺着 `apiKey` / `apiUrl` /
9
+ * `pushSubscription` 等凭据,投影只挑下面列出的那几个,凭据一个都不出现。
10
+ */
11
+ /**
12
+ * @typedef {Object} TaskProjection
13
+ * @property {number|null} id
14
+ * @property {string|null} uuid
15
+ * @property {string} contactName
16
+ * @property {string} messageType
17
+ * @property {string} messageSubtype
18
+ * @property {string|null} nextSendAt
19
+ * @property {string} recurrenceType
20
+ * @property {string|null} tzId - 循环推进用的 IANA 时区 id;没设 → null(按 UTC 推进)
21
+ * @property {string|null} status
22
+ * @property {number} retryCount
23
+ * @property {string|null} createdAt
24
+ * @property {string|null} updatedAt
25
+ * @property {string|null} charId - 取自 metadata.charId
26
+ * @property {string|null} clientTaskId - 取自 metadata.amsgClientTaskId
27
+ * @property {Object|null} credRefs - 凭据引用({ <purpose>: <cred_id> });引用不是机密,客户端对账要看。没带 → null
28
+ * @property {Object|null} [metadata] - 只有 includeMetadata 时才有,见下
29
+ * @property {{ at: string, occurrence: string, reason: string, errorCode?: string, pushStatus?: number }|null} lastError
30
+ * `errorCode` 是底层错误的稳定 code(如 `PUSH_PAYLOAD_TOO_LARGE`),拿得到就
31
+ * 带上;`pushStatus` 只在投递失败于推送这一步时出现,值是推送服务回的 HTTP
32
+ * 状态码。判断该怎么处置读这两个字段,别去正则匹配 `reason`。
33
+ */
34
+ /**
35
+ * @param {Object} row - 数据库任务行
36
+ * @param {Object} decryptedPayload - 解密后的任务 payload
37
+ * @param {{ includeMetadata?: boolean }} [options]
38
+ * includeMetadata 时多带一个完整的 `metadata`。列表接口不带:一页最多 100
39
+ * 条,每条都驮着宿主的整份 metadata 会把响应撑得很大,而列表要的只是「有哪
40
+ * 些任务」。要整份 metadata 的是另一件事——`PUT /update-message` 对 metadata
41
+ * 是整体替换,宿主想只改其中一个子字段就必须先读回完整的那份,那条路径走
42
+ * `GET /message?id=<uuid>`(单条,带完整 metadata)。
43
+ * @returns {TaskProjection}
44
+ */
45
+ export function projectTask(row: any, decryptedPayload: any, options?: {
46
+ includeMetadata?: boolean;
47
+ }): TaskProjection;
48
+ export type TaskProjection = {
49
+ id: number | null;
50
+ uuid: string | null;
51
+ contactName: string;
52
+ messageType: string;
53
+ messageSubtype: string;
54
+ nextSendAt: string | null;
55
+ recurrenceType: string;
56
+ /**
57
+ * - 循环推进用的 IANA 时区 id;没设 → null(按 UTC 推进)
58
+ */
59
+ tzId: string | null;
60
+ status: string | null;
61
+ retryCount: number;
62
+ createdAt: string | null;
63
+ updatedAt: string | null;
64
+ /**
65
+ * - 取自 metadata.charId
66
+ */
67
+ charId: string | null;
68
+ /**
69
+ * - 取自 metadata.amsgClientTaskId
70
+ */
71
+ clientTaskId: string | null;
72
+ /**
73
+ * - 凭据引用({ <purpose>: <cred_id> });引用不是机密,客户端对账要看。没带 → null
74
+ */
75
+ credRefs: any | null;
76
+ /**
77
+ * - 只有 includeMetadata 时才有,见下
78
+ */
79
+ metadata?: any | null;
80
+ /**
81
+ * `errorCode` 是底层错误的稳定 code(如 `PUSH_PAYLOAD_TOO_LARGE`),拿得到就
82
+ * 带上;`pushStatus` 只在投递失败于推送这一步时出现,值是推送服务回的 HTTP
83
+ * 状态码。判断该怎么处置读这两个字段,别去正则匹配 `reason`。
84
+ */
85
+ lastError: {
86
+ at: string;
87
+ occurrence: string;
88
+ reason: string;
89
+ errorCode?: string;
90
+ pushStatus?: number;
91
+ } | null;
92
+ };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Validate ISO 8601 date string.
3
+ * @param {string} dateString
4
+ * @returns {boolean}
5
+ */
6
+ export function isValidISO8601(dateString: string): boolean;
7
+ /**
8
+ * Validate UUID format.
9
+ * @param {string} uuid
10
+ * @returns {boolean}
11
+ */
12
+ export function isValidUUID(uuid: string): boolean;
13
+ /**
14
+ * Validate UUID v4 format.
15
+ * @param {string} uuid
16
+ * @returns {boolean}
17
+ */
18
+ export function isValidUUIDv4(uuid: string): boolean;
19
+ /**
20
+ * Validate the optional `splitPattern` field (amsg-server scheduled tasks
21
+ * only; amsg-instant 0.8.0 dropped its request-level `splitPattern`).
22
+ * Accepts `string`, `string[]`, or absent/null. Returns an error message
23
+ * string, or null when valid.
24
+ *
25
+ * Limits (per-item length ≤ 200, array ≤ 10 items, must compile via
26
+ * `new RegExp(item)`) are an **input-size guard**, NOT a ReDoS defense —
27
+ * a 6-character pattern like `(a+)+$` is enough to trigger catastrophic
28
+ * backtracking. The real backstop is Worker / runtime CPU limits + the
29
+ * fact that splitPattern is stored under the user's own encrypted task
30
+ * and matched against output from the user's own LLM API key, so the
31
+ * blast radius is self-inflicted only (no cross-tenant attack surface).
32
+ *
33
+ * @param {unknown} value
34
+ * @returns {string | null}
35
+ */
36
+ export function validateSplitPattern(value: unknown): string | null;
37
+ /**
38
+ * Validate an OpenAI-style messages array. 形状规则统一在
39
+ * `@rei-standard/amsg-shared` 的 `validateLlmMessagesShape`(amsg-instant
40
+ * 的 `validateMessagesArray` 走同一实现,两包接受的形状不会漂移)——
41
+ * 因此 agentic 会话回放(assistant 带 `tool_calls` 时 content 可空、
42
+ * `role:'tool'` 消息要求 `tool_call_id`)在这里同样合法。本函数只把
43
+ * 结构化错误码映射成本包既有的英文错误文案,返回形状不变。
44
+ *
45
+ * @param {unknown} messages
46
+ * @returns {string | null} Error message, or null if valid.
47
+ */
48
+ export function validateLlmMessagesArray(messages: unknown): string | null;
49
+ /**
50
+ * 落库前量一次任务正文。合法返回 null,超限返回可以直接放进 400 响应体的
51
+ * error 对象——`POST /schedule-message` 与 `PUT /update-message` 共用同一个错
52
+ * 误码和同一套 details,下游判断读 `details.bytes` / `details.maxBytes`,不用
53
+ * 去解析那句人话。
54
+ *
55
+ * 按 UTF-8 字节算而不是字符数:D1 限的是字节,加密前也是先转 UTF-8,一段全中
56
+ * 文的正文字符数只有字节数的三分之一,用 `.length` 量等于把上限放大三倍。
57
+ *
58
+ * @param {string} serializedPayload - 将要交给 encryptForStorage 的那个字符串。
59
+ * @returns {{ code: string, message: string, details: { bytes: number, maxBytes: number } } | null}
60
+ */
61
+ export function validateTaskPayloadSize(serializedPayload: string): {
62
+ code: string;
63
+ message: string;
64
+ details: {
65
+ bytes: number;
66
+ maxBytes: number;
67
+ };
68
+ } | null;
69
+ /**
70
+ * 任务正文的 UTF-8 字节数(与 {@link validateTaskPayloadSize} 同一把尺子)。
71
+ *
72
+ * `PUT /update-message` 拿它跟改动前的正文比大小:上限是后加的,比它更早建出来
73
+ * 的大任务不该因此连排期都改不了,只要这次改动没把它变得更大就放行。
74
+ *
75
+ * @param {string} serializedPayload
76
+ * @returns {number}
77
+ */
78
+ export function taskPayloadByteLength(serializedPayload: string): number;
79
+ /**
80
+ * Validate the schedule-message request payload.
81
+ *
82
+ * @param {Object} payload
83
+ * @returns {{ valid: boolean, errorCode?: string, errorMessage?: string, details?: Object }}
84
+ */
85
+ export function validateScheduleMessagePayload(payload: any): {
86
+ valid: boolean;
87
+ errorCode?: string;
88
+ errorMessage?: string;
89
+ details?: any;
90
+ };
91
+ /**
92
+ * 一条任务正文(`JSON.stringify` 的结果)的明文上限,UTF-8 字节数。
93
+ * 客户端想在提交前自己预算就读这个数(从 `@rei-standard/amsg-server` 导出)。
94
+ */
95
+ export const MAX_TASK_PAYLOAD_BYTES: number;
96
+ import { isValidTimeZoneId } from './recurrence.js';
97
+ export { isValidTimeZoneId, isValidUrl, validateAvatarUrl };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 构建期注入的包版本。tsup 用 define 把 __AMSG_SERVER_VERSION__ 替换成
3
+ * package.json 的 version(见 tsup.config.js),发布产物里是真实版本号;
4
+ * 直接跑 src(node --test / 本地调试)没有这个替换,typeof 守卫落到
5
+ * '0.0.0-dev'。
6
+ */
7
+ export const SERVER_VERSION: any;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 组 payload 前做预算用:先量骨架有多大,剩下多少字节才是能塞附加数据的额度。
3
+ *
4
+ * @example
5
+ * // hook 里组 push:把库要补的信封字段先扣掉
6
+ * const { remainingBytes } = measurePushPayload(
7
+ * JSON.stringify({ ...basePush, message: '' }),
8
+ * { reserveEnvelope: true }
9
+ * );
10
+ * const excerpt = remainingBytes > 0 ? text.slice(0, remainingBytes) : '';
11
+ *
12
+ * @param {string} payload - 待发送的 payload 字符串(通常是 JSON.stringify 的结果)。
13
+ * @param {{ reserveEnvelope?: boolean }} [options]
14
+ * `reserveEnvelope: true` → 额度里扣掉 {@link PUSH_ENVELOPE_RESERVED_BYTES},
15
+ * 也就是「库补完字段之后还装得下」的口径。fire-time hook 组 payload 时用这个。
16
+ * 默认 false = 量这个字符串本身对上限的占用。
17
+ * @returns {{ bytes: number, maxBytes: number, remainingBytes: number, withinLimit: boolean, envelopeReservedBytes: number }}
18
+ * `bytes` 是 payload 的 UTF-8 字节数;`maxBytes` 是本次口径下的上限;
19
+ * `remainingBytes` 是还能加多少字节(已超限时为负);`withinLimit` 为 false
20
+ * 时(不留信封的口径下)sendWebPush 会抛 `PUSH_PAYLOAD_TOO_LARGE`。
21
+ */
22
+ export function measurePushPayload(payload: string, options?: {
23
+ reserveEnvelope?: boolean;
24
+ }): {
25
+ bytes: number;
26
+ maxBytes: number;
27
+ remainingBytes: number;
28
+ withinLimit: boolean;
29
+ envelopeReservedBytes: number;
30
+ };
31
+ /**
32
+ * Send a single Web Push notification. 委托 shared 的 `sendWebPush`(参数、
33
+ * 返回值与之完全一致),只在加密前多做一步 payload 大小护栏——超限的
34
+ * payload 发出去只会被推送服务 413 掉,用户什么也收不到。加密之前就拦下
35
+ * 来,让调用方拿到一个说得清的错误(宿主可以退回一条短消息 + 引用键)。
36
+ *
37
+ * @param {Object} args - 同 shared `sendWebPush`:{ subscription, payload, vapid, ttl, fetch }。
38
+ * @returns {Promise<{ statusCode: number, body: string, headers: Headers }>}
39
+ * @throws {Error} err.code = 'PUSH_PAYLOAD_TOO_LARGE' when the payload
40
+ * exceeds MAX_PUSH_PAYLOAD_BYTES (nothing is sent);
41
+ * err.code = 'PUSH_SEND_FAILED' on push-service error.
42
+ */
43
+ export function sendWebPush(args: any): Promise<{
44
+ statusCode: number;
45
+ body: string;
46
+ headers: Headers;
47
+ }>;
48
+ /**
49
+ * web-push-compatible sender backed by the shared Web Crypto implementation.
50
+ * message-processor calls `ctx.webpush.sendNotification(subscription, payloadString)`,
51
+ * so we only need that one method. VAPID keys are baked in at construction.
52
+ *
53
+ * @param {{ email: string, publicKey: string, privateKey: string }} [vapid]
54
+ * @param {{ ttl?: number }} [options] - Push TTL in seconds; defaults to 4 weeks
55
+ * (matches the web-push backend) so scheduled pushes outlive an offline device.
56
+ * @returns {{ sendNotification: (subscription: Object, payload: string) => Promise<any> }}
57
+ */
58
+ export function createWebCryptoWebPush(vapid?: {
59
+ email: string;
60
+ publicKey: string;
61
+ privateKey: string;
62
+ }, { ttl }?: {
63
+ ttl?: number;
64
+ }): {
65
+ sendNotification: (subscription: any, payload: string) => Promise<any>;
66
+ };
67
+ /** 推送服务对加密后 body 的上限(字节)。 */
68
+ export const WEB_PUSH_MAX_BODY_BYTES: 4096;
69
+ /** aes128gcm 固定开销:header 86 + 填充分隔符 1 + GCM tag 16。 */
70
+ export const WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES: number;
71
+ /** 一条 push 的明文(JSON 字符串)上限,UTF-8 字节数。 */
72
+ export const MAX_PUSH_PAYLOAD_BYTES: number;
73
+ /**
74
+ * 库在 hook 交还 payload 之后还要补的那批字段占的字节数上界。
75
+ * 组 payload 时按 `MAX_PUSH_PAYLOAD_BYTES - PUSH_ENVELOPE_RESERVED_BYTES`
76
+ * 预算,或者直接用 `measurePushPayload(s, { reserveEnvelope: true })`。
77
+ */
78
+ export const PUSH_ENVELOPE_RESERVED_BYTES: 384;