@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.
@@ -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, errorCode = null, permanentFlag = false) {
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 = permanentFlag === true || errorCode === "PUSH_SUBSCRIPTION_MISSING" || errorCode === "PUSH_SUBSCRIPTION_STORE_UNSUPPORTED";
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 escapeLikePrefix(prefix) {
3290
- return prefix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
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 LIKE ? ESCAPE '\\' AND updated_at <= ?`;
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, `${escapeLikePrefix(c.keyPrefix)}%`, c.updatedAt)),
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 placeholders = credIds.map(() => "?").join(", ");
3830
- const res = await this._db.prepare(
3831
- `SELECT cred_id, encrypted_value, updated_at
3832
- FROM llm_credentials
3833
- WHERE user_id = ? AND cred_id IN (${placeholders})`
3834
- ).bind(userId, ...credIds).all();
3835
- return res.results || [];
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
- } else {
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 res.meta.changes || 0;
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
- const placeholders = messageIds.map(() => "?").join(", ");
3929
- const res = await this._db.prepare(
3930
- `UPDATE message_outbox SET delivered_at = ?
3931
- WHERE user_id = ? AND message_id IN (${placeholders})`
3932
- ).bind(deliveredAt, userId, ...messageIds).run();
3933
- return res.meta.changes || 0;
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
- const placeholders = messageIds.map(() => "?").join(", ");
3966
- const res = await this._db.prepare(
3967
- `UPDATE message_outbox SET acked_at = ?
3968
- WHERE user_id = ? AND acked_at IS NULL AND message_id IN (${placeholders})`
3969
- ).bind(ackedAt, userId, ...messageIds).run();
3970
- return res.meta.changes || 0;
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.19" : "0.0.0-dev";
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, errorCode = null, permanentFlag = false) {
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 = permanentFlag === true || errorCode === "PUSH_SUBSCRIPTION_MISSING" || errorCode === "PUSH_SUBSCRIPTION_STORE_UNSUPPORTED";
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 escapeLikePrefix(prefix) {
3290
- return prefix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
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 LIKE ? ESCAPE '\\' AND updated_at <= ?`;
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, `${escapeLikePrefix(c.keyPrefix)}%`, c.updatedAt)),
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 placeholders = credIds.map(() => "?").join(", ");
3830
- const res = await this._db.prepare(
3831
- `SELECT cred_id, encrypted_value, updated_at
3832
- FROM llm_credentials
3833
- WHERE user_id = ? AND cred_id IN (${placeholders})`
3834
- ).bind(userId, ...credIds).all();
3835
- return res.results || [];
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
- } else {
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 res.meta.changes || 0;
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
- const placeholders = messageIds.map(() => "?").join(", ");
3929
- const res = await this._db.prepare(
3930
- `UPDATE message_outbox SET delivered_at = ?
3931
- WHERE user_id = ? AND message_id IN (${placeholders})`
3932
- ).bind(deliveredAt, userId, ...messageIds).run();
3933
- return res.meta.changes || 0;
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
- const placeholders = messageIds.map(() => "?").join(", ");
3966
- const res = await this._db.prepare(
3967
- `UPDATE message_outbox SET acked_at = ?
3968
- WHERE user_id = ? AND acked_at IS NULL AND message_id IN (${placeholders})`
3969
- ).bind(ackedAt, userId, ...messageIds).run();
3970
- return res.meta.changes || 0;
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.19" : "0.0.0-dev";
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([
@@ -24,7 +24,7 @@
24
24
 
25
25
 
26
26
 
27
- var _chunkXVJ7ZLZXcjs = require('./chunk-XVJ7ZLZX.cjs');
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 = _chunkXVJ7ZLZXcjs.MAX_PUSH_PAYLOAD_BYTES; exports.NonRetryableError = _chunkXVJ7ZLZXcjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkXVJ7ZLZXcjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkXVJ7ZLZXcjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkXVJ7ZLZXcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkXVJ7ZLZXcjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkXVJ7ZLZXcjs.advanceOccurrence; exports.createD1Adapter = _chunkXVJ7ZLZXcjs.createD1Adapter; exports.createSingleUserCloudflareWorker = _chunkXVJ7ZLZXcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkXVJ7ZLZXcjs.createSingleUserServer; exports.createWebCryptoWebPush = _chunkXVJ7ZLZXcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkXVJ7ZLZXcjs.decryptFromStorage; exports.decryptPayload = _chunkXVJ7ZLZXcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkXVJ7ZLZXcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkXVJ7ZLZXcjs.encryptForStorage; exports.ensureSchema = _chunkXVJ7ZLZXcjs.ensureSchema; exports.getSchemaVersion = _chunkXVJ7ZLZXcjs.getSchemaVersion; exports.isNonRetryableError = _chunkXVJ7ZLZXcjs.isNonRetryableError; exports.isValidTimeZoneId = _chunkXVJ7ZLZXcjs.isValidTimeZoneId; exports.measurePushPayload = _chunkXVJ7ZLZXcjs.measurePushPayload; exports.nextFutureOccurrence = _chunkXVJ7ZLZXcjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkXVJ7ZLZXcjs.planNextOccurrence; exports.runScheduledTick = _chunkXVJ7ZLZXcjs.runScheduledTick; exports.runTask = _chunkXVJ7ZLZXcjs.runTask; exports.summarizeErrorCause = _chunkXVJ7ZLZXcjs.summarizeErrorCause;
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;
@@ -24,7 +24,7 @@ import {
24
24
  runScheduledTick,
25
25
  runTask,
26
26
  summarizeErrorCause
27
- } from "./chunk-PRQHHRAK.mjs";
27
+ } from "./chunk-HTGYGGUH.mjs";
28
28
  import "./chunk-5FXVSC5O.mjs";
29
29
  export {
30
30
  MAX_PUSH_PAYLOAD_BYTES,
package/dist/index.cjs CHANGED
@@ -67,7 +67,7 @@
67
67
 
68
68
 
69
69
 
70
- var _chunkXVJ7ZLZXcjs = require('./chunk-XVJ7ZLZX.cjs');
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 = _chunkXVJ7ZLZXcjs.getHeader.call(void 0, headers, "x-init-secret");
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 = _chunkXVJ7ZLZXcjs.parseJsonBody.call(void 0, body);
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 && !_chunkXVJ7ZLZXcjs.isValidUUIDv4.call(void 0, 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 _chunkXVJ7ZLZXcjs.runScheduledTick.call(void 0, { ...ctx, db, masterKey });
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.${_chunkXVJ7ZLZXcjs.bytesToBase64Url.call(void 0, iv)}.${_chunkXVJ7ZLZXcjs.bytesToBase64Url.call(void 0, authTag)}.${_chunkXVJ7ZLZXcjs.bytesToBase64Url.call(void 0, ciphertext)}`;
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 = _chunkXVJ7ZLZXcjs.base64UrlToBytes.call(void 0, parts[1]);
281
- const authTag = _chunkXVJ7ZLZXcjs.base64UrlToBytes.call(void 0, parts[2]);
282
- const ciphertext = _chunkXVJ7ZLZXcjs.base64UrlToBytes.call(void 0, parts[3]);
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 _chunkXVJ7ZLZXcjs.bytesToBase64Url.call(void 0, _crypto.createHmac.call(void 0, "sha256", secret).update(input).digest());
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 = _chunkXVJ7ZLZXcjs.bytesToBase64Url.call(void 0, _chunkXVJ7ZLZXcjs.utf8.call(void 0, JSON.stringify(header)));
367
- const encodedPayload = _chunkXVJ7ZLZXcjs.bytesToBase64Url.call(void 0, _chunkXVJ7ZLZXcjs.utf8.call(void 0, JSON.stringify(payload)));
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 = _chunkXVJ7ZLZXcjs.base64UrlToBytes.call(void 0, receivedSignature);
387
- expectedBytes = _chunkXVJ7ZLZXcjs.base64UrlToBytes.call(void 0, expectedSignature);
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 (!_chunkXVJ7ZLZXcjs.timingSafeEqualBytes.call(void 0, receivedBytes, expectedBytes)) {
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(_chunkXVJ7ZLZXcjs.utf8Decode.call(void 0, _chunkXVJ7ZLZXcjs.base64UrlToBytes.call(void 0, encodedPayload)));
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: _chunkXVJ7ZLZXcjs.createGetUserKeyHandler.call(void 0, ctx),
617
- scheduleMessage: _chunkXVJ7ZLZXcjs.createScheduleMessageHandler.call(void 0, ctx),
616
+ getUserKey: _chunkWOXSQBAMcjs.createGetUserKeyHandler.call(void 0, ctx),
617
+ scheduleMessage: _chunkWOXSQBAMcjs.createScheduleMessageHandler.call(void 0, ctx),
618
618
  sendNotifications: createSendNotificationsHandler(ctx),
619
- updateMessage: _chunkXVJ7ZLZXcjs.createUpdateMessageHandler.call(void 0, ctx),
620
- cancelMessage: _chunkXVJ7ZLZXcjs.createCancelMessageHandler.call(void 0, ctx),
621
- messages: _chunkXVJ7ZLZXcjs.createMessagesHandler.call(void 0, ctx),
622
- getMessage: _chunkXVJ7ZLZXcjs.createGetMessageHandler.call(void 0, ctx),
623
- pushSubscription: _chunkXVJ7ZLZXcjs.createPushSubscriptionHandler.call(void 0, ctx),
624
- llmCredentials: _chunkXVJ7ZLZXcjs.createLlmCredentialsHandler.call(void 0, ctx)
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 = _chunkXVJ7ZLZXcjs.CORS_ALLOW_HEADERS; exports.CORS_ALLOW_METHODS = _chunkXVJ7ZLZXcjs.CORS_ALLOW_METHODS; exports.CRED_ID_MAX_LENGTH = _chunkXVJ7ZLZXcjs.CRED_ID_MAX_LENGTH; exports.CRED_PUT_BATCH_MAX = _chunkXVJ7ZLZXcjs.CRED_PUT_BATCH_MAX; exports.CRED_REFS_MAX_ENTRIES = _chunkXVJ7ZLZXcjs.CRED_REFS_MAX_ENTRIES; exports.CRED_ROWS_PER_USER_MAX = _chunkXVJ7ZLZXcjs.CRED_ROWS_PER_USER_MAX; exports.DEFAULT_CLAIM_LEASE_MS = _chunkXVJ7ZLZXcjs.DEFAULT_CLAIM_LEASE_MS; exports.DEFAULT_HEARTBEAT_LEASE_TTL_MS = _chunkXVJ7ZLZXcjs.DEFAULT_HEARTBEAT_LEASE_TTL_MS; exports.DEFAULT_LEASE_HEARTBEAT_MS = _chunkXVJ7ZLZXcjs.DEFAULT_LEASE_HEARTBEAT_MS; exports.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = _chunkXVJ7ZLZXcjs.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE; exports.DEFAULT_MAX_TOOL_ITERATIONS = _chunkXVJ7ZLZXcjs.DEFAULT_MAX_TOOL_ITERATIONS; exports.DEFAULT_TOTAL_TIMEOUT_MS = _chunkXVJ7ZLZXcjs.DEFAULT_TOTAL_TIMEOUT_MS; exports.MAX_PUSH_PAYLOAD_BYTES = _chunkXVJ7ZLZXcjs.MAX_PUSH_PAYLOAD_BYTES; exports.MIN_SCHEDULE_LEAD_MS = _chunkXVJ7ZLZXcjs.MIN_SCHEDULE_LEAD_MS; exports.NonRetryableError = _chunkXVJ7ZLZXcjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkXVJ7ZLZXcjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkXVJ7ZLZXcjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkXVJ7ZLZXcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkXVJ7ZLZXcjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkXVJ7ZLZXcjs.advanceOccurrence; exports.createAdapter = createAdapter; exports.createD1Adapter = _chunkXVJ7ZLZXcjs.createD1Adapter; exports.createReiServer = createReiServer; exports.createSingleUserCloudflareWorker = _chunkXVJ7ZLZXcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkXVJ7ZLZXcjs.createSingleUserServer; exports.createTenantToken = createTenantToken; exports.createWebCryptoWebPush = _chunkXVJ7ZLZXcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkXVJ7ZLZXcjs.decryptFromStorage; exports.decryptPayload = _chunkXVJ7ZLZXcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkXVJ7ZLZXcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkXVJ7ZLZXcjs.encryptForStorage; exports.ensureSchema = _chunkXVJ7ZLZXcjs.ensureSchema; exports.getSchemaVersion = _chunkXVJ7ZLZXcjs.getSchemaVersion; exports.hasChatCredRef = _chunkXVJ7ZLZXcjs.hasChatCredRef; exports.isNonRetryableError = _chunkXVJ7ZLZXcjs.isNonRetryableError; exports.isValidCredId = _chunkXVJ7ZLZXcjs.isValidCredId; exports.isValidISO8601 = _chunkXVJ7ZLZXcjs.isValidISO8601; exports.isValidTimeZoneId = _chunkXVJ7ZLZXcjs.isValidTimeZoneId; exports.isValidUUID = _chunkXVJ7ZLZXcjs.isValidUUID; exports.isValidUUIDv4 = _chunkXVJ7ZLZXcjs.isValidUUIDv4; exports.isValidUrl = _chunkXVJ7ZLZXcjs.isValidUrl; exports.measurePushPayload = _chunkXVJ7ZLZXcjs.measurePushPayload; exports.nextFutureOccurrence = _chunkXVJ7ZLZXcjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkXVJ7ZLZXcjs.planNextOccurrence; exports.resolveLlmCredential = _chunkXVJ7ZLZXcjs.resolveLlmCredential; exports.runScheduledTick = _chunkXVJ7ZLZXcjs.runScheduledTick; exports.runTask = _chunkXVJ7ZLZXcjs.runTask; exports.sanitizeErrorSummary = _chunkXVJ7ZLZXcjs.sanitizeErrorSummary; exports.summarizeErrorCause = _chunkXVJ7ZLZXcjs.summarizeErrorCause; exports.supportsLlmCredentialsStore = _chunkXVJ7ZLZXcjs.supportsLlmCredentialsStore; exports.validateAvatarUrl = _chunkXVJ7ZLZXcjs.validateAvatarUrl; exports.validateCredRefs = _chunkXVJ7ZLZXcjs.validateCredRefs; exports.validateCredValue = _chunkXVJ7ZLZXcjs.validateCredValue; exports.validateLlmMessagesArray = _chunkXVJ7ZLZXcjs.validateLlmMessagesArray; exports.validateScheduleMessagePayload = _chunkXVJ7ZLZXcjs.validateScheduleMessagePayload; exports.validateSplitPattern = _chunkXVJ7ZLZXcjs.validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
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
@@ -67,7 +67,7 @@ import {
67
67
  validateLlmMessagesArray,
68
68
  validateScheduleMessagePayload,
69
69
  validateSplitPattern
70
- } from "./chunk-PRQHHRAK.mjs";
70
+ } from "./chunk-HTGYGGUH.mjs";
71
71
  import "./chunk-5FXVSC5O.mjs";
72
72
 
73
73
  // src/server/adapters/factory.js
@@ -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.19",
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",