@rei-standard/amsg-server 2.6.0-next.19 → 2.6.0-next.20
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/dist/adapters/d1.d.ts +32 -0
- package/dist/{chunk-PRQHHRAK.mjs → chunk-HTGYGGUH.mjs} +127 -42
- package/dist/{chunk-XVJ7ZLZX.cjs → chunk-WOXSQBAM.cjs} +127 -42
- package/dist/cloudflare.cjs +2 -2
- package/dist/cloudflare.mjs +1 -1
- package/dist/index.cjs +25 -25
- package/dist/index.mjs +1 -1
- package/dist/lib/message-processor.d.ts +4 -1
- package/dist/lib/task-projection.d.ts +6 -1
- package/package.json +1 -1
package/dist/adapters/d1.d.ts
CHANGED
|
@@ -17,6 +17,38 @@ export class D1Adapter {
|
|
|
17
17
|
private _now;
|
|
18
18
|
/** @private */
|
|
19
19
|
private _iso;
|
|
20
|
+
/**
|
|
21
|
+
* 组一组带 `IN (...)` 的语句:值多到一条塞不下时,按 D1 的绑定参数上限切成
|
|
22
|
+
* 几条(见 chunkForBoundParams)。
|
|
23
|
+
*
|
|
24
|
+
* 只有拿得到事务的绑定才切。没有 batch() 的绑定(测试 shim、自定义适配器,
|
|
25
|
+
* 都不是真实 D1)原样发一条不切的语句:切批是为 D1 那条上限服务的,这类绑
|
|
26
|
+
* 定不受它约束,切了反倒会因为没有事务而多出一个「写一半」的中间态。
|
|
27
|
+
*
|
|
28
|
+
* @private
|
|
29
|
+
* @param {(placeholders: string) => string} buildSql - 拿占位符串(`?, ?, ?`)组 SQL
|
|
30
|
+
* @param {unknown[]} leadingParams - IN 列表之前的固定参数,按出现顺序
|
|
31
|
+
* @param {unknown[]} values - IN 列表里的全部值
|
|
32
|
+
* @returns {any[]} 已经 bind 好的语句
|
|
33
|
+
*/
|
|
34
|
+
private _prepareInClauseStatements;
|
|
35
|
+
/**
|
|
36
|
+
* 带 `IN (...)` 的批量写,切成几条也仍然是一次原子操作。
|
|
37
|
+
*
|
|
38
|
+
* 一条语句本来天然原子,拆开之后这份保证得自己补回来,否则就会多出「只删掉
|
|
39
|
+
* 前 99 个」「只 ack 了前 98 条」这类比原问题更难查的中间态。D1 的 batch()
|
|
40
|
+
* 是隐式事务——其中一条失败,整批回滚——正好接住这件事。
|
|
41
|
+
*
|
|
42
|
+
* 一条就够时照旧单发,跟切批以前走的是同一条路,常见调用(一次三五个 id)
|
|
43
|
+
* 的行为一个字节都没变。
|
|
44
|
+
*
|
|
45
|
+
* @private
|
|
46
|
+
* @param {(placeholders: string) => string} buildSql
|
|
47
|
+
* @param {unknown[]} leadingParams
|
|
48
|
+
* @param {unknown[]} values
|
|
49
|
+
* @returns {Promise<number>} 各批影响行数之和
|
|
50
|
+
*/
|
|
51
|
+
private _runInClauseWrite;
|
|
20
52
|
initSchema(): Promise<{
|
|
21
53
|
columnsCreated: number;
|
|
22
54
|
indexesCreated: number;
|
|
@@ -931,6 +931,9 @@ function projectTask(row, decryptedPayload, options = {}) {
|
|
|
931
931
|
// reason 'stale' 表示错过触发时刻太久被判定不再补发;其余是投递失败的
|
|
932
932
|
// 错误信息。payload 里没有时退回行上的 last_error 列(同形状的脱敏摘要,
|
|
933
933
|
// 等重试期间的失败也记在那里)。没有记录 → null。
|
|
934
|
+
// 记进去的字段整份带出来,不再挑一遍——`pushStatus`(410 = 订阅已注销,
|
|
935
|
+
// 404 = 端点不存在)这类机读标注就是给客户端看的,白名单挡在这一层的话,
|
|
936
|
+
// 客户端只能回去正则匹配 reason 那句人话。
|
|
934
937
|
lastError: payload.lastError ?? parseRowLastError(row.last_error)
|
|
935
938
|
};
|
|
936
939
|
}
|
|
@@ -1784,6 +1787,7 @@ async function processSingleMessage(task, ctx, providedMasterKey, predecrypted =
|
|
|
1784
1787
|
messagesSent: 0,
|
|
1785
1788
|
error: error.message,
|
|
1786
1789
|
errorCode: error.code || null,
|
|
1790
|
+
pushStatusCode: Number.isInteger(error.statusCode) ? error.statusCode : null,
|
|
1787
1791
|
permanent: isNonRetryableError(error)
|
|
1788
1792
|
};
|
|
1789
1793
|
}
|
|
@@ -2116,6 +2120,10 @@ var CLAIM_LEASE_MARGIN_MS = 2 * 60 * 1e3;
|
|
|
2116
2120
|
var DEFAULT_LEASE_HEARTBEAT_MS = 30 * 1e3;
|
|
2117
2121
|
var DEFAULT_HEARTBEAT_LEASE_TTL_MS = 90 * 1e3;
|
|
2118
2122
|
var STALE_AFTER_MS = 60 * 60 * 1e3;
|
|
2123
|
+
var TERMINAL_PUSH_STATUSES = /* @__PURE__ */ new Set([404, 410]);
|
|
2124
|
+
function toPushStatus(value) {
|
|
2125
|
+
return Number.isInteger(value) ? value : null;
|
|
2126
|
+
}
|
|
2119
2127
|
function resolveStaleAfterMs(ctx) {
|
|
2120
2128
|
return positiveNumber(ctx.staleAfterMs) || STALE_AFTER_MS;
|
|
2121
2129
|
}
|
|
@@ -2298,27 +2306,30 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2298
2306
|
console.warn("[amsg-server] onStaleSkip hook \u629B\u9519\uFF08\u5DF2\u5FFD\u7565\uFF09:", hookError && hookError.message);
|
|
2299
2307
|
}
|
|
2300
2308
|
}
|
|
2301
|
-
async function handleDeliveryFailure(task, reason, recurrenceType, decryptedPayload, userKey,
|
|
2309
|
+
async function handleDeliveryFailure(task, reason, recurrenceType, decryptedPayload, userKey, failure = {}) {
|
|
2302
2310
|
results.failedCount++;
|
|
2311
|
+
const errorCode = failure.errorCode ?? null;
|
|
2312
|
+
const pushStatus = toPushStatus(failure.pushStatus);
|
|
2303
2313
|
const tzId = decryptedPayload ? decryptedPayload.tzId ?? null : null;
|
|
2304
|
-
const permanent =
|
|
2314
|
+
const permanent = failure.permanent === true || errorCode === "PUSH_SUBSCRIPTION_MISSING" || errorCode === "PUSH_SUBSCRIPTION_STORE_UNSUPPORTED" || TERMINAL_PUSH_STATUSES.has(pushStatus);
|
|
2315
|
+
const errorExtra = pushStatus === null ? void 0 : { pushStatus };
|
|
2305
2316
|
try {
|
|
2306
2317
|
if (permanent || task.retry_count >= 3) {
|
|
2307
|
-
const encrypted = await encryptPayloadWithLastError(task, decryptedPayload, userKey, reason);
|
|
2318
|
+
const encrypted = await encryptPayloadWithLastError(task, decryptedPayload, userKey, reason, errorExtra);
|
|
2308
2319
|
if (isRecurringType(recurrenceType)) {
|
|
2309
2320
|
const nextSendAt = nextFutureOccurrence(Date.parse(task.next_send_at), recurrenceType, Date.now(), tzId);
|
|
2310
2321
|
await updateAndRelease(task.id, {
|
|
2311
2322
|
next_send_at: nextSendAt,
|
|
2312
2323
|
retry_count: 0,
|
|
2313
2324
|
...encrypted ? { encrypted_payload: encrypted } : {},
|
|
2314
|
-
...supportsLastError ? { last_error: lastErrorJson(task, reason) } : {}
|
|
2325
|
+
...supportsLastError ? { last_error: lastErrorJson(task, reason, errorExtra) } : {}
|
|
2315
2326
|
});
|
|
2316
2327
|
results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "occurrence_skipped", nextSendAt, ...permanent ? { permanent: true } : {} });
|
|
2317
2328
|
} else {
|
|
2318
2329
|
await updateAndRelease(task.id, {
|
|
2319
2330
|
status: "failed",
|
|
2320
2331
|
...encrypted ? { encrypted_payload: encrypted } : {},
|
|
2321
|
-
...supportsLastError ? { last_error: lastErrorJson(task, reason) } : {}
|
|
2332
|
+
...supportsLastError ? { last_error: lastErrorJson(task, reason, errorExtra) } : {}
|
|
2322
2333
|
});
|
|
2323
2334
|
results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "permanently_failed", ...permanent ? { permanent: true } : {} });
|
|
2324
2335
|
}
|
|
@@ -2329,7 +2340,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2329
2340
|
retry_after: nextRetryTime.toISOString(),
|
|
2330
2341
|
lease_until: null,
|
|
2331
2342
|
retry_count: task.retry_count + 1,
|
|
2332
|
-
...supportsLastError ? { last_error: lastErrorJson(task, reason) } : {}
|
|
2343
|
+
...supportsLastError ? { last_error: lastErrorJson(task, reason, errorExtra) } : {}
|
|
2333
2344
|
});
|
|
2334
2345
|
} else {
|
|
2335
2346
|
await db.updateTaskById(task.id, { next_send_at: nextRetryTime.toISOString(), retry_count: task.retry_count + 1 });
|
|
@@ -2474,8 +2485,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2474
2485
|
recurrenceType,
|
|
2475
2486
|
decryptedPayload,
|
|
2476
2487
|
userKey,
|
|
2477
|
-
error.code || null,
|
|
2478
|
-
error.permanent === true
|
|
2488
|
+
{ errorCode: error.code || null, permanent: error.permanent === true, pushStatus: error.statusCode }
|
|
2479
2489
|
);
|
|
2480
2490
|
return;
|
|
2481
2491
|
}
|
|
@@ -2486,8 +2496,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2486
2496
|
recurrenceType,
|
|
2487
2497
|
decryptedPayload,
|
|
2488
2498
|
userKey,
|
|
2489
|
-
sendResult.errorCode || null,
|
|
2490
|
-
sendResult.permanent === true
|
|
2499
|
+
{ errorCode: sendResult.errorCode || null, permanent: sendResult.permanent === true, pushStatus: sendResult.pushStatusCode }
|
|
2491
2500
|
);
|
|
2492
2501
|
return;
|
|
2493
2502
|
}
|
|
@@ -3286,8 +3295,35 @@ var SQLITE_REQUIRED_SCHEMA = Object.freeze({
|
|
|
3286
3295
|
});
|
|
3287
3296
|
|
|
3288
3297
|
// src/server/adapters/d1.js
|
|
3289
|
-
function
|
|
3290
|
-
|
|
3298
|
+
function prefixRangeEnd(prefix) {
|
|
3299
|
+
const points = Array.from(prefix);
|
|
3300
|
+
while (points.length > 0) {
|
|
3301
|
+
const cp = points[points.length - 1].codePointAt(0);
|
|
3302
|
+
if (cp === 55295) {
|
|
3303
|
+
points[points.length - 1] = String.fromCodePoint(57344);
|
|
3304
|
+
return points.join("");
|
|
3305
|
+
}
|
|
3306
|
+
if (cp < 1114111) {
|
|
3307
|
+
points[points.length - 1] = String.fromCodePoint(cp + 1);
|
|
3308
|
+
return points.join("");
|
|
3309
|
+
}
|
|
3310
|
+
points.pop();
|
|
3311
|
+
}
|
|
3312
|
+
return "";
|
|
3313
|
+
}
|
|
3314
|
+
var D1_MAX_BOUND_PARAMS = 100;
|
|
3315
|
+
function chunkForBoundParams(values, fixedParams) {
|
|
3316
|
+
const perStatement = D1_MAX_BOUND_PARAMS - fixedParams;
|
|
3317
|
+
if (perStatement < 1) {
|
|
3318
|
+
throw new Error(
|
|
3319
|
+
`[amsg-server D1] \u56FA\u5B9A\u53C2\u6570\u5DF2\u5360\u6EE1 ${D1_MAX_BOUND_PARAMS} \u4E2A\u7ED1\u5B9A\u4F4D\uFF0CIN \u5217\u8868\u653E\u4E0D\u4E0B`
|
|
3320
|
+
);
|
|
3321
|
+
}
|
|
3322
|
+
const batches = [];
|
|
3323
|
+
for (let i = 0; i < values.length; i += perStatement) {
|
|
3324
|
+
batches.push(values.slice(i, i + perStatement));
|
|
3325
|
+
}
|
|
3326
|
+
return batches;
|
|
3291
3327
|
}
|
|
3292
3328
|
var INTERNAL_TABLE_PREFIXES = ["sqlite_", "_cf_"];
|
|
3293
3329
|
function isInternalTableName(name) {
|
|
@@ -3310,6 +3346,50 @@ var D1Adapter = class {
|
|
|
3310
3346
|
}
|
|
3311
3347
|
return d.toISOString();
|
|
3312
3348
|
}
|
|
3349
|
+
/**
|
|
3350
|
+
* 组一组带 `IN (...)` 的语句:值多到一条塞不下时,按 D1 的绑定参数上限切成
|
|
3351
|
+
* 几条(见 chunkForBoundParams)。
|
|
3352
|
+
*
|
|
3353
|
+
* 只有拿得到事务的绑定才切。没有 batch() 的绑定(测试 shim、自定义适配器,
|
|
3354
|
+
* 都不是真实 D1)原样发一条不切的语句:切批是为 D1 那条上限服务的,这类绑
|
|
3355
|
+
* 定不受它约束,切了反倒会因为没有事务而多出一个「写一半」的中间态。
|
|
3356
|
+
*
|
|
3357
|
+
* @private
|
|
3358
|
+
* @param {(placeholders: string) => string} buildSql - 拿占位符串(`?, ?, ?`)组 SQL
|
|
3359
|
+
* @param {unknown[]} leadingParams - IN 列表之前的固定参数,按出现顺序
|
|
3360
|
+
* @param {unknown[]} values - IN 列表里的全部值
|
|
3361
|
+
* @returns {any[]} 已经 bind 好的语句
|
|
3362
|
+
*/
|
|
3363
|
+
_prepareInClauseStatements(buildSql, leadingParams, values) {
|
|
3364
|
+
const batches = typeof this._db.batch === "function" ? chunkForBoundParams(values, leadingParams.length) : [values];
|
|
3365
|
+
return batches.map((batch) => this._db.prepare(buildSql(batch.map(() => "?").join(", "))).bind(...leadingParams, ...batch));
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* 带 `IN (...)` 的批量写,切成几条也仍然是一次原子操作。
|
|
3369
|
+
*
|
|
3370
|
+
* 一条语句本来天然原子,拆开之后这份保证得自己补回来,否则就会多出「只删掉
|
|
3371
|
+
* 前 99 个」「只 ack 了前 98 条」这类比原问题更难查的中间态。D1 的 batch()
|
|
3372
|
+
* 是隐式事务——其中一条失败,整批回滚——正好接住这件事。
|
|
3373
|
+
*
|
|
3374
|
+
* 一条就够时照旧单发,跟切批以前走的是同一条路,常见调用(一次三五个 id)
|
|
3375
|
+
* 的行为一个字节都没变。
|
|
3376
|
+
*
|
|
3377
|
+
* @private
|
|
3378
|
+
* @param {(placeholders: string) => string} buildSql
|
|
3379
|
+
* @param {unknown[]} leadingParams
|
|
3380
|
+
* @param {unknown[]} values
|
|
3381
|
+
* @returns {Promise<number>} 各批影响行数之和
|
|
3382
|
+
*/
|
|
3383
|
+
async _runInClauseWrite(buildSql, leadingParams, values) {
|
|
3384
|
+
const statements = this._prepareInClauseStatements(buildSql, leadingParams, values);
|
|
3385
|
+
if (statements.length === 0) return 0;
|
|
3386
|
+
if (statements.length === 1) {
|
|
3387
|
+
const res = await statements[0].run();
|
|
3388
|
+
return res.meta.changes || 0;
|
|
3389
|
+
}
|
|
3390
|
+
const results = await this._db.batch(statements);
|
|
3391
|
+
return results.reduce((n, res) => n + (res.meta.changes || 0), 0);
|
|
3392
|
+
}
|
|
3313
3393
|
async initSchema() {
|
|
3314
3394
|
await this._db.prepare(SQLITE_TABLE_SQL).run();
|
|
3315
3395
|
await this._db.prepare(CLIENT_STATE_TABLE_SQL).run();
|
|
@@ -3693,11 +3773,11 @@ var D1Adapter = class {
|
|
|
3693
3773
|
updated_at = excluded.updated_at
|
|
3694
3774
|
WHERE excluded.updated_at >= client_state.updated_at`;
|
|
3695
3775
|
const CLEANUP_PREFIX_SQL = `DELETE FROM client_state
|
|
3696
|
-
WHERE user_id = ? AND namespace = ? AND key
|
|
3776
|
+
WHERE user_id = ? AND namespace = ? AND key >= ? AND key < ? AND updated_at <= ?`;
|
|
3697
3777
|
const CLEANUP_KEY_SQL = `DELETE FROM client_state
|
|
3698
3778
|
WHERE user_id = ? AND namespace = ? AND key = ? AND updated_at <= ?`;
|
|
3699
3779
|
const buildStatements = () => [
|
|
3700
|
-
...cleanups.map((c) => typeof c.key === "string" ? this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt) : this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace,
|
|
3780
|
+
...cleanups.map((c) => typeof c.key === "string" ? this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt) : this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace, c.keyPrefix, prefixRangeEnd(c.keyPrefix), c.updatedAt)),
|
|
3701
3781
|
...entries.map(
|
|
3702
3782
|
(entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt)
|
|
3703
3783
|
)
|
|
@@ -3826,13 +3906,19 @@ var D1Adapter = class {
|
|
|
3826
3906
|
*/
|
|
3827
3907
|
async getLlmCredentials(userId, credIds) {
|
|
3828
3908
|
if (!credIds || credIds.length === 0) return [];
|
|
3829
|
-
const
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3909
|
+
const statements = this._prepareInClauseStatements(
|
|
3910
|
+
(placeholders) => `SELECT cred_id, encrypted_value, updated_at
|
|
3911
|
+
FROM llm_credentials
|
|
3912
|
+
WHERE user_id = ? AND cred_id IN (${placeholders})`,
|
|
3913
|
+
[userId],
|
|
3914
|
+
credIds
|
|
3915
|
+
);
|
|
3916
|
+
const rows = [];
|
|
3917
|
+
for (const stmt of statements) {
|
|
3918
|
+
const res = await stmt.all();
|
|
3919
|
+
rows.push(...res.results || []);
|
|
3920
|
+
}
|
|
3921
|
+
return rows;
|
|
3836
3922
|
}
|
|
3837
3923
|
/**
|
|
3838
3924
|
* 这个用户名下所有凭据的对账清单(只有 cred_id 和 updated_at,密文不出
|
|
@@ -3858,18 +3944,17 @@ var D1Adapter = class {
|
|
|
3858
3944
|
*/
|
|
3859
3945
|
async deleteLlmCredentials(userId, credIds = null) {
|
|
3860
3946
|
if (credIds !== null && credIds.length === 0) return 0;
|
|
3861
|
-
let res;
|
|
3862
3947
|
if (credIds === null) {
|
|
3863
|
-
res = await this._db.prepare(
|
|
3948
|
+
const res = await this._db.prepare(
|
|
3864
3949
|
"DELETE FROM llm_credentials WHERE user_id = ?"
|
|
3865
3950
|
).bind(userId).run();
|
|
3866
|
-
|
|
3867
|
-
const placeholders = credIds.map(() => "?").join(", ");
|
|
3868
|
-
res = await this._db.prepare(
|
|
3869
|
-
`DELETE FROM llm_credentials WHERE user_id = ? AND cred_id IN (${placeholders})`
|
|
3870
|
-
).bind(userId, ...credIds).run();
|
|
3951
|
+
return res.meta.changes || 0;
|
|
3871
3952
|
}
|
|
3872
|
-
return
|
|
3953
|
+
return this._runInClauseWrite(
|
|
3954
|
+
(placeholders) => `DELETE FROM llm_credentials WHERE user_id = ? AND cred_id IN (${placeholders})`,
|
|
3955
|
+
[userId],
|
|
3956
|
+
credIds
|
|
3957
|
+
);
|
|
3873
3958
|
}
|
|
3874
3959
|
// ── message_outbox(服务端消息收件箱,客户端 ack)────────────────────────
|
|
3875
3960
|
/**
|
|
@@ -3925,12 +4010,12 @@ var D1Adapter = class {
|
|
|
3925
4010
|
*/
|
|
3926
4011
|
async markOutboxDelivered(userId, messageIds, deliveredAt) {
|
|
3927
4012
|
if (!messageIds || messageIds.length === 0) return 0;
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
4013
|
+
return this._runInClauseWrite(
|
|
4014
|
+
(placeholders) => `UPDATE message_outbox SET delivered_at = ?
|
|
4015
|
+
WHERE user_id = ? AND message_id IN (${placeholders})`,
|
|
4016
|
+
[deliveredAt, userId],
|
|
4017
|
+
messageIds
|
|
4018
|
+
);
|
|
3934
4019
|
}
|
|
3935
4020
|
/**
|
|
3936
4021
|
* 未 ack 的行(id 升序,游标翻页)。payload 仍是密文,解密在 handler。
|
|
@@ -3962,12 +4047,12 @@ var D1Adapter = class {
|
|
|
3962
4047
|
*/
|
|
3963
4048
|
async ackOutboxMessages(userId, messageIds, ackedAt) {
|
|
3964
4049
|
if (!messageIds || messageIds.length === 0) return 0;
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
4050
|
+
return this._runInClauseWrite(
|
|
4051
|
+
(placeholders) => `UPDATE message_outbox SET acked_at = ?
|
|
4052
|
+
WHERE user_id = ? AND acked_at IS NULL AND message_id IN (${placeholders})`,
|
|
4053
|
+
[ackedAt, userId],
|
|
4054
|
+
messageIds
|
|
4055
|
+
);
|
|
3971
4056
|
}
|
|
3972
4057
|
/**
|
|
3973
4058
|
* outbox 的例行清理(run-tick 每跳顺手调):已 ack 的行留短一些,未 ack 的
|
|
@@ -4249,7 +4334,7 @@ function createClientStateHandler(ctx) {
|
|
|
4249
4334
|
}
|
|
4250
4335
|
|
|
4251
4336
|
// src/server/lib/version.js
|
|
4252
|
-
var SERVER_VERSION = true ? "2.6.0-next.
|
|
4337
|
+
var SERVER_VERSION = true ? "2.6.0-next.20" : "0.0.0-dev";
|
|
4253
4338
|
|
|
4254
4339
|
// src/server/handlers/capabilities.js
|
|
4255
4340
|
var SERVER_FEATURES = Object.freeze([
|
|
@@ -931,6 +931,9 @@ function projectTask(row, decryptedPayload, options = {}) {
|
|
|
931
931
|
// reason 'stale' 表示错过触发时刻太久被判定不再补发;其余是投递失败的
|
|
932
932
|
// 错误信息。payload 里没有时退回行上的 last_error 列(同形状的脱敏摘要,
|
|
933
933
|
// 等重试期间的失败也记在那里)。没有记录 → null。
|
|
934
|
+
// 记进去的字段整份带出来,不再挑一遍——`pushStatus`(410 = 订阅已注销,
|
|
935
|
+
// 404 = 端点不存在)这类机读标注就是给客户端看的,白名单挡在这一层的话,
|
|
936
|
+
// 客户端只能回去正则匹配 reason 那句人话。
|
|
934
937
|
lastError: _nullishCoalesce(payload.lastError, () => ( parseRowLastError(row.last_error)))
|
|
935
938
|
};
|
|
936
939
|
}
|
|
@@ -1784,6 +1787,7 @@ async function processSingleMessage(task, ctx, providedMasterKey, predecrypted =
|
|
|
1784
1787
|
messagesSent: 0,
|
|
1785
1788
|
error: error.message,
|
|
1786
1789
|
errorCode: error.code || null,
|
|
1790
|
+
pushStatusCode: Number.isInteger(error.statusCode) ? error.statusCode : null,
|
|
1787
1791
|
permanent: isNonRetryableError(error)
|
|
1788
1792
|
};
|
|
1789
1793
|
}
|
|
@@ -2116,6 +2120,10 @@ var CLAIM_LEASE_MARGIN_MS = 2 * 60 * 1e3;
|
|
|
2116
2120
|
var DEFAULT_LEASE_HEARTBEAT_MS = 30 * 1e3;
|
|
2117
2121
|
var DEFAULT_HEARTBEAT_LEASE_TTL_MS = 90 * 1e3;
|
|
2118
2122
|
var STALE_AFTER_MS = 60 * 60 * 1e3;
|
|
2123
|
+
var TERMINAL_PUSH_STATUSES = /* @__PURE__ */ new Set([404, 410]);
|
|
2124
|
+
function toPushStatus(value) {
|
|
2125
|
+
return Number.isInteger(value) ? value : null;
|
|
2126
|
+
}
|
|
2119
2127
|
function resolveStaleAfterMs(ctx) {
|
|
2120
2128
|
return positiveNumber(ctx.staleAfterMs) || STALE_AFTER_MS;
|
|
2121
2129
|
}
|
|
@@ -2298,27 +2306,30 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2298
2306
|
console.warn("[amsg-server] onStaleSkip hook \u629B\u9519\uFF08\u5DF2\u5FFD\u7565\uFF09:", hookError && hookError.message);
|
|
2299
2307
|
}
|
|
2300
2308
|
}
|
|
2301
|
-
async function handleDeliveryFailure(task, reason, recurrenceType, decryptedPayload, userKey,
|
|
2309
|
+
async function handleDeliveryFailure(task, reason, recurrenceType, decryptedPayload, userKey, failure = {}) {
|
|
2302
2310
|
results.failedCount++;
|
|
2311
|
+
const errorCode = _nullishCoalesce(failure.errorCode, () => ( null));
|
|
2312
|
+
const pushStatus = toPushStatus(failure.pushStatus);
|
|
2303
2313
|
const tzId = decryptedPayload ? _nullishCoalesce(decryptedPayload.tzId, () => ( null)) : null;
|
|
2304
|
-
const permanent =
|
|
2314
|
+
const permanent = failure.permanent === true || errorCode === "PUSH_SUBSCRIPTION_MISSING" || errorCode === "PUSH_SUBSCRIPTION_STORE_UNSUPPORTED" || TERMINAL_PUSH_STATUSES.has(pushStatus);
|
|
2315
|
+
const errorExtra = pushStatus === null ? void 0 : { pushStatus };
|
|
2305
2316
|
try {
|
|
2306
2317
|
if (permanent || task.retry_count >= 3) {
|
|
2307
|
-
const encrypted = await encryptPayloadWithLastError(task, decryptedPayload, userKey, reason);
|
|
2318
|
+
const encrypted = await encryptPayloadWithLastError(task, decryptedPayload, userKey, reason, errorExtra);
|
|
2308
2319
|
if (isRecurringType(recurrenceType)) {
|
|
2309
2320
|
const nextSendAt = nextFutureOccurrence(Date.parse(task.next_send_at), recurrenceType, Date.now(), tzId);
|
|
2310
2321
|
await updateAndRelease(task.id, {
|
|
2311
2322
|
next_send_at: nextSendAt,
|
|
2312
2323
|
retry_count: 0,
|
|
2313
2324
|
...encrypted ? { encrypted_payload: encrypted } : {},
|
|
2314
|
-
...supportsLastError ? { last_error: lastErrorJson(task, reason) } : {}
|
|
2325
|
+
...supportsLastError ? { last_error: lastErrorJson(task, reason, errorExtra) } : {}
|
|
2315
2326
|
});
|
|
2316
2327
|
results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "occurrence_skipped", nextSendAt, ...permanent ? { permanent: true } : {} });
|
|
2317
2328
|
} else {
|
|
2318
2329
|
await updateAndRelease(task.id, {
|
|
2319
2330
|
status: "failed",
|
|
2320
2331
|
...encrypted ? { encrypted_payload: encrypted } : {},
|
|
2321
|
-
...supportsLastError ? { last_error: lastErrorJson(task, reason) } : {}
|
|
2332
|
+
...supportsLastError ? { last_error: lastErrorJson(task, reason, errorExtra) } : {}
|
|
2322
2333
|
});
|
|
2323
2334
|
results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "permanently_failed", ...permanent ? { permanent: true } : {} });
|
|
2324
2335
|
}
|
|
@@ -2329,7 +2340,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2329
2340
|
retry_after: nextRetryTime.toISOString(),
|
|
2330
2341
|
lease_until: null,
|
|
2331
2342
|
retry_count: task.retry_count + 1,
|
|
2332
|
-
...supportsLastError ? { last_error: lastErrorJson(task, reason) } : {}
|
|
2343
|
+
...supportsLastError ? { last_error: lastErrorJson(task, reason, errorExtra) } : {}
|
|
2333
2344
|
});
|
|
2334
2345
|
} else {
|
|
2335
2346
|
await db.updateTaskById(task.id, { next_send_at: nextRetryTime.toISOString(), retry_count: task.retry_count + 1 });
|
|
@@ -2474,8 +2485,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2474
2485
|
recurrenceType,
|
|
2475
2486
|
decryptedPayload,
|
|
2476
2487
|
userKey,
|
|
2477
|
-
error.code || null,
|
|
2478
|
-
error.permanent === true
|
|
2488
|
+
{ errorCode: error.code || null, permanent: error.permanent === true, pushStatus: error.statusCode }
|
|
2479
2489
|
);
|
|
2480
2490
|
return;
|
|
2481
2491
|
}
|
|
@@ -2486,8 +2496,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2486
2496
|
recurrenceType,
|
|
2487
2497
|
decryptedPayload,
|
|
2488
2498
|
userKey,
|
|
2489
|
-
sendResult.errorCode || null,
|
|
2490
|
-
sendResult.permanent === true
|
|
2499
|
+
{ errorCode: sendResult.errorCode || null, permanent: sendResult.permanent === true, pushStatus: sendResult.pushStatusCode }
|
|
2491
2500
|
);
|
|
2492
2501
|
return;
|
|
2493
2502
|
}
|
|
@@ -3286,8 +3295,35 @@ var SQLITE_REQUIRED_SCHEMA = Object.freeze({
|
|
|
3286
3295
|
});
|
|
3287
3296
|
|
|
3288
3297
|
// src/server/adapters/d1.js
|
|
3289
|
-
function
|
|
3290
|
-
|
|
3298
|
+
function prefixRangeEnd(prefix) {
|
|
3299
|
+
const points = Array.from(prefix);
|
|
3300
|
+
while (points.length > 0) {
|
|
3301
|
+
const cp = points[points.length - 1].codePointAt(0);
|
|
3302
|
+
if (cp === 55295) {
|
|
3303
|
+
points[points.length - 1] = String.fromCodePoint(57344);
|
|
3304
|
+
return points.join("");
|
|
3305
|
+
}
|
|
3306
|
+
if (cp < 1114111) {
|
|
3307
|
+
points[points.length - 1] = String.fromCodePoint(cp + 1);
|
|
3308
|
+
return points.join("");
|
|
3309
|
+
}
|
|
3310
|
+
points.pop();
|
|
3311
|
+
}
|
|
3312
|
+
return "";
|
|
3313
|
+
}
|
|
3314
|
+
var D1_MAX_BOUND_PARAMS = 100;
|
|
3315
|
+
function chunkForBoundParams(values, fixedParams) {
|
|
3316
|
+
const perStatement = D1_MAX_BOUND_PARAMS - fixedParams;
|
|
3317
|
+
if (perStatement < 1) {
|
|
3318
|
+
throw new Error(
|
|
3319
|
+
`[amsg-server D1] \u56FA\u5B9A\u53C2\u6570\u5DF2\u5360\u6EE1 ${D1_MAX_BOUND_PARAMS} \u4E2A\u7ED1\u5B9A\u4F4D\uFF0CIN \u5217\u8868\u653E\u4E0D\u4E0B`
|
|
3320
|
+
);
|
|
3321
|
+
}
|
|
3322
|
+
const batches = [];
|
|
3323
|
+
for (let i = 0; i < values.length; i += perStatement) {
|
|
3324
|
+
batches.push(values.slice(i, i + perStatement));
|
|
3325
|
+
}
|
|
3326
|
+
return batches;
|
|
3291
3327
|
}
|
|
3292
3328
|
var INTERNAL_TABLE_PREFIXES = ["sqlite_", "_cf_"];
|
|
3293
3329
|
function isInternalTableName(name) {
|
|
@@ -3310,6 +3346,50 @@ var D1Adapter = class {
|
|
|
3310
3346
|
}
|
|
3311
3347
|
return d.toISOString();
|
|
3312
3348
|
}
|
|
3349
|
+
/**
|
|
3350
|
+
* 组一组带 `IN (...)` 的语句:值多到一条塞不下时,按 D1 的绑定参数上限切成
|
|
3351
|
+
* 几条(见 chunkForBoundParams)。
|
|
3352
|
+
*
|
|
3353
|
+
* 只有拿得到事务的绑定才切。没有 batch() 的绑定(测试 shim、自定义适配器,
|
|
3354
|
+
* 都不是真实 D1)原样发一条不切的语句:切批是为 D1 那条上限服务的,这类绑
|
|
3355
|
+
* 定不受它约束,切了反倒会因为没有事务而多出一个「写一半」的中间态。
|
|
3356
|
+
*
|
|
3357
|
+
* @private
|
|
3358
|
+
* @param {(placeholders: string) => string} buildSql - 拿占位符串(`?, ?, ?`)组 SQL
|
|
3359
|
+
* @param {unknown[]} leadingParams - IN 列表之前的固定参数,按出现顺序
|
|
3360
|
+
* @param {unknown[]} values - IN 列表里的全部值
|
|
3361
|
+
* @returns {any[]} 已经 bind 好的语句
|
|
3362
|
+
*/
|
|
3363
|
+
_prepareInClauseStatements(buildSql, leadingParams, values) {
|
|
3364
|
+
const batches = typeof this._db.batch === "function" ? chunkForBoundParams(values, leadingParams.length) : [values];
|
|
3365
|
+
return batches.map((batch) => this._db.prepare(buildSql(batch.map(() => "?").join(", "))).bind(...leadingParams, ...batch));
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* 带 `IN (...)` 的批量写,切成几条也仍然是一次原子操作。
|
|
3369
|
+
*
|
|
3370
|
+
* 一条语句本来天然原子,拆开之后这份保证得自己补回来,否则就会多出「只删掉
|
|
3371
|
+
* 前 99 个」「只 ack 了前 98 条」这类比原问题更难查的中间态。D1 的 batch()
|
|
3372
|
+
* 是隐式事务——其中一条失败,整批回滚——正好接住这件事。
|
|
3373
|
+
*
|
|
3374
|
+
* 一条就够时照旧单发,跟切批以前走的是同一条路,常见调用(一次三五个 id)
|
|
3375
|
+
* 的行为一个字节都没变。
|
|
3376
|
+
*
|
|
3377
|
+
* @private
|
|
3378
|
+
* @param {(placeholders: string) => string} buildSql
|
|
3379
|
+
* @param {unknown[]} leadingParams
|
|
3380
|
+
* @param {unknown[]} values
|
|
3381
|
+
* @returns {Promise<number>} 各批影响行数之和
|
|
3382
|
+
*/
|
|
3383
|
+
async _runInClauseWrite(buildSql, leadingParams, values) {
|
|
3384
|
+
const statements = this._prepareInClauseStatements(buildSql, leadingParams, values);
|
|
3385
|
+
if (statements.length === 0) return 0;
|
|
3386
|
+
if (statements.length === 1) {
|
|
3387
|
+
const res = await statements[0].run();
|
|
3388
|
+
return res.meta.changes || 0;
|
|
3389
|
+
}
|
|
3390
|
+
const results = await this._db.batch(statements);
|
|
3391
|
+
return results.reduce((n, res) => n + (res.meta.changes || 0), 0);
|
|
3392
|
+
}
|
|
3313
3393
|
async initSchema() {
|
|
3314
3394
|
await this._db.prepare(SQLITE_TABLE_SQL).run();
|
|
3315
3395
|
await this._db.prepare(CLIENT_STATE_TABLE_SQL).run();
|
|
@@ -3693,11 +3773,11 @@ var D1Adapter = class {
|
|
|
3693
3773
|
updated_at = excluded.updated_at
|
|
3694
3774
|
WHERE excluded.updated_at >= client_state.updated_at`;
|
|
3695
3775
|
const CLEANUP_PREFIX_SQL = `DELETE FROM client_state
|
|
3696
|
-
WHERE user_id = ? AND namespace = ? AND key
|
|
3776
|
+
WHERE user_id = ? AND namespace = ? AND key >= ? AND key < ? AND updated_at <= ?`;
|
|
3697
3777
|
const CLEANUP_KEY_SQL = `DELETE FROM client_state
|
|
3698
3778
|
WHERE user_id = ? AND namespace = ? AND key = ? AND updated_at <= ?`;
|
|
3699
3779
|
const buildStatements = () => [
|
|
3700
|
-
...cleanups.map((c) => typeof c.key === "string" ? this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt) : this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace,
|
|
3780
|
+
...cleanups.map((c) => typeof c.key === "string" ? this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt) : this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace, c.keyPrefix, prefixRangeEnd(c.keyPrefix), c.updatedAt)),
|
|
3701
3781
|
...entries.map(
|
|
3702
3782
|
(entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt)
|
|
3703
3783
|
)
|
|
@@ -3826,13 +3906,19 @@ var D1Adapter = class {
|
|
|
3826
3906
|
*/
|
|
3827
3907
|
async getLlmCredentials(userId, credIds) {
|
|
3828
3908
|
if (!credIds || credIds.length === 0) return [];
|
|
3829
|
-
const
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3909
|
+
const statements = this._prepareInClauseStatements(
|
|
3910
|
+
(placeholders) => `SELECT cred_id, encrypted_value, updated_at
|
|
3911
|
+
FROM llm_credentials
|
|
3912
|
+
WHERE user_id = ? AND cred_id IN (${placeholders})`,
|
|
3913
|
+
[userId],
|
|
3914
|
+
credIds
|
|
3915
|
+
);
|
|
3916
|
+
const rows = [];
|
|
3917
|
+
for (const stmt of statements) {
|
|
3918
|
+
const res = await stmt.all();
|
|
3919
|
+
rows.push(...res.results || []);
|
|
3920
|
+
}
|
|
3921
|
+
return rows;
|
|
3836
3922
|
}
|
|
3837
3923
|
/**
|
|
3838
3924
|
* 这个用户名下所有凭据的对账清单(只有 cred_id 和 updated_at,密文不出
|
|
@@ -3858,18 +3944,17 @@ var D1Adapter = class {
|
|
|
3858
3944
|
*/
|
|
3859
3945
|
async deleteLlmCredentials(userId, credIds = null) {
|
|
3860
3946
|
if (credIds !== null && credIds.length === 0) return 0;
|
|
3861
|
-
let res;
|
|
3862
3947
|
if (credIds === null) {
|
|
3863
|
-
res = await this._db.prepare(
|
|
3948
|
+
const res = await this._db.prepare(
|
|
3864
3949
|
"DELETE FROM llm_credentials WHERE user_id = ?"
|
|
3865
3950
|
).bind(userId).run();
|
|
3866
|
-
|
|
3867
|
-
const placeholders = credIds.map(() => "?").join(", ");
|
|
3868
|
-
res = await this._db.prepare(
|
|
3869
|
-
`DELETE FROM llm_credentials WHERE user_id = ? AND cred_id IN (${placeholders})`
|
|
3870
|
-
).bind(userId, ...credIds).run();
|
|
3951
|
+
return res.meta.changes || 0;
|
|
3871
3952
|
}
|
|
3872
|
-
return
|
|
3953
|
+
return this._runInClauseWrite(
|
|
3954
|
+
(placeholders) => `DELETE FROM llm_credentials WHERE user_id = ? AND cred_id IN (${placeholders})`,
|
|
3955
|
+
[userId],
|
|
3956
|
+
credIds
|
|
3957
|
+
);
|
|
3873
3958
|
}
|
|
3874
3959
|
// ── message_outbox(服务端消息收件箱,客户端 ack)────────────────────────
|
|
3875
3960
|
/**
|
|
@@ -3925,12 +4010,12 @@ var D1Adapter = class {
|
|
|
3925
4010
|
*/
|
|
3926
4011
|
async markOutboxDelivered(userId, messageIds, deliveredAt) {
|
|
3927
4012
|
if (!messageIds || messageIds.length === 0) return 0;
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
4013
|
+
return this._runInClauseWrite(
|
|
4014
|
+
(placeholders) => `UPDATE message_outbox SET delivered_at = ?
|
|
4015
|
+
WHERE user_id = ? AND message_id IN (${placeholders})`,
|
|
4016
|
+
[deliveredAt, userId],
|
|
4017
|
+
messageIds
|
|
4018
|
+
);
|
|
3934
4019
|
}
|
|
3935
4020
|
/**
|
|
3936
4021
|
* 未 ack 的行(id 升序,游标翻页)。payload 仍是密文,解密在 handler。
|
|
@@ -3962,12 +4047,12 @@ var D1Adapter = class {
|
|
|
3962
4047
|
*/
|
|
3963
4048
|
async ackOutboxMessages(userId, messageIds, ackedAt) {
|
|
3964
4049
|
if (!messageIds || messageIds.length === 0) return 0;
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
4050
|
+
return this._runInClauseWrite(
|
|
4051
|
+
(placeholders) => `UPDATE message_outbox SET acked_at = ?
|
|
4052
|
+
WHERE user_id = ? AND acked_at IS NULL AND message_id IN (${placeholders})`,
|
|
4053
|
+
[ackedAt, userId],
|
|
4054
|
+
messageIds
|
|
4055
|
+
);
|
|
3971
4056
|
}
|
|
3972
4057
|
/**
|
|
3973
4058
|
* outbox 的例行清理(run-tick 每跳顺手调):已 ack 的行留短一些,未 ack 的
|
|
@@ -4249,7 +4334,7 @@ function createClientStateHandler(ctx) {
|
|
|
4249
4334
|
}
|
|
4250
4335
|
|
|
4251
4336
|
// src/server/lib/version.js
|
|
4252
|
-
var SERVER_VERSION = true ? "2.6.0-next.
|
|
4337
|
+
var SERVER_VERSION = true ? "2.6.0-next.20" : "0.0.0-dev";
|
|
4253
4338
|
|
|
4254
4339
|
// src/server/handlers/capabilities.js
|
|
4255
4340
|
var SERVER_FEATURES = Object.freeze([
|
package/dist/cloudflare.cjs
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
var
|
|
27
|
+
var _chunkWOXSQBAMcjs = require('./chunk-WOXSQBAM.cjs');
|
|
28
28
|
require('./chunk-V6NZQ22S.cjs');
|
|
29
29
|
|
|
30
30
|
|
|
@@ -52,4 +52,4 @@ require('./chunk-V6NZQ22S.cjs');
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
exports.MAX_PUSH_PAYLOAD_BYTES =
|
|
55
|
+
exports.MAX_PUSH_PAYLOAD_BYTES = _chunkWOXSQBAMcjs.MAX_PUSH_PAYLOAD_BYTES; exports.NonRetryableError = _chunkWOXSQBAMcjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkWOXSQBAMcjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkWOXSQBAMcjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkWOXSQBAMcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkWOXSQBAMcjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkWOXSQBAMcjs.advanceOccurrence; exports.createD1Adapter = _chunkWOXSQBAMcjs.createD1Adapter; exports.createSingleUserCloudflareWorker = _chunkWOXSQBAMcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkWOXSQBAMcjs.createSingleUserServer; exports.createWebCryptoWebPush = _chunkWOXSQBAMcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkWOXSQBAMcjs.decryptFromStorage; exports.decryptPayload = _chunkWOXSQBAMcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkWOXSQBAMcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkWOXSQBAMcjs.encryptForStorage; exports.ensureSchema = _chunkWOXSQBAMcjs.ensureSchema; exports.getSchemaVersion = _chunkWOXSQBAMcjs.getSchemaVersion; exports.isNonRetryableError = _chunkWOXSQBAMcjs.isNonRetryableError; exports.isValidTimeZoneId = _chunkWOXSQBAMcjs.isValidTimeZoneId; exports.measurePushPayload = _chunkWOXSQBAMcjs.measurePushPayload; exports.nextFutureOccurrence = _chunkWOXSQBAMcjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkWOXSQBAMcjs.planNextOccurrence; exports.runScheduledTick = _chunkWOXSQBAMcjs.runScheduledTick; exports.runTask = _chunkWOXSQBAMcjs.runTask; exports.summarizeErrorCause = _chunkWOXSQBAMcjs.summarizeErrorCause;
|
package/dist/cloudflare.mjs
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
var
|
|
70
|
+
var _chunkWOXSQBAMcjs = require('./chunk-WOXSQBAM.cjs');
|
|
71
71
|
require('./chunk-V6NZQ22S.cjs');
|
|
72
72
|
|
|
73
73
|
// src/server/adapters/factory.js
|
|
@@ -101,7 +101,7 @@ async function createAdapter(config) {
|
|
|
101
101
|
// src/server/handlers/init-tenant.js
|
|
102
102
|
function createInitTenantHandler(ctx) {
|
|
103
103
|
async function POST(headers, body) {
|
|
104
|
-
const initSecret =
|
|
104
|
+
const initSecret = _chunkWOXSQBAMcjs.getHeader.call(void 0, headers, "x-init-secret");
|
|
105
105
|
if (ctx.tenant.initSecret && initSecret !== ctx.tenant.initSecret) {
|
|
106
106
|
return {
|
|
107
107
|
status: 401,
|
|
@@ -114,7 +114,7 @@ function createInitTenantHandler(ctx) {
|
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
-
const parsed =
|
|
117
|
+
const parsed = _chunkWOXSQBAMcjs.parseJsonBody.call(void 0, body);
|
|
118
118
|
if (!parsed.ok) {
|
|
119
119
|
return {
|
|
120
120
|
status: 400,
|
|
@@ -128,7 +128,7 @@ function createInitTenantHandler(ctx) {
|
|
|
128
128
|
const tenantId = payload.tenantId || void 0;
|
|
129
129
|
const driver = payload.driver;
|
|
130
130
|
const databaseUrl = payload.databaseUrl || payload.connectionString;
|
|
131
|
-
if (tenantId && !
|
|
131
|
+
if (tenantId && !_chunkWOXSQBAMcjs.isValidUUIDv4.call(void 0, tenantId)) {
|
|
132
132
|
return {
|
|
133
133
|
status: 400,
|
|
134
134
|
body: {
|
|
@@ -248,7 +248,7 @@ function createSendNotificationsHandler(ctx) {
|
|
|
248
248
|
}
|
|
249
249
|
};
|
|
250
250
|
}
|
|
251
|
-
const data = await
|
|
251
|
+
const data = await _chunkWOXSQBAMcjs.runScheduledTick.call(void 0, { ...ctx, db, masterKey });
|
|
252
252
|
return { status: 200, body: { success: true, data } };
|
|
253
253
|
}
|
|
254
254
|
return { POST };
|
|
@@ -270,16 +270,16 @@ function encryptConfig(config, kekBuffer) {
|
|
|
270
270
|
const plaintext = Buffer.from(JSON.stringify(config), "utf8");
|
|
271
271
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
272
272
|
const authTag = cipher.getAuthTag();
|
|
273
|
-
return `v1.${
|
|
273
|
+
return `v1.${_chunkWOXSQBAMcjs.bytesToBase64Url.call(void 0, iv)}.${_chunkWOXSQBAMcjs.bytesToBase64Url.call(void 0, authTag)}.${_chunkWOXSQBAMcjs.bytesToBase64Url.call(void 0, ciphertext)}`;
|
|
274
274
|
}
|
|
275
275
|
function decryptConfig(encrypted, kekBuffer) {
|
|
276
276
|
const parts = String(encrypted || "").split(".");
|
|
277
277
|
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
278
278
|
throw new Error("INVALID_TENANT_CONFIG");
|
|
279
279
|
}
|
|
280
|
-
const iv =
|
|
281
|
-
const authTag =
|
|
282
|
-
const ciphertext =
|
|
280
|
+
const iv = _chunkWOXSQBAMcjs.base64UrlToBytes.call(void 0, parts[1]);
|
|
281
|
+
const authTag = _chunkWOXSQBAMcjs.base64UrlToBytes.call(void 0, parts[2]);
|
|
282
|
+
const ciphertext = _chunkWOXSQBAMcjs.base64UrlToBytes.call(void 0, parts[3]);
|
|
283
283
|
const decipher = _crypto.createDecipheriv.call(void 0, "aes-256-gcm", kekBuffer, iv);
|
|
284
284
|
decipher.setAuthTag(authTag);
|
|
285
285
|
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
@@ -347,7 +347,7 @@ function createTenantBlobStore(options) {
|
|
|
347
347
|
// src/server/tenant/token.js
|
|
348
348
|
|
|
349
349
|
function sign(input, secret) {
|
|
350
|
-
return
|
|
350
|
+
return _chunkWOXSQBAMcjs.bytesToBase64Url.call(void 0, _crypto.createHmac.call(void 0, "sha256", secret).update(input).digest());
|
|
351
351
|
}
|
|
352
352
|
function nowEpochSeconds() {
|
|
353
353
|
return Math.floor(Date.now() / 1e3);
|
|
@@ -363,8 +363,8 @@ function createTenantToken(params, secret) {
|
|
|
363
363
|
v: 1
|
|
364
364
|
};
|
|
365
365
|
const header = { alg: "HS256", typ: "JWT" };
|
|
366
|
-
const encodedHeader =
|
|
367
|
-
const encodedPayload =
|
|
366
|
+
const encodedHeader = _chunkWOXSQBAMcjs.bytesToBase64Url.call(void 0, _chunkWOXSQBAMcjs.utf8.call(void 0, JSON.stringify(header)));
|
|
367
|
+
const encodedPayload = _chunkWOXSQBAMcjs.bytesToBase64Url.call(void 0, _chunkWOXSQBAMcjs.utf8.call(void 0, JSON.stringify(payload)));
|
|
368
368
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
369
369
|
const signature = sign(signingInput, secret);
|
|
370
370
|
return `${signingInput}.${signature}`;
|
|
@@ -383,17 +383,17 @@ function verifyTenantToken(token, secret, options = {}) {
|
|
|
383
383
|
let receivedBytes;
|
|
384
384
|
let expectedBytes;
|
|
385
385
|
try {
|
|
386
|
-
receivedBytes =
|
|
387
|
-
expectedBytes =
|
|
386
|
+
receivedBytes = _chunkWOXSQBAMcjs.base64UrlToBytes.call(void 0, receivedSignature);
|
|
387
|
+
expectedBytes = _chunkWOXSQBAMcjs.base64UrlToBytes.call(void 0, expectedSignature);
|
|
388
388
|
} catch (e) {
|
|
389
389
|
throw new Error("INVALID_TENANT_AUTH");
|
|
390
390
|
}
|
|
391
|
-
if (!
|
|
391
|
+
if (!_chunkWOXSQBAMcjs.timingSafeEqualBytes.call(void 0, receivedBytes, expectedBytes)) {
|
|
392
392
|
throw new Error("INVALID_TENANT_AUTH");
|
|
393
393
|
}
|
|
394
394
|
let payload;
|
|
395
395
|
try {
|
|
396
|
-
payload = JSON.parse(
|
|
396
|
+
payload = JSON.parse(_chunkWOXSQBAMcjs.utf8Decode.call(void 0, _chunkWOXSQBAMcjs.base64UrlToBytes.call(void 0, encodedPayload)));
|
|
397
397
|
} catch (e2) {
|
|
398
398
|
throw new Error("INVALID_TENANT_AUTH");
|
|
399
399
|
}
|
|
@@ -613,15 +613,15 @@ async function createReiServer(config) {
|
|
|
613
613
|
return {
|
|
614
614
|
handlers: {
|
|
615
615
|
initTenant: createInitTenantHandler(ctx),
|
|
616
|
-
getUserKey:
|
|
617
|
-
scheduleMessage:
|
|
616
|
+
getUserKey: _chunkWOXSQBAMcjs.createGetUserKeyHandler.call(void 0, ctx),
|
|
617
|
+
scheduleMessage: _chunkWOXSQBAMcjs.createScheduleMessageHandler.call(void 0, ctx),
|
|
618
618
|
sendNotifications: createSendNotificationsHandler(ctx),
|
|
619
|
-
updateMessage:
|
|
620
|
-
cancelMessage:
|
|
621
|
-
messages:
|
|
622
|
-
getMessage:
|
|
623
|
-
pushSubscription:
|
|
624
|
-
llmCredentials:
|
|
619
|
+
updateMessage: _chunkWOXSQBAMcjs.createUpdateMessageHandler.call(void 0, ctx),
|
|
620
|
+
cancelMessage: _chunkWOXSQBAMcjs.createCancelMessageHandler.call(void 0, ctx),
|
|
621
|
+
messages: _chunkWOXSQBAMcjs.createMessagesHandler.call(void 0, ctx),
|
|
622
|
+
getMessage: _chunkWOXSQBAMcjs.createGetMessageHandler.call(void 0, ctx),
|
|
623
|
+
pushSubscription: _chunkWOXSQBAMcjs.createPushSubscriptionHandler.call(void 0, ctx),
|
|
624
|
+
llmCredentials: _chunkWOXSQBAMcjs.createLlmCredentialsHandler.call(void 0, ctx)
|
|
625
625
|
}
|
|
626
626
|
};
|
|
627
627
|
}
|
|
@@ -683,4 +683,4 @@ async function createReiServer(config) {
|
|
|
683
683
|
|
|
684
684
|
|
|
685
685
|
|
|
686
|
-
exports.CORS_ALLOW_HEADERS =
|
|
686
|
+
exports.CORS_ALLOW_HEADERS = _chunkWOXSQBAMcjs.CORS_ALLOW_HEADERS; exports.CORS_ALLOW_METHODS = _chunkWOXSQBAMcjs.CORS_ALLOW_METHODS; exports.CRED_ID_MAX_LENGTH = _chunkWOXSQBAMcjs.CRED_ID_MAX_LENGTH; exports.CRED_PUT_BATCH_MAX = _chunkWOXSQBAMcjs.CRED_PUT_BATCH_MAX; exports.CRED_REFS_MAX_ENTRIES = _chunkWOXSQBAMcjs.CRED_REFS_MAX_ENTRIES; exports.CRED_ROWS_PER_USER_MAX = _chunkWOXSQBAMcjs.CRED_ROWS_PER_USER_MAX; exports.DEFAULT_CLAIM_LEASE_MS = _chunkWOXSQBAMcjs.DEFAULT_CLAIM_LEASE_MS; exports.DEFAULT_HEARTBEAT_LEASE_TTL_MS = _chunkWOXSQBAMcjs.DEFAULT_HEARTBEAT_LEASE_TTL_MS; exports.DEFAULT_LEASE_HEARTBEAT_MS = _chunkWOXSQBAMcjs.DEFAULT_LEASE_HEARTBEAT_MS; exports.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = _chunkWOXSQBAMcjs.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE; exports.DEFAULT_MAX_TOOL_ITERATIONS = _chunkWOXSQBAMcjs.DEFAULT_MAX_TOOL_ITERATIONS; exports.DEFAULT_TOTAL_TIMEOUT_MS = _chunkWOXSQBAMcjs.DEFAULT_TOTAL_TIMEOUT_MS; exports.MAX_PUSH_PAYLOAD_BYTES = _chunkWOXSQBAMcjs.MAX_PUSH_PAYLOAD_BYTES; exports.MIN_SCHEDULE_LEAD_MS = _chunkWOXSQBAMcjs.MIN_SCHEDULE_LEAD_MS; exports.NonRetryableError = _chunkWOXSQBAMcjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkWOXSQBAMcjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkWOXSQBAMcjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkWOXSQBAMcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkWOXSQBAMcjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkWOXSQBAMcjs.advanceOccurrence; exports.createAdapter = createAdapter; exports.createD1Adapter = _chunkWOXSQBAMcjs.createD1Adapter; exports.createReiServer = createReiServer; exports.createSingleUserCloudflareWorker = _chunkWOXSQBAMcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkWOXSQBAMcjs.createSingleUserServer; exports.createTenantToken = createTenantToken; exports.createWebCryptoWebPush = _chunkWOXSQBAMcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkWOXSQBAMcjs.decryptFromStorage; exports.decryptPayload = _chunkWOXSQBAMcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkWOXSQBAMcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkWOXSQBAMcjs.encryptForStorage; exports.ensureSchema = _chunkWOXSQBAMcjs.ensureSchema; exports.getSchemaVersion = _chunkWOXSQBAMcjs.getSchemaVersion; exports.hasChatCredRef = _chunkWOXSQBAMcjs.hasChatCredRef; exports.isNonRetryableError = _chunkWOXSQBAMcjs.isNonRetryableError; exports.isValidCredId = _chunkWOXSQBAMcjs.isValidCredId; exports.isValidISO8601 = _chunkWOXSQBAMcjs.isValidISO8601; exports.isValidTimeZoneId = _chunkWOXSQBAMcjs.isValidTimeZoneId; exports.isValidUUID = _chunkWOXSQBAMcjs.isValidUUID; exports.isValidUUIDv4 = _chunkWOXSQBAMcjs.isValidUUIDv4; exports.isValidUrl = _chunkWOXSQBAMcjs.isValidUrl; exports.measurePushPayload = _chunkWOXSQBAMcjs.measurePushPayload; exports.nextFutureOccurrence = _chunkWOXSQBAMcjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkWOXSQBAMcjs.planNextOccurrence; exports.resolveLlmCredential = _chunkWOXSQBAMcjs.resolveLlmCredential; exports.runScheduledTick = _chunkWOXSQBAMcjs.runScheduledTick; exports.runTask = _chunkWOXSQBAMcjs.runTask; exports.sanitizeErrorSummary = _chunkWOXSQBAMcjs.sanitizeErrorSummary; exports.summarizeErrorCause = _chunkWOXSQBAMcjs.summarizeErrorCause; exports.supportsLlmCredentialsStore = _chunkWOXSQBAMcjs.supportsLlmCredentialsStore; exports.validateAvatarUrl = _chunkWOXSQBAMcjs.validateAvatarUrl; exports.validateCredRefs = _chunkWOXSQBAMcjs.validateCredRefs; exports.validateCredValue = _chunkWOXSQBAMcjs.validateCredValue; exports.validateLlmMessagesArray = _chunkWOXSQBAMcjs.validateLlmMessagesArray; exports.validateScheduleMessagePayload = _chunkWOXSQBAMcjs.validateScheduleMessagePayload; exports.validateSplitPattern = _chunkWOXSQBAMcjs.validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
|
package/dist/index.mjs
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* @param {string} [providedMasterKey]
|
|
13
13
|
* @param {{ userKey: string, payload: Object } | null} [predecrypted] - 调用方
|
|
14
14
|
* (run-tick 的预扫描)已经解好的 payload;传了就不再解第二遍。
|
|
15
|
-
* @returns {Promise<{ success: boolean, messagesSent: number, error?: string, errorCode?: string|null }>}
|
|
15
|
+
* @returns {Promise<{ success: boolean, messagesSent: number, error?: string, errorCode?: string|null, pushStatusCode?: number|null, permanent?: boolean }>}
|
|
16
|
+
* 失败时 `pushStatusCode` 是推送服务回的 HTTP 状态码(不是推送阶段炸的 → null)。
|
|
16
17
|
*/
|
|
17
18
|
export function processSingleMessage(task: import("../adapters/interface.js").TaskRow, ctx: ProcessorContext, providedMasterKey?: string, predecrypted?: {
|
|
18
19
|
userKey: string;
|
|
@@ -22,6 +23,8 @@ export function processSingleMessage(task: import("../adapters/interface.js").Ta
|
|
|
22
23
|
messagesSent: number;
|
|
23
24
|
error?: string;
|
|
24
25
|
errorCode?: string | null;
|
|
26
|
+
pushStatusCode?: number | null;
|
|
27
|
+
permanent?: boolean;
|
|
25
28
|
}>;
|
|
26
29
|
/**
|
|
27
30
|
* Process a single message identified by UUID (used for instant type).
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
* @property {string|null} clientTaskId - 取自 metadata.amsgClientTaskId
|
|
27
27
|
* @property {Object|null} credRefs - 凭据引用({ <purpose>: <cred_id> });引用不是机密,客户端对账要看。没带 → null
|
|
28
28
|
* @property {Object|null} [metadata] - 只有 includeMetadata 时才有,见下
|
|
29
|
-
* @property {{ at: string, occurrence: string, reason: string }|null} lastError
|
|
29
|
+
* @property {{ at: string, occurrence: string, reason: string, pushStatus?: number }|null} lastError
|
|
30
|
+
* `pushStatus` 只在投递失败于推送这一步时出现,值是推送服务回的 HTTP 状态码。
|
|
30
31
|
*/
|
|
31
32
|
/**
|
|
32
33
|
* @param {Object} row - 数据库任务行
|
|
@@ -74,9 +75,13 @@ export type TaskProjection = {
|
|
|
74
75
|
* - 只有 includeMetadata 时才有,见下
|
|
75
76
|
*/
|
|
76
77
|
metadata?: any | null;
|
|
78
|
+
/**
|
|
79
|
+
* `pushStatus` 只在投递失败于推送这一步时出现,值是推送服务回的 HTTP 状态码。
|
|
80
|
+
*/
|
|
77
81
|
lastError: {
|
|
78
82
|
at: string;
|
|
79
83
|
occurrence: string;
|
|
80
84
|
reason: string;
|
|
85
|
+
pushStatus?: number;
|
|
81
86
|
} | null;
|
|
82
87
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rei-standard/amsg-server",
|
|
3
|
-
"version": "2.6.0-next.
|
|
3
|
+
"version": "2.6.0-next.20",
|
|
4
4
|
"description": "ReiStandard Active Messaging server SDK with pluggable database adapters. Three-axis push schema (messageKind / messageType / messageSubtype) from @rei-standard/amsg-shared. Auto-emits ReasoningPush when the LLM response carries reasoning_content.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|