@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.
- package/LICENSE +21 -0
- package/README.md +599 -24
- package/dist/adapters/d1.d.ts +416 -0
- package/dist/adapters/factory.d.ts +39 -0
- package/dist/adapters/interface.d.ts +306 -0
- package/dist/adapters/neon.d.ts +88 -0
- package/dist/adapters/pg-shared.d.ts +161 -0
- package/dist/adapters/pg.d.ts +84 -0
- package/dist/adapters/schema.d.ts +57 -0
- package/dist/adapters/schema.sqlite.d.ts +49 -0
- package/dist/chunk-7NMQFTDJ.cjs +148 -0
- package/dist/chunk-E7OWP3VL.cjs +135 -0
- package/dist/chunk-GN44PST5.mjs +148 -0
- package/dist/chunk-KOJQVOYS.mjs +5847 -0
- package/dist/chunk-QHVID3V2.cjs +5847 -0
- package/dist/chunk-ZGGF4GMA.mjs +135 -0
- package/dist/cloudflare/single-user-worker.d.ts +37 -0
- package/dist/cloudflare.cjs +39 -2
- package/dist/cloudflare.d.ts +10 -2
- package/dist/cloudflare.mjs +40 -3
- package/dist/handlers/cancel-message.d.ts +3 -0
- package/dist/handlers/capabilities.d.ts +4 -0
- package/dist/handlers/client-state.d.ts +7 -0
- package/dist/handlers/get-message.d.ts +3 -0
- package/dist/handlers/get-user-key.d.ts +3 -0
- package/dist/handlers/init-tenant.d.ts +23 -0
- package/dist/handlers/llm-credentials.d.ts +5 -0
- package/dist/handlers/messages.d.ts +3 -0
- package/dist/handlers/outbox.d.ts +7 -0
- package/dist/handlers/push-subscription.d.ts +5 -0
- package/dist/handlers/schedule-message.d.ts +3 -0
- package/dist/handlers/send-notifications.d.ts +3 -0
- package/dist/handlers/single-user-init.d.ts +13 -0
- package/dist/handlers/update-message.d.ts +3 -0
- package/dist/handlers/vapid-public-key.d.ts +19 -0
- package/dist/index.cjs +133 -44
- package/dist/index.d.cts +131 -766
- package/dist/index.d.ts +131 -766
- package/dist/index.mjs +125 -36
- package/dist/lib/agentic-fire.d.ts +108 -0
- package/dist/lib/client-state-store.d.ts +80 -0
- package/dist/lib/constant-time.d.ts +14 -0
- package/dist/lib/db-errors.d.ts +10 -0
- package/dist/lib/encryption.d.ts +48 -0
- package/dist/lib/errors.d.ts +215 -0
- package/dist/lib/llm-credentials-store.d.ts +147 -0
- package/dist/lib/llm.d.ts +1 -0
- package/dist/lib/message-processor.d.ts +73 -0
- package/dist/lib/outbox-store.d.ts +95 -0
- package/dist/lib/push-subscription-store.d.ts +94 -0
- package/dist/lib/recurrence.d.ts +51 -0
- package/dist/lib/request.d.ts +182 -0
- package/dist/lib/result-emitter.d.ts +54 -0
- package/dist/lib/run-tick.d.ts +115 -0
- package/dist/lib/schema-version.d.ts +50 -0
- package/dist/lib/state-accessors.d.ts +70 -0
- package/dist/lib/state-chunks.d.ts +55 -0
- package/dist/lib/task-projection.d.ts +92 -0
- package/dist/lib/validation.d.ts +97 -0
- package/dist/lib/version.d.ts +7 -0
- package/dist/lib/webcrypto-utils.d.ts +1 -0
- package/dist/lib/webpush-webcrypto.d.ts +78 -0
- package/dist/{neon-BFUS25UX.cjs → neon-KP2CPA57.cjs} +110 -10
- package/dist/{neon-CU5N3CSW.mjs → neon-ZIMHALYI.mjs} +106 -6
- package/dist/{pg-QO6NKTGL.mjs → pg-QGC2XHUT.mjs} +94 -5
- package/dist/{pg-IIH3M4OM.cjs → pg-RZPQGXR3.cjs} +98 -9
- package/dist/single-user.d.ts +95 -0
- package/dist/tenant/blob-store.d.ts +30 -0
- package/dist/tenant/context.d.ts +47 -0
- package/dist/tenant/single-user-context.d.ts +29 -0
- package/dist/tenant/token.d.ts +26 -0
- package/package.json +3 -3
- package/dist/chunk-5ENJGVZX.mjs +0 -72
- package/dist/chunk-GN5QTMCL.cjs +0 -1870
- package/dist/chunk-PVNLB6TF.mjs +0 -1870
- package/dist/chunk-RGECD4OH.cjs +0 -72
- package/dist/cloudflare-B-E51VjP.d.cts +0 -2848
- package/dist/cloudflare-B-E51VjP.d.ts +0 -2848
- package/dist/cloudflare.d.cts +0 -2
- package/dist/neon-CJl66EGy.d.cts +0 -251
- package/dist/neon-DYvGnCzx.d.ts +0 -251
- package/dist/pg-07S-u_H4.d.cts +0 -246
- package/dist/pg-Du-pN_UT.d.ts +0 -246
- package/dist/schema-C8OnYk6j.d.cts +0 -74
- package/dist/schema-C8OnYk6j.d.ts +0 -74
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把请求正文读成字符串,`Content-Encoding: gzip` 的在这一步还原。
|
|
3
|
+
*
|
|
4
|
+
* 客户端把大 body(一次 fire_pack、一整批 client_state)压了再传能省下几倍
|
|
5
|
+
* 传输量,代价只有服务端这一次解压。所有带 body 的端点走同一个入口,谁都不用
|
|
6
|
+
* 各自判一遍这个头。
|
|
7
|
+
*
|
|
8
|
+
* 说是 gzip、字节却不是 gzip 开头时按明文处理:有些边缘网关会替你把请求体解
|
|
9
|
+
* 开却留着原来的 `Content-Encoding` 头,再解一次只会解出乱码。魔数判断两种情
|
|
10
|
+
* 况都接得住。
|
|
11
|
+
*
|
|
12
|
+
* 只认 gzip。deflate / br 回 415 而不是猜着解——猜错解出来的是一段乱码,会变
|
|
13
|
+
* 成一句让人找不着北的「请求体不是有效的 JSON」。
|
|
14
|
+
*
|
|
15
|
+
* @param {{ headers?: { get?: (name: string) => string|null }, text: () => Promise<string>, arrayBuffer: () => Promise<ArrayBuffer> }} request
|
|
16
|
+
* Fetch API 的 Request(或形状相同的对象)。
|
|
17
|
+
* @param {{ maxBytes?: number }} [options] - `maxBytes`:解压后的字节上限,
|
|
18
|
+
* 默认 {@link DEFAULT_MAX_REQUEST_BODY_BYTES}。
|
|
19
|
+
* @returns {Promise<{ ok: true, body: string } | { ok: false, error: { status: number, body: Object } }>}
|
|
20
|
+
* `error` 就是可以直接回给调用方的那个信封(与 handler 的返回同构)。
|
|
21
|
+
*/
|
|
22
|
+
export function readRequestBody(request: {
|
|
23
|
+
headers?: {
|
|
24
|
+
get?: (name: string) => string | null;
|
|
25
|
+
};
|
|
26
|
+
text: () => Promise<string>;
|
|
27
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
28
|
+
}, options?: {
|
|
29
|
+
maxBytes?: number;
|
|
30
|
+
}): Promise<{
|
|
31
|
+
ok: true;
|
|
32
|
+
body: string;
|
|
33
|
+
} | {
|
|
34
|
+
ok: false;
|
|
35
|
+
error: {
|
|
36
|
+
status: number;
|
|
37
|
+
body: any;
|
|
38
|
+
};
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {{ code: string, message: string }} ValidationError
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {{
|
|
45
|
+
* invalidJson?: ValidationError,
|
|
46
|
+
* invalidType?: ValidationError
|
|
47
|
+
* }} ParseBodyOptions
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {{
|
|
51
|
+
* ok: true,
|
|
52
|
+
* data: Record<string, any>
|
|
53
|
+
* } | {
|
|
54
|
+
* ok: false,
|
|
55
|
+
* error: ValidationError
|
|
56
|
+
* }} ParseBodyResult
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Parse body into a JSON object.
|
|
60
|
+
*
|
|
61
|
+
* @param {unknown} body
|
|
62
|
+
* @param {ParseBodyOptions} [options]
|
|
63
|
+
* @returns {ParseBodyResult}
|
|
64
|
+
*/
|
|
65
|
+
export function parseBodyAsObject(body: unknown, options?: ParseBodyOptions): ParseBodyResult;
|
|
66
|
+
/**
|
|
67
|
+
* Parse a standard JSON object body.
|
|
68
|
+
*
|
|
69
|
+
* @param {unknown} body
|
|
70
|
+
* @returns {ParseBodyResult}
|
|
71
|
+
*/
|
|
72
|
+
export function parseJsonBody(body: unknown): ParseBodyResult;
|
|
73
|
+
/**
|
|
74
|
+
* Check if a value is a plain object (and not null/array).
|
|
75
|
+
*
|
|
76
|
+
* @param {unknown} value
|
|
77
|
+
* @returns {value is Record<string, any>}
|
|
78
|
+
*/
|
|
79
|
+
export function isPlainObject(value: unknown): value is Record<string, any>;
|
|
80
|
+
/**
|
|
81
|
+
* Check if an object follows the encrypted payload envelope shape.
|
|
82
|
+
*
|
|
83
|
+
* @param {unknown} payload
|
|
84
|
+
* @returns {payload is { iv: string, authTag: string, encryptedData: string }}
|
|
85
|
+
*/
|
|
86
|
+
export function isEncryptedEnvelope(payload: unknown): payload is {
|
|
87
|
+
iv: string;
|
|
88
|
+
authTag: string;
|
|
89
|
+
encryptedData: string;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Parse and validate an encrypted payload envelope.
|
|
93
|
+
*
|
|
94
|
+
* @param {unknown} body
|
|
95
|
+
* @returns {ParseBodyResult}
|
|
96
|
+
*/
|
|
97
|
+
export function parseEncryptedBody(body: unknown): ParseBodyResult;
|
|
98
|
+
/**
|
|
99
|
+
* 标准错误信封:{ status, body: { success: false, error: { code, message, details? } } }。
|
|
100
|
+
*
|
|
101
|
+
* @param {number} status
|
|
102
|
+
* @param {string} code
|
|
103
|
+
* @param {string} message
|
|
104
|
+
* @param {Object} [details]
|
|
105
|
+
*/
|
|
106
|
+
export function errorResponse(status: number, code: string, message: string, details?: any): {
|
|
107
|
+
status: number;
|
|
108
|
+
body: {
|
|
109
|
+
success: boolean;
|
|
110
|
+
error: {
|
|
111
|
+
code: string;
|
|
112
|
+
message: string;
|
|
113
|
+
details?: undefined;
|
|
114
|
+
} | {
|
|
115
|
+
code: string;
|
|
116
|
+
message: string;
|
|
117
|
+
details: any;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* X-User-Id 门禁。所有按用户读写的端点共用这一份:规则(必填 + UUID v4)和
|
|
123
|
+
* 文案只此一处,改口径不用挨个 handler 找复制粘贴的副本。
|
|
124
|
+
*
|
|
125
|
+
* @param {Record<string, any>} headers
|
|
126
|
+
* @returns {{ userId: string, error?: undefined } | { error: ReturnType<typeof errorResponse> }}
|
|
127
|
+
*/
|
|
128
|
+
export function requireUserId(headers: Record<string, any>): {
|
|
129
|
+
userId: string;
|
|
130
|
+
error?: undefined;
|
|
131
|
+
} | {
|
|
132
|
+
error: ReturnType<typeof errorResponse>;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Read a header value case-insensitively.
|
|
136
|
+
*
|
|
137
|
+
* @param {Record<string, any>} headers
|
|
138
|
+
* @param {string} name
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
export function getHeader(headers: Record<string, any>, name: string): string;
|
|
142
|
+
export namespace REQUEST_ERRORS {
|
|
143
|
+
namespace INVALID_JSON {
|
|
144
|
+
let code: string;
|
|
145
|
+
let message: string;
|
|
146
|
+
}
|
|
147
|
+
namespace INVALID_REQUEST_BODY {
|
|
148
|
+
let code_1: string;
|
|
149
|
+
export { code_1 as code };
|
|
150
|
+
let message_1: string;
|
|
151
|
+
export { message_1 as message };
|
|
152
|
+
}
|
|
153
|
+
namespace INVALID_ENCRYPTED_PAYLOAD {
|
|
154
|
+
let code_2: string;
|
|
155
|
+
export { code_2 as code };
|
|
156
|
+
let message_2: string;
|
|
157
|
+
export { message_2 as message };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 解压后请求体的字节上限(默认 32MB)。
|
|
162
|
+
*
|
|
163
|
+
* 只管 gzip 那条路:压缩数据能用很小的体积展开成极大的正文(几百 KB 换几个
|
|
164
|
+
* GB),不设上限等于把内存交给调用方决定。不压缩的请求体不受这个数约束,行为
|
|
165
|
+
* 与以前一致——那条路的体积本来就摆在传输量上,平台自己的请求大小限制盖得住。
|
|
166
|
+
*/
|
|
167
|
+
export const DEFAULT_MAX_REQUEST_BODY_BYTES: number;
|
|
168
|
+
export type ValidationError = {
|
|
169
|
+
code: string;
|
|
170
|
+
message: string;
|
|
171
|
+
};
|
|
172
|
+
export type ParseBodyOptions = {
|
|
173
|
+
invalidJson?: ValidationError;
|
|
174
|
+
invalidType?: ValidationError;
|
|
175
|
+
};
|
|
176
|
+
export type ParseBodyResult = {
|
|
177
|
+
ok: true;
|
|
178
|
+
data: Record<string, any>;
|
|
179
|
+
} | {
|
|
180
|
+
ok: false;
|
|
181
|
+
error: ValidationError;
|
|
182
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} ResultEmitter
|
|
3
|
+
* @property {(payload: Object) => Promise<{ messageId: string, pushed: boolean }>} emitResult
|
|
4
|
+
* 把一条结果落进收件箱并推送出去。`payload` 是宿主自己的形状,只有两个约束:
|
|
5
|
+
* 必须是普通对象,且带一个非空的 `resultKind`(这类结果的名字,客户端按它
|
|
6
|
+
* 分流)。返回 `messageId`(补收和去重的键)与 `pushed`(这次推送有没有真
|
|
7
|
+
* 的发出去;`false` 只表示没能当场送达,行还在收件箱里等补收)。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 造一份作用于某条任务的结果出口。
|
|
11
|
+
*
|
|
12
|
+
* 身份字段(`taskId` / `taskUuid` / `recurrenceType` / `occurrenceMs` /
|
|
13
|
+
* `messageKind`)由库覆盖写:它们描述的是这条任务行的事实,不是内容。
|
|
14
|
+
* `messageId` / `sessionId` / `timestamp` 宿主没给才补——`messageId` 的缺省
|
|
15
|
+
* 值掺了任务 id 与本次名义触发时刻,同一次触发重试时组出同一个 id,收件箱靠
|
|
16
|
+
* `(user_id, message_id)` 唯一约束天然去重,不会补出第二条。
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} args
|
|
19
|
+
* @param {import('../adapters/interface.js').DbAdapter} args.db
|
|
20
|
+
* @param {Object} args.task - 数据库任务行
|
|
21
|
+
* @param {string} args.userKey - per-user 存储密钥
|
|
22
|
+
* @param {Object} args.decryptedPayload - 解密后的任务 payload(取 messageType / recurrenceType / 老任务里的订阅)
|
|
23
|
+
* @param {string} args.messageIdBase - messageId 的前缀(与推送链路同一份)
|
|
24
|
+
* @param {string} args.sessionId
|
|
25
|
+
* @param {number|null} args.occurrenceMs
|
|
26
|
+
* @param {{ sendNotification: Function }|null} args.webpush
|
|
27
|
+
* @param {() => number} [args.now] - 取当前时刻(测试可注入假时钟)
|
|
28
|
+
* @returns {ResultEmitter}
|
|
29
|
+
*/
|
|
30
|
+
export function createResultEmitter({ db, task, userKey, decryptedPayload, messageIdBase, sessionId, occurrenceMs, webpush, now, }: {
|
|
31
|
+
db: import("../adapters/interface.js").DbAdapter;
|
|
32
|
+
task: any;
|
|
33
|
+
userKey: string;
|
|
34
|
+
decryptedPayload: any;
|
|
35
|
+
messageIdBase: string;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
occurrenceMs: number | null;
|
|
38
|
+
webpush: {
|
|
39
|
+
sendNotification: Function;
|
|
40
|
+
} | null;
|
|
41
|
+
now?: () => number;
|
|
42
|
+
}): ResultEmitter;
|
|
43
|
+
export type ResultEmitter = {
|
|
44
|
+
/**
|
|
45
|
+
* 把一条结果落进收件箱并推送出去。`payload` 是宿主自己的形状,只有两个约束:
|
|
46
|
+
* 必须是普通对象,且带一个非空的 `resultKind`(这类结果的名字,客户端按它
|
|
47
|
+
* 分流)。返回 `messageId`(补收和去重的键)与 `pushed`(这次推送有没有真
|
|
48
|
+
* 的发出去;`false` 只表示没能当场送达,行还在收件箱里等补收)。
|
|
49
|
+
*/
|
|
50
|
+
emitResult: (payload: any) => Promise<{
|
|
51
|
+
messageId: string;
|
|
52
|
+
pushed: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
cancelledTasks: any[];
|
|
28
|
+
reasoningSkippedTasks: any[];
|
|
29
|
+
failedTasks: any[];
|
|
30
|
+
};
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* 只跑一条任务的官方入口,给「fetch 里只来得及 enqueue、真正的 fire 交给
|
|
34
|
+
* CF Queue 消费者(15 分钟预算)跑」这类宿主用——不用再依赖 cron 恰好捞到。
|
|
35
|
+
*
|
|
36
|
+
* 与 cron tick 走完全同一条投递链:占位(含心跳续租)、过期守卫、分组串行
|
|
37
|
+
* (单任务场景下退化为占位时的跨 tick 分组门)、失败重试/终态、hook 全套。
|
|
38
|
+
* 行没到点(next_send_at 在未来)或正处在退避窗口(retry_after 未到点)时不
|
|
39
|
+
* 跑——这个入口只是换了个触发器,不是绕过排期的后门。
|
|
40
|
+
*
|
|
41
|
+
* 四种不跑的情形分开回报,调用方不用猜:
|
|
42
|
+
* - `not_found`:没有这条 uuid。一次性任务发完即删,所以「发完了」的那条也
|
|
43
|
+
* 落在这里;行还在但已经是终态的走下面那条。
|
|
44
|
+
* - `already_settled`:行还在,但已经是 sent / failed(`status` 带上是哪
|
|
45
|
+
* 个)。适配器没实现 `getTaskStatusByUuidOnly` 时这种情况并进 `not_found`。
|
|
46
|
+
* - `not_due`:还没到 `next_send_at`(`nextSendAt` 带上是什么时候)。
|
|
47
|
+
* - `retry_pending`:上次投递失败,还在退避窗口里(`retryAfter` 带上什么时
|
|
48
|
+
* 候到点)。
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} ctx - 与 runScheduledTick 同一份 ctx
|
|
51
|
+
* @param {string} uuid - 任务 uuid(pending 行)
|
|
52
|
+
* @returns {Promise<{ ran: false, reason: 'not_found'|'already_settled'|'not_due'|'retry_pending', status?: string, nextSendAt?: string, retryAfter?: string }
|
|
53
|
+
* | { ran: true, summary: Object }>} summary 与 runScheduledTick 的返回同构
|
|
54
|
+
* (totalTasks 恒为 1);任务被别的执行者占着时体现在 summary.details.claimSkippedTasks。
|
|
55
|
+
*/
|
|
56
|
+
export function runTask(ctx: any, uuid: string): Promise<{
|
|
57
|
+
ran: false;
|
|
58
|
+
reason: "not_found" | "already_settled" | "not_due" | "retry_pending";
|
|
59
|
+
status?: string;
|
|
60
|
+
nextSendAt?: string;
|
|
61
|
+
retryAfter?: string;
|
|
62
|
+
} | {
|
|
63
|
+
ran: true;
|
|
64
|
+
summary: any;
|
|
65
|
+
}>;
|
|
66
|
+
export const DEFAULT_CLAIM_LEASE_MS: number;
|
|
67
|
+
export const DEFAULT_LEASE_HEARTBEAT_MS: number;
|
|
68
|
+
export const DEFAULT_HEARTBEAT_LEASE_TTL_MS: number;
|
|
69
|
+
export const STALE_AFTER_MS: number;
|
|
70
|
+
export { sanitizeErrorSummary };
|
|
71
|
+
export type StaleSkipInfo = {
|
|
72
|
+
reason: "stale";
|
|
73
|
+
/**
|
|
74
|
+
* `expired` = 一次性任务,这一次永远不会补发了,行已标 failed;
|
|
75
|
+
* `fast_forwarded` = 循环任务,攒下的这几次都跳过,排期已快进到未来第一个
|
|
76
|
+
* 名义时刻,行仍是 pending,下一次照常触发。
|
|
77
|
+
*/
|
|
78
|
+
action: "expired" | "fast_forwarded";
|
|
79
|
+
/**
|
|
80
|
+
* - 解密 payload 里的 metadata 子字段
|
|
81
|
+
*/
|
|
82
|
+
metadata: any | null;
|
|
83
|
+
/**
|
|
84
|
+
* - 'none' / 'daily' / 'weekly'
|
|
85
|
+
*/
|
|
86
|
+
recurrenceType: string;
|
|
87
|
+
/**
|
|
88
|
+
* - 被跳过的第一个名义时刻(epoch 毫秒)
|
|
89
|
+
*/
|
|
90
|
+
occurrenceMs: number | null;
|
|
91
|
+
/**
|
|
92
|
+
* - 一共跳过几次(一次性任务恒为 1)
|
|
93
|
+
*/
|
|
94
|
+
skippedCount: number;
|
|
95
|
+
/**
|
|
96
|
+
* - 被跳过的名义时刻列表(epoch 毫秒);
|
|
97
|
+
* 超过 32 次时只给首末两个,并把 skippedTruncated 置 true
|
|
98
|
+
*/
|
|
99
|
+
skippedOccurrences: number[];
|
|
100
|
+
skippedTruncated: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* - 循环任务快进到的下一次触发时刻;一次性任务为 null
|
|
103
|
+
*/
|
|
104
|
+
nextSendAt: string | null;
|
|
105
|
+
readState: (namespace: string) => Promise<any[]>;
|
|
106
|
+
writeState: (namespace: string, entries: any[]) => Promise<any>;
|
|
107
|
+
/**
|
|
108
|
+
* 往客户端补一条自定义结果(落收件箱 + 推送,见 lib/result-emitter.js)。
|
|
109
|
+
*/
|
|
110
|
+
emitResult: (payload: any) => Promise<{
|
|
111
|
+
messageId: string;
|
|
112
|
+
pushed: boolean;
|
|
113
|
+
}>;
|
|
114
|
+
};
|
|
115
|
+
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
|
+
};
|
|
@@ -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,55 @@
|
|
|
1
|
+
/** 某个用户 namespace 的切片行所在的保留 namespace。 */
|
|
2
|
+
export function chunkNamespaceFor(namespace: any): string;
|
|
3
|
+
/** 第 index 片的存储 key。 */
|
|
4
|
+
export function chunkKeyFor(key: any, index: any): string;
|
|
5
|
+
/** 清理某 key 全部切片行用的 key 前缀。 */
|
|
6
|
+
export function chunkKeyPrefixFor(key: any): string;
|
|
7
|
+
/** 分块根行的 value(纯文本 marker,不加密——不含用户数据)。 */
|
|
8
|
+
export function buildChunkedRootValue(chunkCount: any): string;
|
|
9
|
+
/**
|
|
10
|
+
* 严格解析根行 marker。不是 marker(普通密文 / 任意文本 / 计数非正整数)
|
|
11
|
+
* → null,调用方按普通单行处理。
|
|
12
|
+
*
|
|
13
|
+
* @param {unknown} value
|
|
14
|
+
* @returns {number | null}
|
|
15
|
+
*/
|
|
16
|
+
export function parseChunkedRootCount(value: unknown): number | null;
|
|
17
|
+
/**
|
|
18
|
+
* 把超限 value 切成 ≤ STATE_CHUNK_SLICE_BYTES 的切片。切点在码点边界
|
|
19
|
+
* (chunkReasoningByUtf8Bytes 保证多字节字符 / emoji 代理对不被劈开,
|
|
20
|
+
* join('') === 原文)。
|
|
21
|
+
*
|
|
22
|
+
* @param {string} value
|
|
23
|
+
* @returns {string[]}
|
|
24
|
+
*/
|
|
25
|
+
export function splitStateValue(value: string): string[];
|
|
26
|
+
/**
|
|
27
|
+
* 把一个 namespace 的存储行解析成逻辑条目(GET /client-state 与 readState
|
|
28
|
+
* 共用)。普通行解密直读;分块根行按 marker 拼回。切片行查询是惰性的:
|
|
29
|
+
* 整个 namespace 没有分块根行时一次都不发。
|
|
30
|
+
*
|
|
31
|
+
* @param {Array<{ namespace: string, key: string, value: string, updated_at: number }>} rows
|
|
32
|
+
* 用户 namespace 的存储行(getClientState 返回值)。
|
|
33
|
+
* @param {() => Promise<Array<{ key: string, value: string, updated_at: number }>>} fetchChunkRows
|
|
34
|
+
* 取该 namespace 对应保留 namespace 的全部切片行(最多调用一次)。
|
|
35
|
+
* @param {(value: string) => Promise<string>} decryptValue
|
|
36
|
+
* @returns {Promise<Array<{ namespace: string, key: string, value: string, updatedAt: number }>>}
|
|
37
|
+
*/
|
|
38
|
+
export function resolveClientStateEntries(rows: Array<{
|
|
39
|
+
namespace: string;
|
|
40
|
+
key: string;
|
|
41
|
+
value: string;
|
|
42
|
+
updated_at: number;
|
|
43
|
+
}>, fetchChunkRows: () => Promise<Array<{
|
|
44
|
+
key: string;
|
|
45
|
+
value: string;
|
|
46
|
+
updated_at: number;
|
|
47
|
+
}>>, decryptValue: (value: string) => Promise<string>): Promise<Array<{
|
|
48
|
+
namespace: string;
|
|
49
|
+
key: string;
|
|
50
|
+
value: string;
|
|
51
|
+
updatedAt: number;
|
|
52
|
+
}>>;
|
|
53
|
+
export const STATE_CHUNK_SLICE_BYTES: number;
|
|
54
|
+
export const DEFAULT_MAX_STATE_VALUE_BYTES: number;
|
|
55
|
+
export const INTERNAL_STATE_CHAR_RE: RegExp;
|
|
@@ -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
|
+
};
|