@rei-standard/amsg-server 2.6.0-next.32 → 2.6.0-next.34

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.
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
3
 
4
4
 
@@ -31,7 +31,7 @@ function isValidTimeZoneId(tzId) {
31
31
  try {
32
32
  formatterFor(tzId);
33
33
  return true;
34
- } catch (e) {
34
+ } catch (e2) {
35
35
  return false;
36
36
  }
37
37
  }
@@ -278,8 +278,8 @@ function hasCredRefs(payload) {
278
278
  }
279
279
  async function saveLlmCredentials({ db, userId, userKey, credentials }) {
280
280
  const entries = [];
281
- for (const { credId, value } of credentials) {
282
- entries.push({ credId, encryptedValue: await encryptForStorage(JSON.stringify(value), userKey) });
281
+ for (const { credId, value, owner, kind, ownerGeneration } of credentials) {
282
+ entries.push({ credId, owner, kind, ownerGeneration, encryptedValue: await encryptForStorage(JSON.stringify(value), userKey) });
283
283
  }
284
284
  const upserted = await db.upsertLlmCredentials(userId, entries);
285
285
  return { upserted };
@@ -669,7 +669,7 @@ function parseBodyAsObject(body, options = {}) {
669
669
  if (typeof parsed === "string") {
670
670
  try {
671
671
  parsed = JSON.parse(parsed);
672
- } catch (e2) {
672
+ } catch (e3) {
673
673
  return { ok: false, error: invalidJson };
674
674
  }
675
675
  }
@@ -1025,6 +1025,9 @@ var CHUNK_NAMESPACE_PREFIX = CHUNK_NS_PREFIX;
1025
1025
  function chunkNamespaceFor(namespace) {
1026
1026
  return CHUNK_NS_PREFIX + namespace;
1027
1027
  }
1028
+ function isChunkNamespace(namespace) {
1029
+ return typeof namespace === "string" && namespace.startsWith(CHUNK_NS_PREFIX);
1030
+ }
1028
1031
  function chunkKeyFor(key, index) {
1029
1032
  return `${key}${SEP}${index}`;
1030
1033
  }
@@ -1125,19 +1128,29 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1125
1128
  const rootRowEntries = [];
1126
1129
  const deletions = [];
1127
1130
  for (const entry of entries) {
1131
+ const cloudFields = {
1132
+ owner: entry.owner,
1133
+ kind: entry.kind,
1134
+ ownerGeneration: entry.ownerGeneration,
1135
+ cloudValue: entry.value,
1136
+ cloudNamespace: entry.namespace,
1137
+ cloudKey: entry.key
1138
+ };
1128
1139
  const rawGuardAt = Number.isInteger(entry.version) && entry.version > 0 ? entry.version : entry.updatedAt;
1129
1140
  const guardAt = Math.min(rawGuardAt, at);
1130
1141
  cleanups.push({
1131
1142
  namespace: chunkNamespaceFor(entry.namespace),
1132
1143
  keyPrefix: chunkKeyPrefixFor(entry.key),
1133
- updatedAt: guardAt
1144
+ updatedAt: guardAt,
1145
+ ...cloudFields
1134
1146
  });
1135
1147
  if (entry.value === null) {
1136
1148
  deletions.push({ cleanupIndex: cleanups.length, entry });
1137
1149
  cleanups.push({
1138
1150
  namespace: entry.namespace,
1139
1151
  key: entry.key,
1140
- updatedAt: guardAt
1152
+ updatedAt: guardAt,
1153
+ ...cloudFields
1141
1154
  });
1142
1155
  continue;
1143
1156
  }
@@ -1148,7 +1161,8 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1148
1161
  namespace: entry.namespace,
1149
1162
  key: entry.key,
1150
1163
  value: await encryptForStorage(entry.value, userKey),
1151
- updatedAt: guardAt
1164
+ updatedAt: guardAt,
1165
+ ...cloudFields
1152
1166
  });
1153
1167
  } else {
1154
1168
  const slices = splitStateValue(entry.value);
@@ -1156,7 +1170,8 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1156
1170
  namespace: entry.namespace,
1157
1171
  key: entry.key,
1158
1172
  value: buildChunkedRootValue(slices.length),
1159
- updatedAt: guardAt
1173
+ updatedAt: guardAt,
1174
+ ...cloudFields
1160
1175
  });
1161
1176
  const encryptedSlices = await Promise.all(slices.map((slice) => encryptForStorage(slice, userKey)));
1162
1177
  for (let c = 0; c < encryptedSlices.length; c++) {
@@ -1164,7 +1179,8 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1164
1179
  namespace: chunkNamespaceFor(entry.namespace),
1165
1180
  key: chunkKeyFor(entry.key, c),
1166
1181
  value: encryptedSlices[c],
1167
- updatedAt: guardAt
1182
+ updatedAt: guardAt,
1183
+ ...cloudFields
1168
1184
  });
1169
1185
  }
1170
1186
  }
@@ -1293,6 +1309,7 @@ async function appendPushesToOutbox({ db, userId, userKey, pushes }) {
1293
1309
  await db.appendOutboxMessages(userId, await toOutboxRows(pushes, userKey, Date.now()));
1294
1310
  return true;
1295
1311
  } catch (error) {
1312
+ if (_optionalChain([error, 'optionalAccess', _5 => _5.code]) === "CLOUD_OWNER_RETIRED") throw error;
1296
1313
  console.warn("[amsg-server] outbox \u843D\u884C\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u6295\u9012\uFF09:", error && error.message);
1297
1314
  return false;
1298
1315
  }
@@ -1566,7 +1583,7 @@ async function sendResultPush({ db, task, userKey, decryptedPayload, webpush, pu
1566
1583
  await sendTaggedPush(webpush, subscription, JSON.stringify(push));
1567
1584
  return true;
1568
1585
  } catch (error) {
1569
- if (isTaskCancelledError(error)) throw error;
1586
+ if (isTaskCancelledError(error) || _optionalChain([error, 'optionalAccess', _6 => _6.code]) === "CLOUD_OWNER_RETIRED") throw error;
1570
1587
  console.warn(
1571
1588
  `[amsg-server] \u7ED3\u679C ${push.messageId} \u7684\u63A8\u9001\u6CA1\u53D1\u51FA\u53BB\uFF08\u5DF2\u843D\u8FDB\u6536\u4EF6\u7BB1\uFF0C\u7B49\u5BA2\u6237\u7AEF\u8865\u6536\uFF09:`,
1572
1589
  error && error.message
@@ -1636,6 +1653,7 @@ var DEFAULT_MAX_TOOL_ITERATIONS = 5;
1636
1653
  var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
1637
1654
  var DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
1638
1655
  var MIN_SCHEDULE_LEAD_MS = 6e4;
1656
+ var MAX_DEFER_AFTER_MS = 24 * 60 * 60 * 1e3;
1639
1657
  var SCHEDULABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set(["auto", "prompted", "fixed"]);
1640
1658
  var SCHEDULABLE_RECURRENCE_TYPES = /* @__PURE__ */ new Set(["none", "daily", "weekly"]);
1641
1659
  var SLEEP_BETWEEN_MESSAGES_MS = 1500;
@@ -1670,6 +1688,22 @@ function buildHookTask(task, decryptedPayload) {
1670
1688
  retryCount: _nullishCoalesce(task.retry_count, () => ( 0))
1671
1689
  });
1672
1690
  }
1691
+ var BAD_BEFORE_FIRE_MESSAGE = "AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs?, tools?, toolChoice? } | { skip: true } | { defer: { afterMs } } | null";
1692
+ function readDeferAfterMs(defer) {
1693
+ const afterMs = defer && typeof defer === "object" ? (
1694
+ /** @type {any} */
1695
+ defer.afterMs
1696
+ ) : void 0;
1697
+ if (typeof afterMs !== "number" || !Number.isFinite(afterMs) || afterMs <= 0 || afterMs > MAX_DEFER_AFTER_MS) {
1698
+ throw markPermanent(
1699
+ new TypeError(
1700
+ `${BAD_BEFORE_FIRE_MESSAGE}\uFF08defer.afterMs \u5FC5\u987B\u662F\u6709\u9650\u6B63\u6570\uFF0C\u4E14\u4E0D\u8D85\u8FC7 ${MAX_DEFER_AFTER_MS} \u6BEB\u79D2\uFF09`
1701
+ ),
1702
+ "AGENTIC_BAD_BEFORE_FIRE"
1703
+ );
1704
+ }
1705
+ return afterMs;
1706
+ }
1673
1707
  function normalizeBeforeFireResult(result) {
1674
1708
  if (Array.isArray(result)) {
1675
1709
  return { messages: result };
@@ -1686,12 +1720,7 @@ function normalizeBeforeFireResult(result) {
1686
1720
  toolChoice: result.toolChoice
1687
1721
  };
1688
1722
  }
1689
- throw markPermanent(
1690
- new TypeError(
1691
- "AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs?, tools?, toolChoice? } | { skip: true } | null"
1692
- ),
1693
- "AGENTIC_BAD_BEFORE_FIRE"
1694
- );
1723
+ throw markPermanent(new TypeError(BAD_BEFORE_FIRE_MESSAGE), "AGENTIC_BAD_BEFORE_FIRE");
1695
1724
  }
1696
1725
  function firstPositiveInt(values, fallback) {
1697
1726
  for (const v of values) {
@@ -1708,7 +1737,7 @@ function firstPositiveNumber(values, fallback) {
1708
1737
  async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
1709
1738
  const hooks = ctx.hooks;
1710
1739
  const signal = ctx.signal || new AbortController().signal;
1711
- const isCancelled = () => signal.aborted || _optionalChain([ctx, 'access', _5 => _5.isTaskCancelled, 'optionalCall', _6 => _6()]) === true;
1740
+ const isCancelled = () => signal.aborted || _optionalChain([ctx, 'access', _7 => _7.isTaskCancelled, 'optionalCall', _8 => _8()]) === true;
1712
1741
  const throwIfCancelled = () => {
1713
1742
  if (!isCancelled()) return;
1714
1743
  if (isTaskCancelledError(signal.reason)) throw signal.reason;
@@ -2026,6 +2055,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
2026
2055
  };
2027
2056
  let settledStatus = "failed";
2028
2057
  let settledError = null;
2058
+ let settledRetryAfter = null;
2029
2059
  try {
2030
2060
  const outcome = await runFireChain({
2031
2061
  task,
@@ -2050,7 +2080,11 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
2050
2080
  occurrenceMs
2051
2081
  });
2052
2082
  throwIfCancelled();
2053
- settledStatus = !outcome.handled ? "not-handled" : outcome.result.status === "skipped" ? "skipped" : "sent";
2083
+ if (!outcome.handled) settledStatus = "not-handled";
2084
+ else if (outcome.result.status === "deferred") {
2085
+ settledStatus = "deferred";
2086
+ settledRetryAfter = outcome.result.retryAfter;
2087
+ } else settledStatus = outcome.result.status === "skipped" ? "skipped" : "sent";
2054
2088
  return outcome;
2055
2089
  } catch (error) {
2056
2090
  if (isCancelled() && !isTaskCancelledError(error)) {
@@ -2070,6 +2104,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
2070
2104
  status: settledStatus,
2071
2105
  ...settledStatus === "cancelled" ? { willRetry: false, failureStage: null } : settledStatus === "failed" && ctx._deliveryState ? failureRetryDecision(ctx._deliveryState, settledError) : { willRetry: null, failureStage: null },
2072
2106
  skipReason: settledStatus === "skipped" ? progress.skipReason : null,
2107
+ retryAfter: settledStatus === "deferred" ? settledRetryAfter : null,
2073
2108
  sentCount: progress.sentCount,
2074
2109
  pushedCount: progress.pushedCount,
2075
2110
  total: progress.total,
@@ -2127,6 +2162,27 @@ async function runFireChain({
2127
2162
  progress.skipReason = "before-fire";
2128
2163
  return { handled: true, result: { success: true, messagesSent: 0, status: "skipped", iterations: 0 } };
2129
2164
  }
2165
+ if (typeof before === "object" && !Array.isArray(before) && before.defer != null) {
2166
+ const afterMs = readDeferAfterMs(before.defer);
2167
+ if (ctx._deferSupported !== true) {
2168
+ throw new DeploymentConfigError(
2169
+ "AGENTIC_DEFER_UNSUPPORTED: onBeforeFire \u8FD4\u56DE\u4E86 { defer }\uFF0C\u4F46\u8FD9\u6761\u6295\u9012\u8DEF\u5F84\u4E0D\u652F\u6301\u63A8\u8FDF\u2014\u2014\u53EA\u6709 runScheduledTick / runTask \u4E14\u9002\u914D\u5668\u5B9E\u73B0\u4E86 claimTask\uFF08\u4EFB\u52A1\u8868\u6709 retry_after \u5217\uFF09\u65F6\u53EF\u7528\uFF0C\u8BF7\u6C42\u5185\u5F53\u573A\u6295\u9012\u7684 instant \u4EFB\u52A1\u4E0D\u53EF\u7528",
2170
+ { code: "AGENTIC_DEFER_UNSUPPORTED" }
2171
+ );
2172
+ }
2173
+ return {
2174
+ handled: true,
2175
+ result: {
2176
+ success: false,
2177
+ deferred: true,
2178
+ // 用真实时钟:这个时刻要落进 retry_after,跟数据库捞取条件里的「现在」比。
2179
+ retryAfter: new Date(Date.now() + afterMs).toISOString(),
2180
+ messagesSent: 0,
2181
+ status: "deferred",
2182
+ iterations: 0
2183
+ }
2184
+ };
2185
+ }
2130
2186
  const normalized = normalizeBeforeFireResult(before);
2131
2187
  const maxToolIterations = firstPositiveInt(
2132
2188
  [normalized.maxToolIterations, ctx.maxToolIterations],
@@ -2256,7 +2312,7 @@ async function runFireChain({
2256
2312
  toolResults = toolCalls.map((toolCall) => ({
2257
2313
  tool_call_id: toolCall && typeof toolCall === "object" && typeof toolCall.id === "string" ? toolCall.id : "",
2258
2314
  role: "tool",
2259
- content: `Tool execution failed: ${_nullishCoalesce(_optionalChain([error, 'optionalAccess', _7 => _7.message]), () => ( String(error)))}`
2315
+ content: `Tool execution failed: ${_nullishCoalesce(_optionalChain([error, 'optionalAccess', _9 => _9.message]), () => ( String(error)))}`
2260
2316
  }));
2261
2317
  }
2262
2318
  const nativeCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
@@ -2431,6 +2487,330 @@ function addTokenCount(sum, value) {
2431
2487
 
2432
2488
 
2433
2489
 
2490
+
2491
+ // src/server/lib/cloud-data-guard.js
2492
+ var RAW_CLOUD_ADAPTER = /* @__PURE__ */ Symbol("cloud-data-adapter");
2493
+ function guardError(code, message) {
2494
+ return Object.assign(new Error(message), { code, status: 409 });
2495
+ }
2496
+ function validOwner(owner) {
2497
+ return owner && typeof owner.type === "string" && owner.type.trim().length > 0 && owner.type.length <= 100 && typeof owner.id === "string" && owner.id.trim().length > 0 && owner.id.length <= 300;
2498
+ }
2499
+ function validateGuard(guard) {
2500
+ if (!validOwner(_optionalChain([guard, 'optionalAccess', _10 => _10.owner])))
2501
+ throw guardError("INVALID_CLOUD_OWNER", "Cloud owner is invalid");
2502
+ if (!Number.isSafeInteger(guard.generation) || guard.generation < 0) {
2503
+ throw guardError(
2504
+ "INVALID_CLOUD_OWNER_GENERATION",
2505
+ "Cloud owner generation must be a non-negative integer"
2506
+ );
2507
+ }
2508
+ return { owner: guard.owner, generation: guard.generation };
2509
+ }
2510
+ async function assertCloudGuard(db, userId, guard) {
2511
+ if (!guard) return;
2512
+ validateGuard(guard);
2513
+ if (typeof db.isCloudOwnerGuardValid !== "function") {
2514
+ throw guardError(
2515
+ "CLOUD_GUARD_UNSUPPORTED",
2516
+ "Adapter cannot validate cloud owner guards"
2517
+ );
2518
+ }
2519
+ if (!await db.isCloudOwnerGuardValid(userId, guard)) {
2520
+ throw guardError(
2521
+ "CLOUD_OWNER_RETIRED",
2522
+ "Cloud owner is retired or its generation changed"
2523
+ );
2524
+ }
2525
+ }
2526
+ async function resolveMetadata(resolveOwner, input, explicit) {
2527
+ const resolved = typeof resolveOwner === "function" ? await resolveOwner(input) : null;
2528
+ const owner = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_optionalChain([explicit, 'optionalAccess', _11 => _11.owner]), () => ( _optionalChain([explicit, 'optionalAccess', _12 => _12.metadata, 'optionalAccess', _13 => _13.cloudOwner]))), () => ( _optionalChain([resolved, 'optionalAccess', _14 => _14.owner]))), () => ( (validOwner(resolved) ? resolved : null)));
2529
+ if (owner !== null && !validOwner(owner))
2530
+ throw guardError("INVALID_CLOUD_OWNER", "Cloud owner is invalid");
2531
+ return { owner, kind: _nullishCoalesce(_nullishCoalesce(_optionalChain([explicit, 'optionalAccess', _15 => _15.kind]), () => ( _optionalChain([resolved, 'optionalAccess', _16 => _16.kind]))), () => ( null)) };
2532
+ }
2533
+ async function captureCloudTaskGuard(db, userId, payload, resolveOwner, task) {
2534
+ if (!db.cloudDataManagement) return null;
2535
+ if (_optionalChain([payload, 'optionalAccess', _17 => _17.__cloudOwnerGuard])) {
2536
+ const guard2 = validateGuard(payload.__cloudOwnerGuard);
2537
+ await assertCloudGuard(db, userId, guard2);
2538
+ return guard2;
2539
+ }
2540
+ const { owner } = await resolveMetadata(
2541
+ resolveOwner,
2542
+ { type: "task", payload, task },
2543
+ payload
2544
+ );
2545
+ if (!owner) return null;
2546
+ const guard = validateGuard({
2547
+ owner,
2548
+ generation: _nullishCoalesce(_optionalChain([payload, 'optionalAccess', _18 => _18.ownerGeneration]), () => ( 0))
2549
+ });
2550
+ await assertCloudGuard(db, userId, guard);
2551
+ return guard;
2552
+ }
2553
+ function createCloudGuardedAdapter(db, {
2554
+ masterKey,
2555
+ resolveOwner,
2556
+ guard: inheritedGuard = null,
2557
+ userId: scopedUserId
2558
+ } = {}) {
2559
+ db = _nullishCoalesce(db[RAW_CLOUD_ADAPTER], () => ( db));
2560
+ if (!db.cloudDataManagement) return db;
2561
+ const methods = /* @__PURE__ */ new Map();
2562
+ const keyFor = (userId) => deriveUserEncryptionKey(userId, masterKey);
2563
+ const metadataFor = async (userId, input, explicit, userKey) => {
2564
+ const resolved = await resolveMetadata(resolveOwner, input, explicit);
2565
+ const resourceKey = input.type === "state" ? ["state", input.namespace, input.key] : input.type === "credential" ? ["credential", input.credId] : null;
2566
+ let expectedCloudMetadata;
2567
+ if (resourceKey && typeof db.getCloudResourceMetadata === "function") {
2568
+ const stored = await db.getCloudResourceMetadata(
2569
+ userId,
2570
+ JSON.stringify(resourceKey)
2571
+ );
2572
+ expectedCloudMetadata = stored;
2573
+ if (stored) {
2574
+ const previous = JSON.parse(await decryptFromStorage(stored, userKey));
2575
+ if (previous.owner) {
2576
+ if (resolved.owner && (previous.owner.type !== resolved.owner.type || previous.owner.id !== resolved.owner.id)) {
2577
+ throw guardError(
2578
+ "CLOUD_OWNER_IMMUTABLE",
2579
+ "Existing cloud resource ownership cannot be changed by ordinary writes"
2580
+ );
2581
+ }
2582
+ resolved.owner ??= previous.owner;
2583
+ resolved.kind ??= previous.kind;
2584
+ }
2585
+ }
2586
+ }
2587
+ const inheritsOwner = input.type === "task" || input.type === "outbox";
2588
+ let guard;
2589
+ if (inheritedGuard && inheritsOwner) {
2590
+ guard = inheritedGuard;
2591
+ } else if (resolved.owner) {
2592
+ if (inheritedGuard && (resolved.owner.type !== inheritedGuard.owner.type || resolved.owner.id !== inheritedGuard.owner.id)) {
2593
+ throw guardError(
2594
+ "CLOUD_CROSS_OWNER_WRITE",
2595
+ "A task cannot write another cloud owner's resources"
2596
+ );
2597
+ }
2598
+ guard = inheritedGuard || validateGuard({
2599
+ owner: resolved.owner,
2600
+ generation: _nullishCoalesce(_optionalChain([explicit, 'optionalAccess', _19 => _19.ownerGeneration]), () => ( 0))
2601
+ });
2602
+ } else {
2603
+ guard = null;
2604
+ }
2605
+ await assertCloudGuard(db, userId, inheritedGuard);
2606
+ await assertCloudGuard(db, userId, guard);
2607
+ const metadata = {
2608
+ owner: _nullishCoalesce(_optionalChain([guard, 'optionalAccess', _20 => _20.owner]), () => ( resolved.owner)),
2609
+ kind: resolved.kind,
2610
+ generation: _nullishCoalesce(_optionalChain([guard, 'optionalAccess', _21 => _21.generation]), () => ( 0))
2611
+ };
2612
+ return {
2613
+ cloudGuard: guard,
2614
+ encryptedCloudMetadata: await encryptForStorage(
2615
+ JSON.stringify(metadata),
2616
+ userKey
2617
+ ),
2618
+ ...expectedCloudMetadata !== void 0 ? { expectedCloudMetadata } : {}
2619
+ };
2620
+ };
2621
+ const stateIdentity = (row) => {
2622
+ const chunk = isChunkNamespace(row.namespace);
2623
+ const namespace = _nullishCoalesce(row.cloudNamespace, () => ( (chunk ? row.namespace.slice(CHUNK_NAMESPACE_PREFIX.length) : row.namespace)));
2624
+ let key = _nullishCoalesce(_nullishCoalesce(row.cloudKey, () => ( row.key)), () => ( row.keyPrefix));
2625
+ if (chunk && row.cloudKey === void 0 && typeof key === "string")
2626
+ key = key.slice(0, key.lastIndexOf(""));
2627
+ return { namespace, key };
2628
+ };
2629
+ const wrap = (name, fn) => {
2630
+ if (typeof db[name] === "function") methods.set(name, fn);
2631
+ };
2632
+ for (const method of ["createTask", "createTaskSuperseding"]) {
2633
+ wrap(method, async (params, supersedesUuid) => {
2634
+ const key = await keyFor(params.user_id);
2635
+ const payload = JSON.parse(
2636
+ await decryptFromStorage(params.encrypted_payload, key)
2637
+ );
2638
+ delete payload.__cloudOwnerGuard;
2639
+ const metadata = await metadataFor(
2640
+ params.user_id,
2641
+ { type: "task", payload, task: params },
2642
+ payload,
2643
+ key
2644
+ );
2645
+ if (metadata.cloudGuard) payload.__cloudOwnerGuard = metadata.cloudGuard;
2646
+ const next = {
2647
+ ...params,
2648
+ ...metadata,
2649
+ encrypted_payload: await encryptForStorage(JSON.stringify(payload), key)
2650
+ };
2651
+ return method === "createTaskSuperseding" ? db[method](next, supersedesUuid, metadata.cloudGuard) : db[method](next, metadata.cloudGuard);
2652
+ });
2653
+ }
2654
+ wrap(
2655
+ "upsertClientState",
2656
+ async (userId, entries, cleanups = [], now = Date.now()) => {
2657
+ const key = await keyFor(userId);
2658
+ const roots = /* @__PURE__ */ new Map();
2659
+ const rows = [];
2660
+ for (const entry of entries) {
2661
+ const identity = stateIdentity(entry);
2662
+ const token = JSON.stringify([identity.namespace, identity.key]);
2663
+ let metadata = roots.get(token);
2664
+ if (!metadata) {
2665
+ let value = entry.cloudValue;
2666
+ if (value === void 0 && entry.value && !isChunkNamespace(entry.namespace) && !parseChunkedRootCount(entry.value)) {
2667
+ value = await decryptFromStorage(entry.value, key);
2668
+ }
2669
+ metadata = await metadataFor(
2670
+ userId,
2671
+ { type: "state", ...identity, value, row: entry },
2672
+ entry,
2673
+ key
2674
+ );
2675
+ roots.set(token, metadata);
2676
+ }
2677
+ rows.push(
2678
+ isChunkNamespace(entry.namespace) ? { ...entry, cloudGuard: metadata.cloudGuard } : { ...entry, ...metadata }
2679
+ );
2680
+ }
2681
+ const guardedCleanups = [];
2682
+ for (const cleanup of cleanups) {
2683
+ const identity = stateIdentity(cleanup);
2684
+ const token = JSON.stringify([identity.namespace, identity.key]);
2685
+ const metadata = await _asyncNullishCoalesce(roots.get(token), async () => ( await metadataFor(
2686
+ userId,
2687
+ {
2688
+ type: "state",
2689
+ ...identity,
2690
+ value: cleanup.cloudValue,
2691
+ row: cleanup
2692
+ },
2693
+ cleanup,
2694
+ key
2695
+ )));
2696
+ guardedCleanups.push({
2697
+ ...cleanup,
2698
+ cloudGuard: metadata.cloudGuard,
2699
+ cloudMetadataKey: JSON.stringify([
2700
+ "state",
2701
+ identity.namespace,
2702
+ identity.key
2703
+ ]),
2704
+ ...metadata.expectedCloudMetadata !== void 0 ? { expectedCloudMetadata: metadata.expectedCloudMetadata } : {}
2705
+ });
2706
+ }
2707
+ return db.upsertClientState(
2708
+ userId,
2709
+ rows,
2710
+ guardedCleanups,
2711
+ now,
2712
+ inheritedGuard
2713
+ );
2714
+ }
2715
+ );
2716
+ wrap("upsertLlmCredentials", async (userId, entries) => {
2717
+ const key = await keyFor(userId);
2718
+ const rows = [];
2719
+ for (const entry of entries) {
2720
+ const value = JSON.parse(
2721
+ await decryptFromStorage(entry.encryptedValue, key)
2722
+ );
2723
+ const metadata = await metadataFor(
2724
+ userId,
2725
+ { type: "credential", credId: entry.credId, value, row: entry },
2726
+ entry,
2727
+ key
2728
+ );
2729
+ rows.push({ ...entry, ...metadata });
2730
+ }
2731
+ return db.upsertLlmCredentials(userId, rows, inheritedGuard);
2732
+ });
2733
+ wrap("appendOutboxMessages", async (userId, entries) => {
2734
+ const key = await keyFor(userId);
2735
+ const rows = [];
2736
+ for (const entry of entries) {
2737
+ const payload = JSON.parse(await decryptFromStorage(entry.payload, key));
2738
+ const metadata = await metadataFor(
2739
+ userId,
2740
+ { type: "outbox", payload, row: entry },
2741
+ payload,
2742
+ key
2743
+ );
2744
+ rows.push({ ...entry, ...metadata });
2745
+ }
2746
+ return db.appendOutboxMessages(userId, rows, inheritedGuard);
2747
+ });
2748
+ if (inheritedGuard) {
2749
+ wrap("updateTaskById", async (id, updates) => {
2750
+ if (scopedUserId)
2751
+ await assertCloudGuard(db, scopedUserId, inheritedGuard);
2752
+ return db.updateTaskById(id, updates, inheritedGuard);
2753
+ });
2754
+ }
2755
+ wrap(
2756
+ "updateTaskByUuid",
2757
+ async (uuid, userId, encryptedPayload, extraFields) => {
2758
+ const existing = await db.getTaskByUuid(uuid, userId);
2759
+ if (!existing) return null;
2760
+ const key = await keyFor(userId);
2761
+ const originalPayload = JSON.parse(
2762
+ await decryptFromStorage(existing.encrypted_payload, key)
2763
+ );
2764
+ const originalGuard = await captureCloudTaskGuard(
2765
+ db,
2766
+ userId,
2767
+ originalPayload,
2768
+ resolveOwner,
2769
+ existing
2770
+ );
2771
+ if (inheritedGuard && originalGuard && (originalGuard.owner.type !== inheritedGuard.owner.type || originalGuard.owner.id !== inheritedGuard.owner.id)) {
2772
+ throw guardError(
2773
+ "CLOUD_CROSS_OWNER_WRITE",
2774
+ "A task cannot update another cloud owner"
2775
+ );
2776
+ }
2777
+ const guard = inheritedGuard || originalGuard;
2778
+ await assertCloudGuard(db, userId, guard);
2779
+ const payload = JSON.parse(
2780
+ await decryptFromStorage(encryptedPayload, key)
2781
+ );
2782
+ delete payload.__cloudOwnerGuard;
2783
+ if (guard) payload.__cloudOwnerGuard = guard;
2784
+ for (const field of ["owner", "ownerGeneration"]) {
2785
+ if (originalPayload[field] !== void 0)
2786
+ payload[field] = originalPayload[field];
2787
+ else delete payload[field];
2788
+ }
2789
+ if (payload.metadata && typeof payload.metadata === "object") {
2790
+ if (_optionalChain([originalPayload, 'access', _22 => _22.metadata, 'optionalAccess', _23 => _23.cloudOwner]) !== void 0)
2791
+ payload.metadata.cloudOwner = originalPayload.metadata.cloudOwner;
2792
+ else delete payload.metadata.cloudOwner;
2793
+ }
2794
+ return db.updateTaskByUuid(
2795
+ uuid,
2796
+ userId,
2797
+ await encryptForStorage(JSON.stringify(payload), key),
2798
+ extraFields,
2799
+ guard
2800
+ );
2801
+ }
2802
+ );
2803
+ return new Proxy(db, {
2804
+ get(target, property) {
2805
+ if (property === RAW_CLOUD_ADAPTER) return target;
2806
+ if (methods.has(property)) return methods.get(property);
2807
+ const value = Reflect.get(target, property, target);
2808
+ return typeof value === "function" ? value.bind(target) : value;
2809
+ }
2810
+ });
2811
+ }
2812
+
2813
+ // src/server/lib/message-processor.js
2434
2814
  var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
2435
2815
  var SLEEP_BETWEEN_MESSAGES_MS2 = 1500;
2436
2816
  var MULTIPART_WINDOW_USAGE = 0.5;
@@ -2599,6 +2979,19 @@ async function processSingleMessage(task, ctx, providedMasterKey, predecrypted =
2599
2979
  }
2600
2980
  const userKey = predecrypted && predecrypted.userKey || await deriveUserEncryptionKey(task.user_id, masterKey);
2601
2981
  const decryptedPayload = predecrypted && predecrypted.payload || JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
2982
+ const cloudGuard = await captureCloudTaskGuard(ctx.db, task.user_id, decryptedPayload, _optionalChain([ctx, 'access', _24 => _24.cloudData, 'optionalAccess', _25 => _25.resolveOwner]), task);
2983
+ if (cloudGuard) {
2984
+ await assertCloudGuard(ctx.db, task.user_id, cloudGuard);
2985
+ const originalWebpush = ctx.webpush;
2986
+ const guardedDb = createCloudGuardedAdapter(ctx.db, { masterKey, resolveOwner: _optionalChain([ctx, 'access', _26 => _26.cloudData, 'optionalAccess', _27 => _27.resolveOwner]), guard: cloudGuard, userId: task.user_id });
2987
+ ctx = { ...ctx, db: guardedDb, webpush: originalWebpush && {
2988
+ ...originalWebpush,
2989
+ async sendNotification(...args) {
2990
+ await assertCloudGuard(guardedDb, task.user_id, cloudGuard);
2991
+ return originalWebpush.sendNotification(...args);
2992
+ }
2993
+ } };
2994
+ }
2602
2995
  const committed = await findCommittedBatch({
2603
2996
  db: ctx.db,
2604
2997
  userId: task.user_id,
@@ -2781,8 +3174,22 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
2781
3174
  if (!task) {
2782
3175
  return { success: false, error: { code: "TASK_NOT_FOUND", message: "\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u5DF2\u5904\u7406" } };
2783
3176
  }
2784
- const result = await processSingleMessage({ ...task, retry_count: retryCount }, { ...ctx, maxDeliveryRetries: maxRetries }, masterKey, null);
3177
+ let attemptDb = ctx.db;
3178
+ if (ctx.db.cloudDataManagement) {
3179
+ try {
3180
+ const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
3181
+ const payload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
3182
+ const guard = await captureCloudTaskGuard(ctx.db, task.user_id, payload, _optionalChain([ctx, 'access', _28 => _28.cloudData, 'optionalAccess', _29 => _29.resolveOwner]), task);
3183
+ if (guard) attemptDb = createCloudGuardedAdapter(ctx.db, { masterKey, resolveOwner: _optionalChain([ctx, 'access', _30 => _30.cloudData, 'optionalAccess', _31 => _31.resolveOwner]), guard, userId: task.user_id });
3184
+ } catch (error) {
3185
+ return { success: false, error: { code: error.code || "PROCESSING_ERROR", message: error.message, retriesAttempted: retryCount } };
3186
+ }
3187
+ }
3188
+ const result = await processSingleMessage({ ...task, retry_count: retryCount }, { ...ctx, db: attemptDb, maxDeliveryRetries: maxRetries }, masterKey, null);
2785
3189
  if (!result.success) {
3190
+ if (result.errorCode === "CLOUD_OWNER_RETIRED") {
3191
+ return { success: false, error: { code: result.errorCode, message: result.error, retriesAttempted: retryCount } };
3192
+ }
2786
3193
  const permanent = isPermanentDeliveryFailure({
2787
3194
  permanent: result.permanent,
2788
3195
  errorCode: result.errorCode,
@@ -2794,7 +3201,7 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
2794
3201
  continue;
2795
3202
  }
2796
3203
  try {
2797
- await ctx.db.updateTaskById(task.id, {
3204
+ await attemptDb.updateTaskById(task.id, {
2798
3205
  status: "failed",
2799
3206
  retry_count: retryCount,
2800
3207
  // 记录的形状跟定时任务那条路一致(同一个 buildErrorExtra):reason
@@ -2809,8 +3216,11 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
2809
3216
  })
2810
3217
  });
2811
3218
  } catch (_updateError) {
3219
+ if (_optionalChain([_updateError, 'optionalAccess', _32 => _32.code]) === "CLOUD_OWNER_RETIRED") {
3220
+ return { success: false, error: { code: _updateError.code, message: _updateError.message, retriesAttempted: retryCount } };
3221
+ }
2812
3222
  try {
2813
- await ctx.db.updateTaskById(task.id, { status: "failed", retry_count: retryCount });
3223
+ await attemptDb.updateTaskById(task.id, { status: "failed", retry_count: retryCount });
2814
3224
  } catch (_retryError) {
2815
3225
  }
2816
3226
  }
@@ -2828,7 +3238,7 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
2828
3238
  await ctx.db.deleteTaskById(task.id);
2829
3239
  } catch (error) {
2830
3240
  try {
2831
- await ctx.db.updateTaskById(task.id, { status: "sent", retry_count: 0 });
3241
+ await attemptDb.updateTaskById(task.id, { status: "sent", retry_count: 0 });
2832
3242
  } catch (_markSentError) {
2833
3243
  }
2834
3244
  return {
@@ -2979,6 +3389,9 @@ function createScheduleMessageHandler(ctx) {
2979
3389
  // 透传给 LLM 中转的非标准参数(thinking 之类),buildLlmRequestBody 会
2980
3390
  // 把它展开进请求体(核心字段优先)。
2981
3391
  llmExtraBody: _nullishCoalesce(payload.llmExtraBody, () => ( null)),
3392
+ owner: payload.owner,
3393
+ kind: payload.kind,
3394
+ ownerGeneration: payload.ownerGeneration,
2982
3395
  metadata: payload.metadata || {}
2983
3396
  };
2984
3397
  const serializedTaskData = JSON.stringify(fullTaskData);
@@ -3240,19 +3653,26 @@ async function deliverTasks(ctx, tasks) {
3240
3653
  deletedOnceOffTasks: 0,
3241
3654
  updatedRecurringTasks: 0,
3242
3655
  staleTasks: [],
3656
+ deferredTasks: [],
3243
3657
  cancelledTasks: [],
3244
3658
  reasoningSkippedTasks: [],
3245
3659
  redeliveredTasks: [],
3246
3660
  failedTasks: []
3247
3661
  };
3248
3662
  const groupsTakenThisTick = /* @__PURE__ */ new Set();
3663
+ const taskCloudGuards = /* @__PURE__ */ new Map();
3664
+ const updateLifecycleTask = (taskId, fields) => {
3665
+ const guard = taskCloudGuards.get(taskId);
3666
+ return guard ? db.updateTaskById(taskId, fields, guard) : db.updateTaskById(taskId, fields);
3667
+ };
3249
3668
  const supportsClaim = typeof db.claimTask === "function";
3250
3669
  async function claimForThisTick(task, serializeGroup) {
3251
3670
  if (!supportsClaim) return true;
3252
3671
  const leaseUntil = new Date(
3253
3672
  Date.now() + (heartbeatEnabled ? heartbeatLeaseTtlMs : claimLeaseMs)
3254
3673
  ).toISOString();
3255
- return !!await db.claimTask(task.id, task.next_send_at, leaseUntil, serializeGroup);
3674
+ const guard = taskCloudGuards.get(task.id);
3675
+ return !!(guard ? await db.claimTask(task.id, task.next_send_at, leaseUntil, serializeGroup, guard) : await db.claimTask(task.id, task.next_send_at, leaseUntil, serializeGroup));
3256
3676
  }
3257
3677
  function startLeaseHeartbeat(task) {
3258
3678
  const controller = new AbortController();
@@ -3265,9 +3685,12 @@ async function deliverTasks(ctx, tasks) {
3265
3685
  if (stopped) return;
3266
3686
  let renewed;
3267
3687
  try {
3268
- renewed = await db.renewTaskLease(task.id, new Date(Date.now() + heartbeatLeaseTtlMs).toISOString());
3688
+ const guard = taskCloudGuards.get(task.id);
3689
+ const until = new Date(Date.now() + heartbeatLeaseTtlMs).toISOString();
3690
+ renewed = guard ? await db.renewTaskLease(task.id, until, guard) : await db.renewTaskLease(task.id, until);
3269
3691
  } catch (error) {
3270
- console.warn("[amsg-server] \u79DF\u7EA6\u7EED\u79DF\u5931\u8D25\uFF08\u4E0B\u4E2A\u5FC3\u8DF3\u518D\u8BD5\uFF09:", error && error.message);
3692
+ if (_optionalChain([error, 'optionalAccess', _33 => _33.code]) === "CLOUD_OWNER_RETIRED") renewed = false;
3693
+ else console.warn("[amsg-server] \u79DF\u7EA6\u7EED\u79DF\u5931\u8D25\uFF08\u4E0B\u4E2A\u5FC3\u8DF3\u518D\u8BD5\uFF09:", error && error.message);
3271
3694
  }
3272
3695
  if (renewed === false) {
3273
3696
  if (stopped || lease.released) return;
@@ -3299,19 +3722,20 @@ async function deliverTasks(ctx, tasks) {
3299
3722
  async function updateTaskWithLastError(taskId, fields) {
3300
3723
  if (fields.lease_until === null) markLeaseReleased(taskId);
3301
3724
  if (!Object.prototype.hasOwnProperty.call(fields, "last_error")) {
3302
- return db.updateTaskById(taskId, fields);
3725
+ return updateLifecycleTask(taskId, fields);
3303
3726
  }
3304
3727
  const { last_error: _lastError, ...stateFields } = fields;
3305
- if (adaptersWithoutLastErrorColumn.has(db)) return db.updateTaskById(taskId, stateFields);
3728
+ if (adaptersWithoutLastErrorColumn.has(db)) return updateLifecycleTask(taskId, stateFields);
3306
3729
  let combinedError;
3307
3730
  try {
3308
- const result2 = await db.updateTaskById(taskId, fields);
3731
+ const result2 = await updateLifecycleTask(taskId, fields);
3309
3732
  lastErrorColumnSuspicions.delete(db);
3310
3733
  return result2;
3311
3734
  } catch (error) {
3735
+ if (_optionalChain([error, 'optionalAccess', _34 => _34.code]) === "CLOUD_OWNER_RETIRED") throw error;
3312
3736
  combinedError = error;
3313
3737
  }
3314
- const result = await db.updateTaskById(taskId, stateFields);
3738
+ const result = await updateLifecycleTask(taskId, stateFields);
3315
3739
  warnMissingLastErrorColumn(combinedError);
3316
3740
  const suspicions = (lastErrorColumnSuspicions.get(db) || 0) + 1;
3317
3741
  if (suspicions >= 2) {
@@ -3502,8 +3926,16 @@ async function deliverTasks(ctx, tasks) {
3502
3926
  const serializeGroup = serializeKey ? await deriveSerializeGroup(userKey, serializeKey) : null;
3503
3927
  let claimed;
3504
3928
  try {
3929
+ if (decrypted.ok) {
3930
+ const guard = await captureCloudTaskGuard(db, task.user_id, decryptedPayload, _optionalChain([ctx, 'access', _35 => _35.cloudData, 'optionalAccess', _36 => _36.resolveOwner]), task);
3931
+ if (guard) taskCloudGuards.set(task.id, guard);
3932
+ }
3505
3933
  claimed = await claimForThisTick(task, serializeGroup);
3506
3934
  } catch (error) {
3935
+ if (_optionalChain([error, 'optionalAccess', _37 => _37.code]) === "CLOUD_OWNER_RETIRED") {
3936
+ await recordCancelled(task, "owner_retired");
3937
+ return;
3938
+ }
3507
3939
  results.failedCount++;
3508
3940
  results.failedTasks.push({ taskId: task.id, reason: error.message || "\u4EFB\u52A1\u5360\u4F4D\u5931\u8D25", status: "claim_failed" });
3509
3941
  return;
@@ -3533,23 +3965,32 @@ async function deliverTasks(ctx, tasks) {
3533
3965
  const occurrenceMs = Date.parse(task.next_send_at);
3534
3966
  const retryAfterMs = task.retry_after ? Date.parse(task.retry_after) : NaN;
3535
3967
  const notOnFreshRetryChain = (task.retry_count || 0) === 0 || Number.isFinite(retryAfterMs) && Date.now() - retryAfterMs > staleAfterMs;
3536
- if (Number.isFinite(occurrenceMs) && Date.now() - occurrenceMs > staleAfterMs && notOnFreshRetryChain) {
3968
+ const settleAsStale = async () => {
3537
3969
  try {
3970
+ const cloudGuard = await captureCloudTaskGuard(db, task.user_id, decryptedPayload, _optionalChain([ctx, 'access', _38 => _38.cloudData, 'optionalAccess', _39 => _39.resolveOwner]), task);
3971
+ const guardedDb = createCloudGuardedAdapter(db, { masterKey, resolveOwner: _optionalChain([ctx, 'access', _40 => _40.cloudData, 'optionalAccess', _41 => _41.resolveOwner]), guard: cloudGuard, userId: task.user_id });
3972
+ const guardedWebpush = cloudGuard && ctx.webpush ? {
3973
+ ...ctx.webpush,
3974
+ async sendNotification(...args) {
3975
+ await assertCloudGuard(db, task.user_id, cloudGuard);
3976
+ return ctx.webpush.sendNotification(...args);
3977
+ }
3978
+ } : ctx.webpush;
3538
3979
  const stateAccessors = createStateAccessors({
3539
- db,
3980
+ db: guardedDb,
3540
3981
  userId: task.user_id,
3541
3982
  userKey,
3542
3983
  maxStateValueBytes: ctx.maxStateValueBytes
3543
3984
  });
3544
3985
  const { emitResult } = createResultEmitter({
3545
- db,
3986
+ db: guardedDb,
3546
3987
  task,
3547
3988
  userKey,
3548
3989
  decryptedPayload,
3549
3990
  messageIdBase: task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_stale_${task.uuid || ""}`,
3550
3991
  sessionId: task.id != null ? `sess_task_${task.id}${occurrenceSuffix(task)}` : `sess_stale_${task.uuid || ""}`,
3551
3992
  occurrenceMs,
3552
- webpush: ctx.webpush,
3993
+ webpush: guardedWebpush,
3553
3994
  isCancelled: () => lease.lost
3554
3995
  });
3555
3996
  const recurring = isRecurringType(recurrenceType);
@@ -3593,8 +4034,32 @@ async function deliverTasks(ctx, tasks) {
3593
4034
  results.failedCount++;
3594
4035
  results.failedTasks.push({ taskId: task.id, reason: error.message || "\u8FC7\u671F\u4EFB\u52A1\u5904\u7406\u5931\u8D25", status: "stale_update_failed" });
3595
4036
  }
4037
+ };
4038
+ if (Number.isFinite(occurrenceMs) && Date.now() - occurrenceMs > staleAfterMs && notOnFreshRetryChain) {
4039
+ await settleAsStale();
3596
4040
  return;
3597
4041
  }
4042
+ const handleDeferred = async (retryAfter) => {
4043
+ const wakeMs = Date.parse(retryAfter);
4044
+ if (Number.isFinite(occurrenceMs) && wakeMs - occurrenceMs > staleAfterMs) {
4045
+ await settleAsStale();
4046
+ return;
4047
+ }
4048
+ try {
4049
+ const updated = await updateTaskWithLastError(task.id, {
4050
+ retry_after: retryAfter,
4051
+ lease_until: null
4052
+ });
4053
+ if (rowVanished(updated)) {
4054
+ await recordCancelled(task, "cancelled_mid_delivery");
4055
+ return;
4056
+ }
4057
+ results.deferredTasks.push({ taskId: task.id, retryAfter });
4058
+ } catch (error) {
4059
+ results.failedCount++;
4060
+ results.failedTasks.push({ taskId: task.id, reason: error.message || "\u63A8\u8FDF\u5199\u5E93\u5931\u8D25", status: "defer_update_failed" });
4061
+ }
4062
+ };
3598
4063
  let sendResult;
3599
4064
  try {
3600
4065
  sendResult = await processSingleMessage(
@@ -3605,7 +4070,11 @@ async function deliverTasks(ctx, tasks) {
3605
4070
  masterKey,
3606
4071
  webpush: guardWebpushWithLease(ctx.webpush, lease),
3607
4072
  isTaskCancelled: () => lease.lost,
3608
- signal: lease.signal
4073
+ signal: lease.signal,
4074
+ // onBeforeFire 的 { defer } 要靠 retry_after 这一列落地,没实现
4075
+ // claimTask 的适配器没有这一列(见 lib/agentic-fire.js 的
4076
+ // AGENTIC_DEFER_UNSUPPORTED)。
4077
+ _deferSupported: supportsClaim
3609
4078
  },
3610
4079
  masterKey,
3611
4080
  { userKey, payload: decryptedPayload }
@@ -3625,7 +4094,19 @@ async function deliverTasks(ctx, tasks) {
3625
4094
  );
3626
4095
  return;
3627
4096
  }
4097
+ if (sendResult.deferred) {
4098
+ if (lease.lost) {
4099
+ await recordCancelled(task, "cancelled_mid_delivery");
4100
+ return;
4101
+ }
4102
+ await handleDeferred(sendResult.retryAfter);
4103
+ return;
4104
+ }
3628
4105
  if (!sendResult.success) {
4106
+ if (sendResult.errorCode === "CLOUD_OWNER_RETIRED") {
4107
+ await recordCancelled(task, "owner_retired");
4108
+ return;
4109
+ }
3629
4110
  if (lease.lost) {
3630
4111
  await recordCancelled(task, "cancelled_mid_delivery");
3631
4112
  return;
@@ -3717,6 +4198,9 @@ async function deliverTasks(ctx, tasks) {
3717
4198
  deletedOnceOffTasks: results.deletedOnceOffTasks,
3718
4199
  updatedRecurringTasks: results.updatedRecurringTasks,
3719
4200
  staleTasks: results.staleTasks,
4201
+ // onBeforeFire 返回 { defer } 被推迟的任务({ taskId, retryAfter }):这一
4202
+ // 跳没生成也没发,到 retryAfter 之后再问。不计入 successCount / failedCount。
4203
+ deferredTasks: results.deferredTasks,
3720
4204
  // 投递期间行被取消 / 顶替的任务。`cancelled_mid_delivery` = 推送在发出去
3721
4205
  // 之前被拦下;`cancelled_after_delivery` = 推送已经发完,收尾写库才发现
3722
4206
  // 行没了。两种都不计入 successCount / failedCount。
@@ -4558,6 +5042,34 @@ var SQLITE_ALL_INDEXES = [
4558
5042
  ...CLIENT_STATE_INDEXES,
4559
5043
  ...MESSAGE_OUTBOX_INDEXES
4560
5044
  ];
5045
+ var CLOUD_DATA_TABLES_SQL = [
5046
+ `CREATE TABLE IF NOT EXISTS cloud_data_records (
5047
+ user_id TEXT NOT NULL, kind TEXT NOT NULL, id TEXT NOT NULL,
5048
+ data TEXT NOT NULL, idempotency_key TEXT, updated_at INTEGER NOT NULL, write_token TEXT,
5049
+ lease_until INTEGER, lease_token TEXT,
5050
+ PRIMARY KEY (user_id, kind, id), UNIQUE (user_id, kind, idempotency_key)
5051
+ )`,
5052
+ `CREATE TABLE IF NOT EXISTS cloud_data_record_chunks (
5053
+ user_id TEXT NOT NULL, kind TEXT NOT NULL, id TEXT NOT NULL, chunk_index INTEGER NOT NULL,
5054
+ value TEXT NOT NULL, write_token TEXT NOT NULL,
5055
+ PRIMARY KEY (user_id, kind, id, chunk_index)
5056
+ )`,
5057
+ `CREATE TABLE IF NOT EXISTS cloud_data_owners (
5058
+ user_id TEXT NOT NULL, owner_type TEXT NOT NULL, owner_id TEXT NOT NULL,
5059
+ active INTEGER NOT NULL, generation INTEGER NOT NULL, updated_at INTEGER NOT NULL,
5060
+ PRIMARY KEY (user_id, owner_type, owner_id)
5061
+ )`,
5062
+ `CREATE TABLE IF NOT EXISTS cloud_resource_metadata (
5063
+ user_id TEXT NOT NULL, resource_key TEXT NOT NULL, encrypted_value TEXT NOT NULL,
5064
+ PRIMARY KEY (user_id, resource_key)
5065
+ )`,
5066
+ // The empty table is a transactional assertion: inserting 0 aborts the batch.
5067
+ `CREATE TABLE IF NOT EXISTS cloud_guard_assertions (
5068
+ allowed INTEGER NOT NULL DEFAULT 1 CONSTRAINT cloud_owner_guard CHECK (allowed = 1),
5069
+ metadata_allowed INTEGER CONSTRAINT cloud_metadata_guard CHECK (metadata_allowed = 1),
5070
+ lease_allowed INTEGER CONSTRAINT cloud_lease_guard CHECK (lease_allowed = 1)
5071
+ )`
5072
+ ];
4561
5073
  function parseTableName(sql) {
4562
5074
  const match = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([A-Za-z_][A-Za-z0-9_]*)/i.exec(sql);
4563
5075
  return match ? match[1] : "";
@@ -4590,7 +5102,8 @@ var SQLITE_REQUIRED_SCHEMA = Object.freeze({
4590
5102
  describeTable(CLIENT_STATE_TABLE_SQL),
4591
5103
  describeTable(PUSH_SUBSCRIPTION_TABLE_SQL),
4592
5104
  describeTable(LLM_CREDENTIALS_TABLE_SQL),
4593
- describeTable(MESSAGE_OUTBOX_TABLE_SQL)
5105
+ describeTable(MESSAGE_OUTBOX_TABLE_SQL),
5106
+ ...CLOUD_DATA_TABLES_SQL.map(describeTable)
4594
5107
  ])),
4595
5108
  indexes: Object.freeze(SQLITE_ALL_INDEXES.filter((index) => index.critical).map((index) => index.name))
4596
5109
  });
@@ -4691,12 +5204,350 @@ var D1Adapter = class {
4691
5204
  const results = await this._db.batch(statements);
4692
5205
  return results.reduce((n, res) => n + (res.meta.changes || 0), 0);
4693
5206
  }
5207
+ /** A complete management implementation needs real transactional batches. */
5208
+ get cloudDataManagement() {
5209
+ return typeof this._db.batch === "function";
5210
+ }
5211
+ _cloudGuardPredicate(userId, guard) {
5212
+ if (!_optionalChain([guard, 'optionalAccess', _42 => _42.owner]) || !Number.isSafeInteger(guard.generation) || guard.generation < 0) {
5213
+ throw new Error("Invalid cloud owner guard");
5214
+ }
5215
+ return {
5216
+ sql: `COALESCE((SELECT active = 1 AND generation = ? FROM cloud_data_owners
5217
+ WHERE user_id = ? AND owner_type = ? AND owner_id = ?), ? = 0)`,
5218
+ args: [guard.generation, userId, guard.owner.type, guard.owner.id, guard.generation]
5219
+ };
5220
+ }
5221
+ async _cloudBatch(statements, userId, guards = [], metadata = /* @__PURE__ */ new Map()) {
5222
+ const assertions = guards.filter(Boolean).map((guard) => {
5223
+ const condition = this._cloudGuardPredicate(userId, guard);
5224
+ return this._db.prepare(`INSERT INTO cloud_guard_assertions (allowed)
5225
+ SELECT 0 WHERE NOT (${condition.sql})`).bind(...condition.args);
5226
+ });
5227
+ for (const item of metadata.values()) {
5228
+ if (Object.hasOwn(item, "expected")) assertions.push(this._db.prepare(
5229
+ `INSERT INTO cloud_guard_assertions (metadata_allowed) SELECT 0 WHERE NOT (
5230
+ (SELECT encrypted_value FROM cloud_resource_metadata WHERE user_id=? AND resource_key=?) IS ?)`
5231
+ ).bind(userId, item.key, item.expected));
5232
+ }
5233
+ const expanded = [...assertions];
5234
+ const indexes = [];
5235
+ for (let i = 0; i < statements.length; i++) {
5236
+ indexes.push(expanded.length);
5237
+ expanded.push(statements[i]);
5238
+ const item = metadata.get(i);
5239
+ if (_optionalChain([item, 'optionalAccess', _43 => _43.value])) {
5240
+ expanded.push(this._db.prepare(
5241
+ `INSERT INTO cloud_resource_metadata
5242
+ (user_id, resource_key, encrypted_value) SELECT ?, ?, ? WHERE changes() > 0
5243
+ ON CONFLICT (user_id, resource_key) DO UPDATE SET encrypted_value = excluded.encrypted_value`
5244
+ ).bind(userId, item.key, item.value));
5245
+ }
5246
+ }
5247
+ if (expanded.length === 0) return [];
5248
+ if (expanded.length === 1 && assertions.length === 0) return [await expanded[0].run()];
5249
+ if ((assertions.length || expanded.length !== statements.length) && !this.cloudDataManagement) {
5250
+ throw new Error("Cloud management requires transactional batches");
5251
+ }
5252
+ try {
5253
+ let results;
5254
+ if (this.cloudDataManagement) results = await this._db.batch(expanded);
5255
+ else {
5256
+ results = [];
5257
+ for (const stmt of expanded) results.push(await stmt.run());
5258
+ }
5259
+ return indexes.map((index) => results[index]);
5260
+ } catch (error) {
5261
+ if (/cloud_metadata_guard/.test(error.message || "")) {
5262
+ throw Object.assign(new Error("Cloud resource ownership changed"), { code: "CLOUD_RESOURCE_CHANGED" });
5263
+ }
5264
+ if (/cloud_owner_guard/.test(error.message || "")) {
5265
+ const denied = new Error("Cloud owner is retired or generation changed");
5266
+ denied.code = "CLOUD_OWNER_RETIRED";
5267
+ throw denied;
5268
+ }
5269
+ throw error;
5270
+ }
5271
+ }
5272
+ async getCloudResourceMetadata(userId, resourceKey) {
5273
+ const row = await this._db.prepare("SELECT encrypted_value FROM cloud_resource_metadata WHERE user_id=? AND resource_key=?").bind(userId, resourceKey).first();
5274
+ return _nullishCoalesce(_optionalChain([row, 'optionalAccess', _44 => _44.encrypted_value]), () => ( null));
5275
+ }
5276
+ async listCloudResourceRows(userId) {
5277
+ const tables = { task: "scheduled_messages", state: "client_state", credential: "llm_credentials", outbox: "message_outbox", subscription: "push_subscriptions", metadata: "cloud_resource_metadata" };
5278
+ const result = { gaps: [] };
5279
+ await Promise.all(Object.entries(tables).map(async ([type, table]) => {
5280
+ try {
5281
+ const response = await this._db.prepare(`SELECT * FROM ${table} WHERE user_id = ?`).bind(userId).all();
5282
+ result[type] = response.results || [];
5283
+ } catch (e4) {
5284
+ result[type] = [];
5285
+ result.gaps.push({ source: type, code: "READ_FAILED", message: "Cloud storage source could not be read" });
5286
+ }
5287
+ }));
5288
+ return result;
5289
+ }
5290
+ async _cloudRecord(row) {
5291
+ if (!row) return null;
5292
+ let data = row.data;
5293
+ const marker = /^\u001famsg-record\u001f([1-9][0-9]*)$/.exec(data);
5294
+ if (marker) {
5295
+ const result = await this._db.prepare(
5296
+ `SELECT chunk_index,value FROM cloud_data_record_chunks
5297
+ WHERE user_id=? AND kind=? AND id=? AND write_token=? ORDER BY chunk_index`
5298
+ ).bind(row.user_id, row.kind, row.id, row.write_token).all();
5299
+ const chunks = result.results || [];
5300
+ if (chunks.length !== Number(marker[1]) || chunks.some((chunk, index) => chunk.chunk_index !== index)) {
5301
+ throw Object.assign(new Error("Cloud management record chunks are incomplete"), { code: "CLOUD_RECORD_INCOMPLETE" });
5302
+ }
5303
+ data = chunks.map((chunk) => chunk.value).join("");
5304
+ }
5305
+ return { id: row.id, userId: row.user_id, kind: row.kind, data, updatedAt: row.updated_at };
5306
+ }
5307
+ async putCloudDataRecord(userId, kind, id, data, { idempotencyKey = null, createOnly = false, leaseToken } = {}) {
5308
+ const slices = [];
5309
+ if (data.length > 200 * 1024) for (let offset = 0; offset < data.length; offset += 200 * 1024) slices.push(data.slice(offset, offset + 200 * 1024));
5310
+ const rootValue = slices.length ? "amsg-record" + slices.length : data;
5311
+ const writeToken = globalThis.crypto.randomUUID();
5312
+ const now = Date.now();
5313
+ const statements = [];
5314
+ if (leaseToken !== void 0) {
5315
+ statements.push(this._db.prepare(
5316
+ `UPDATE cloud_data_records SET data=?,updated_at=?,write_token=?
5317
+ WHERE user_id=? AND kind=? AND id=? AND lease_token=? AND lease_until>?`
5318
+ ).bind(rootValue, now, writeToken, userId, kind, id, leaseToken, now));
5319
+ } else {
5320
+ const conflict = idempotencyKey || createOnly ? "DO NOTHING" : "DO UPDATE SET data=excluded.data,updated_at=excluded.updated_at,write_token=excluded.write_token";
5321
+ statements.push(this._db.prepare(`INSERT INTO cloud_data_records (user_id,kind,id,data,idempotency_key,updated_at,write_token)
5322
+ VALUES (?,?,?,?,?,?,?) ON CONFLICT ${conflict}`).bind(userId, kind, id, rootValue, idempotencyKey, now, writeToken));
5323
+ }
5324
+ const rootGate = "EXISTS (SELECT 1 FROM cloud_data_records WHERE user_id=? AND kind=? AND id=? AND write_token=?)";
5325
+ statements.push(this._db.prepare(
5326
+ `DELETE FROM cloud_data_record_chunks WHERE user_id=? AND kind=? AND id=? AND ${rootGate}`
5327
+ ).bind(userId, kind, id, userId, kind, id, writeToken));
5328
+ for (let index = 0; index < slices.length; index++) {
5329
+ statements.push(this._db.prepare(
5330
+ `INSERT INTO cloud_data_record_chunks (user_id,kind,id,chunk_index,value,write_token)
5331
+ SELECT ?,?,?,?,?,? WHERE ${rootGate}`
5332
+ ).bind(userId, kind, id, index, slices[index], writeToken, userId, kind, id, writeToken));
5333
+ }
5334
+ if (!this.cloudDataManagement) throw new Error("Cloud management requires transactional batches");
5335
+ const results = await this._db.batch(statements);
5336
+ if (leaseToken !== void 0 && !results[0].meta.changes) throw Object.assign(new Error("Cloud operation processing lease was lost"), { code: "CLOUD_LEASE_LOST" });
5337
+ if (idempotencyKey) {
5338
+ return this._cloudRecord(await this._db.prepare(`SELECT * FROM cloud_data_records WHERE user_id=? AND kind=? AND idempotency_key=?`).bind(userId, kind, idempotencyKey).first());
5339
+ }
5340
+ return this.getCloudDataRecord(userId, kind, id);
5341
+ }
5342
+ async getCloudDataRecord(userId, kind, id) {
5343
+ return this._cloudRecord(await this._db.prepare("SELECT * FROM cloud_data_records WHERE user_id=? AND kind=? AND id=?").bind(userId, kind, id).first());
5344
+ }
5345
+ async getCloudDataRecordByIdempotency(userId, kind, idempotencyKey) {
5346
+ return this._cloudRecord(await this._db.prepare("SELECT * FROM cloud_data_records WHERE user_id=? AND kind=? AND idempotency_key=?").bind(userId, kind, idempotencyKey).first());
5347
+ }
5348
+ async _cloudRecordForList(row) {
5349
+ try {
5350
+ return await this._cloudRecord(row);
5351
+ } catch (error) {
5352
+ return {
5353
+ id: row.id,
5354
+ userId: row.user_id,
5355
+ kind: row.kind,
5356
+ data: null,
5357
+ error: error.code === "CLOUD_RECORD_INCOMPLETE" ? error.code : "CLOUD_RECORD_READ_FAILED",
5358
+ updatedAt: row.updated_at
5359
+ };
5360
+ }
5361
+ }
5362
+ async listCloudDataRecords(userId, kind) {
5363
+ const rows = await this._db.prepare("SELECT * FROM cloud_data_records WHERE user_id=? AND kind=? ORDER BY updated_at DESC, id").bind(userId, kind).all();
5364
+ return Promise.all((rows.results || []).map((row) => this._cloudRecordForList(row)));
5365
+ }
5366
+ async listCloudDataRecordsAcrossUsers(kind) {
5367
+ const rows = await this._db.prepare("SELECT * FROM cloud_data_records WHERE kind=? ORDER BY updated_at, id").bind(kind).all();
5368
+ return Promise.all((rows.results || []).map((row) => this._cloudRecordForList(row)));
5369
+ }
5370
+ async claimCloudDataRecord(userId, kind, id, leaseMs, leaseToken = null) {
5371
+ const now = Date.now();
5372
+ const result = await this._db.prepare(
5373
+ `UPDATE cloud_data_records SET lease_until=?,lease_token=?
5374
+ WHERE user_id=? AND kind=? AND id=? AND (lease_until IS NULL OR lease_until <= ?)`
5375
+ ).bind(now + leaseMs, leaseToken, userId, kind, id, now).run();
5376
+ return result.meta.changes > 0;
5377
+ }
5378
+ async releaseCloudDataRecord(userId, kind, id, leaseToken) {
5379
+ const tokenGate = leaseToken === void 0 ? "" : " AND lease_token=?";
5380
+ const params = [userId, kind, id, ...leaseToken === void 0 ? [] : [leaseToken]];
5381
+ await this._db.prepare(`UPDATE cloud_data_records SET lease_until=NULL,lease_token=NULL WHERE user_id=? AND kind=? AND id=?${tokenGate}`).bind(...params).run();
5382
+ }
5383
+ async renewCloudDataRecordLease(userId, kind, id, leaseMs, leaseToken) {
5384
+ const now = Date.now();
5385
+ const result = await this._db.prepare(
5386
+ `UPDATE cloud_data_records SET lease_until=?
5387
+ WHERE user_id=? AND kind=? AND id=? AND lease_token=? AND lease_until>?`
5388
+ ).bind(now + leaseMs, userId, kind, id, leaseToken, now).run();
5389
+ return result.meta.changes > 0;
5390
+ }
5391
+ async deleteCloudDataRecord(userId, kind, id) {
5392
+ if (!this.cloudDataManagement) throw new Error("Cloud management requires transactional batches");
5393
+ const results = await this._db.batch([
5394
+ this._db.prepare("DELETE FROM cloud_data_record_chunks WHERE user_id=? AND kind=? AND id=?").bind(userId, kind, id),
5395
+ this._db.prepare("DELETE FROM cloud_data_records WHERE user_id=? AND kind=? AND id=?").bind(userId, kind, id)
5396
+ ]);
5397
+ return results[1].meta.changes > 0;
5398
+ }
5399
+ async cleanupCloudDataRecords(kind, beforeMs) {
5400
+ if (!this.cloudDataManagement) throw new Error("Cloud management requires transactional batches");
5401
+ const now = Date.now();
5402
+ const results = await this._db.batch([
5403
+ this._db.prepare(`DELETE FROM cloud_data_record_chunks AS c WHERE c.kind=? AND EXISTS
5404
+ (SELECT 1 FROM cloud_data_records r WHERE r.user_id=c.user_id AND r.kind=c.kind AND r.id=c.id
5405
+ AND r.updated_at < ? AND (r.lease_until IS NULL OR r.lease_until <= ?))`).bind(kind, beforeMs, now),
5406
+ this._db.prepare(`DELETE FROM cloud_data_records WHERE kind=? AND updated_at < ?
5407
+ AND (lease_until IS NULL OR lease_until <= ?)`).bind(kind, beforeMs, now)
5408
+ ]);
5409
+ return results[1].meta.changes || 0;
5410
+ }
5411
+ /** Sidecars are indexes only. Remove them only while their backing row is absent. */
5412
+ async cleanupCloudResourceMetadata(userId = null) {
5413
+ const targets = {
5414
+ task: `SELECT 1 FROM scheduled_messages r WHERE r.user_id=m.user_id AND r.uuid=json_extract(m.resource_key,'$[1]')`,
5415
+ state: `SELECT 1 FROM client_state r WHERE r.user_id=m.user_id AND
5416
+ ((r.namespace=json_extract(m.resource_key,'$[1]') AND r.key=json_extract(m.resource_key,'$[2]')) OR
5417
+ (r.namespace=char(31)||'amsg-chunks'||char(31)||json_extract(m.resource_key,'$[1]')
5418
+ AND r.key >= json_extract(m.resource_key,'$[2]')||char(31)
5419
+ AND r.key < json_extract(m.resource_key,'$[2]')||char(32)))`,
5420
+ credential: `SELECT 1 FROM llm_credentials r WHERE r.user_id=m.user_id AND r.cred_id=json_extract(m.resource_key,'$[1]')`,
5421
+ outbox: `SELECT 1 FROM message_outbox r WHERE r.user_id=m.user_id AND r.message_id=json_extract(m.resource_key,'$[1]')`,
5422
+ subscription: `SELECT 1 FROM push_subscriptions r WHERE r.user_id=m.user_id`
5423
+ };
5424
+ const statements = Object.entries(targets).map(([type, exists]) => this._db.prepare(
5425
+ `DELETE FROM cloud_resource_metadata AS m WHERE (? IS NULL OR user_id=?) AND json_valid(resource_key)
5426
+ AND json_extract(resource_key,'$[0]')=? AND NOT EXISTS (${exists})`
5427
+ ).bind(userId, userId, type));
5428
+ const results = await this._cloudBatch(statements, userId);
5429
+ return results.reduce((sum, result) => sum + (result.meta.changes || 0), 0);
5430
+ }
5431
+ async listCloudOwners(userId) {
5432
+ const response = await this._db.prepare("SELECT owner_type,owner_id,active,generation,updated_at FROM cloud_data_owners WHERE user_id=? ORDER BY owner_type,owner_id").bind(userId).all();
5433
+ return (response.results || []).map((row) => ({ owner: { type: row.owner_type, id: row.owner_id }, active: !!row.active, generation: row.generation, updatedAt: row.updated_at }));
5434
+ }
5435
+ async getCloudOwner(userId, owner) {
5436
+ const row = await this._db.prepare("SELECT active,generation,updated_at FROM cloud_data_owners WHERE user_id=? AND owner_type=? AND owner_id=?").bind(userId, owner.type, owner.id).first();
5437
+ return row ? { active: !!row.active, generation: row.generation, updatedAt: row.updated_at } : { active: true, generation: 0, updatedAt: null };
5438
+ }
5439
+ async setCloudOwnerActive(userId, owner, active, expectedGeneration) {
5440
+ const statements = [];
5441
+ if (expectedGeneration !== void 0) {
5442
+ if (!Number.isSafeInteger(expectedGeneration) || expectedGeneration < 0) throw new Error("Invalid expected owner generation");
5443
+ statements.push(this._db.prepare(
5444
+ `INSERT INTO cloud_guard_assertions (allowed) SELECT 0 WHERE
5445
+ COALESCE((SELECT generation FROM cloud_data_owners WHERE user_id=? AND owner_type=? AND owner_id=?),0) <> ?`
5446
+ ).bind(userId, owner.type, owner.id, expectedGeneration));
5447
+ }
5448
+ statements.push(this._db.prepare(
5449
+ `INSERT INTO cloud_data_owners (user_id,owner_type,owner_id,active,generation,updated_at)
5450
+ VALUES (?,?,?,?,1,?) ON CONFLICT (user_id,owner_type,owner_id) DO UPDATE SET
5451
+ active=excluded.active,generation=cloud_data_owners.generation+1,updated_at=excluded.updated_at`
5452
+ ).bind(userId, owner.type, owner.id, active ? 1 : 0, Date.now()));
5453
+ statements.push(this._db.prepare("SELECT active,generation,updated_at FROM cloud_data_owners WHERE user_id=? AND owner_type=? AND owner_id=?").bind(userId, owner.type, owner.id));
5454
+ try {
5455
+ if (!this.cloudDataManagement) throw new Error("Cloud management requires transactional batches");
5456
+ const results = await this._db.batch(statements);
5457
+ const row = results[results.length - 1].results[0];
5458
+ return { active: !!row.active, generation: row.generation, updatedAt: row.updated_at };
5459
+ } catch (error) {
5460
+ if (/cloud_owner_guard/.test(error.message || "")) throw Object.assign(new Error("Cloud owner changed; refresh before restoring"), { code: "CLOUD_OWNER_CHANGED" });
5461
+ throw error;
5462
+ }
5463
+ }
5464
+ async isCloudOwnerGuardValid(userId, guard) {
5465
+ if (!guard) return true;
5466
+ const condition = this._cloudGuardPredicate(userId, guard);
5467
+ const row = await this._db.prepare(`SELECT ${condition.sql} AS valid`).bind(...condition.args).first();
5468
+ return !!_optionalChain([row, 'optionalAccess', _45 => _45.valid]);
5469
+ }
5470
+ /** Delete only the exact physical snapshot; no untrusted SQL identifiers. */
5471
+ async deleteCloudResourceRows(userId, locators, { operationId, leaseToken } = {}) {
5472
+ const specs = {
5473
+ task: { table: "scheduled_messages", keys: ["user_id", "id"], columns: ["id", "user_id", "uuid", "encrypted_payload", "message_type", "next_send_at", "lease_until", "retry_after", "serialize_group", "status", "retry_count", "last_error", "created_at", "updated_at"] },
5474
+ state: { table: "client_state", keys: ["user_id", "namespace", "key"], columns: ["user_id", "namespace", "key", "value", "updated_at"] },
5475
+ credential: { table: "llm_credentials", keys: ["user_id", "cred_id"], columns: ["user_id", "cred_id", "encrypted_value", "created_at", "updated_at"] },
5476
+ outbox: { table: "message_outbox", keys: ["user_id", "message_id"], columns: ["id", "user_id", "message_id", "task_uuid", "session_id", "message_index", "total_messages", "payload", "created_at", "delivered_at", "acked_at"] },
5477
+ subscription: { table: "push_subscriptions", keys: ["user_id"], columns: ["user_id", "subscription", "updated_at"] }
5478
+ };
5479
+ let deleted = 0, changed = 0;
5480
+ for (const locator of locators) {
5481
+ const spec = specs[locator.type];
5482
+ if (!spec || !Array.isArray(locator.rows) || !locator.rows.length || locator.rows.some((row) => row.user_id !== userId)) {
5483
+ changed++;
5484
+ continue;
5485
+ }
5486
+ const statements = [];
5487
+ if (operationId !== void 0) {
5488
+ statements.push(this._db.prepare(
5489
+ `INSERT INTO cloud_guard_assertions (lease_allowed)
5490
+ SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM cloud_data_records WHERE user_id=? AND kind='operation'
5491
+ AND id=? AND lease_token=? AND lease_until >
5492
+ CAST(strftime('%s','now') AS INTEGER)*1000 + CAST(substr(strftime('%f','now'),4,3) AS INTEGER))`
5493
+ ).bind(userId, operationId, _nullishCoalesce(leaseToken, () => ( null))));
5494
+ }
5495
+ if (locator.metadataKey && Object.hasOwn(locator, "encryptedMetadata")) {
5496
+ const identityChecks = locator.rows.map((row) => ({
5497
+ sql: `EXISTS (SELECT 1 FROM ${spec.table} WHERE ${spec.keys.map((column) => `${column} IS ?`).join(" AND ")})`,
5498
+ args: spec.keys.map((column) => row[column])
5499
+ }));
5500
+ for (const check of identityChecks) statements.push(this._db.prepare(
5501
+ `INSERT INTO cloud_guard_assertions (allowed) SELECT 0 WHERE ${check.sql}
5502
+ AND NOT ((SELECT encrypted_value FROM cloud_resource_metadata WHERE user_id=? AND resource_key=?) IS ?)`
5503
+ ).bind(...check.args, userId, locator.metadataKey, locator.encryptedMetadata));
5504
+ }
5505
+ if (locator.type === "state" && locator.logicalState) {
5506
+ const { namespace, key } = locator.logicalState;
5507
+ const identities = JSON.stringify(locator.rows.map((row) => ({ namespace: row.namespace, key: row.key })));
5508
+ statements.push(this._db.prepare(
5509
+ `INSERT INTO cloud_guard_assertions (allowed)
5510
+ SELECT 0 WHERE EXISTS (SELECT 1 FROM client_state r WHERE r.user_id=? AND
5511
+ ((r.namespace=? AND r.key=?) OR (r.namespace=? AND r.key>=? AND r.key<?))
5512
+ AND NOT EXISTS (SELECT 1 FROM json_each(?) e WHERE
5513
+ r.namespace=json_extract(e.value,'$.namespace') AND r.key=json_extract(e.value,'$.key')))`
5514
+ ).bind(userId, namespace, key, "amsg-chunks" + namespace, key + "", key + " ", identities));
5515
+ }
5516
+ for (const row of locator.rows) {
5517
+ const columns = spec.columns.filter((column) => Object.hasOwn(row, column));
5518
+ const predicate = columns.map((column) => `${column} IS ?`).join(" AND ");
5519
+ const values = columns.map((column) => row[column]);
5520
+ const identity = spec.keys.map((column) => `${column} IS ?`).join(" AND ");
5521
+ statements.push(this._db.prepare(
5522
+ `INSERT INTO cloud_guard_assertions (allowed)
5523
+ SELECT 0 WHERE EXISTS (SELECT 1 FROM ${spec.table} WHERE ${identity})
5524
+ AND NOT EXISTS (SELECT 1 FROM ${spec.table} WHERE ${predicate})`
5525
+ ).bind(...spec.keys.map((column) => row[column]), ...values));
5526
+ }
5527
+ for (const row of locator.rows) {
5528
+ const columns = spec.columns.filter((column) => Object.hasOwn(row, column));
5529
+ statements.push(this._db.prepare(`DELETE FROM ${spec.table} WHERE ${columns.map((column) => `${column} IS ?`).join(" AND ")}`).bind(...columns.map((column) => row[column])));
5530
+ }
5531
+ if (locator.metadataKey) statements.push(this._db.prepare("DELETE FROM cloud_resource_metadata WHERE user_id=? AND resource_key=?").bind(userId, locator.metadataKey));
5532
+ try {
5533
+ if (!this.cloudDataManagement) throw new Error("Cloud management requires transactional batches");
5534
+ await this._db.batch(statements);
5535
+ deleted++;
5536
+ } catch (error) {
5537
+ if (/cloud_lease_guard/.test(error.message || "")) throw Object.assign(new Error("Cloud operation processing lease was lost"), { code: "CLOUD_LEASE_LOST" });
5538
+ if (/cloud_owner_guard/.test(error.message || "")) changed++;
5539
+ else throw error;
5540
+ }
5541
+ }
5542
+ return { deleted, changed };
5543
+ }
4694
5544
  async initSchema() {
4695
5545
  await this._db.prepare(SQLITE_TABLE_SQL).run();
4696
5546
  await this._db.prepare(CLIENT_STATE_TABLE_SQL).run();
4697
5547
  await this._db.prepare(PUSH_SUBSCRIPTION_TABLE_SQL).run();
4698
5548
  await this._db.prepare(LLM_CREDENTIALS_TABLE_SQL).run();
4699
5549
  await this._db.prepare(MESSAGE_OUTBOX_TABLE_SQL).run();
5550
+ for (const sql of CLOUD_DATA_TABLES_SQL) await this._db.prepare(sql).run();
4700
5551
  for (const migration of SQLITE_MIGRATIONS) {
4701
5552
  try {
4702
5553
  await this._db.prepare(migration.sql).run();
@@ -4761,15 +5612,21 @@ var D1Adapter = class {
4761
5612
  await this._db.prepare("DROP TABLE IF EXISTS push_subscriptions").run();
4762
5613
  await this._db.prepare("DROP TABLE IF EXISTS llm_credentials").run();
4763
5614
  await this._db.prepare("DROP TABLE IF EXISTS message_outbox").run();
5615
+ for (const table of ["cloud_data_record_chunks", "cloud_data_records", "cloud_data_owners", "cloud_resource_metadata", "cloud_guard_assertions"]) {
5616
+ await this._db.prepare(`DROP TABLE IF EXISTS ${table}`).run();
5617
+ }
4764
5618
  }
4765
- async createTask(params) {
5619
+ async createTask(params, cloudGuard = params.cloudGuard) {
4766
5620
  const now = this._now();
4767
5621
  const nextSendAt = this._iso(params.next_send_at);
4768
- const res = await this._db.prepare(
5622
+ const stmt = this._db.prepare(
4769
5623
  `INSERT INTO scheduled_messages
4770
5624
  (user_id, uuid, encrypted_payload, next_send_at, message_type, status, retry_count, created_at, updated_at)
4771
5625
  VALUES (?, ?, ?, ?, ?, 'pending', 0, ?, ?)`
4772
- ).bind(params.user_id, params.uuid, params.encrypted_payload, nextSendAt, params.message_type, now, now).run();
5626
+ ).bind(params.user_id, params.uuid, params.encrypted_payload, nextSendAt, params.message_type, now, now);
5627
+ const [res] = await this._cloudBatch([stmt], params.user_id, [cloudGuard], /* @__PURE__ */ new Map([
5628
+ [0, { key: JSON.stringify(["task", params.uuid]), value: params.encryptedCloudMetadata, ...Object.hasOwn(params, "expectedCloudMetadata") ? { expected: params.expectedCloudMetadata } : {} }]
5629
+ ]));
4773
5630
  const id = res.meta.last_row_id;
4774
5631
  return this._db.prepare(
4775
5632
  `SELECT id, uuid, next_send_at, status, created_at FROM scheduled_messages WHERE id = ?`
@@ -4787,7 +5644,7 @@ var D1Adapter = class {
4787
5644
  * @returns {Promise<Object>} createTask 的返回行 + `superseded`(旧行是否
4788
5645
  * 真的被删掉;false = 旧行本就不存在)
4789
5646
  */
4790
- async createTaskSuperseding(params, supersedesUuid) {
5647
+ async createTaskSuperseding(params, supersedesUuid, cloudGuard = params.cloudGuard) {
4791
5648
  const now = this._now();
4792
5649
  const nextSendAt = this._iso(params.next_send_at);
4793
5650
  const statements = [
@@ -4800,13 +5657,9 @@ var D1Adapter = class {
4800
5657
  VALUES (?, ?, ?, ?, ?, 'pending', 0, ?, ?)`
4801
5658
  ).bind(params.user_id, params.uuid, params.encrypted_payload, nextSendAt, params.message_type, now, now)
4802
5659
  ];
4803
- let results;
4804
- if (typeof this._db.batch === "function") {
4805
- results = await this._db.batch(statements);
4806
- } else {
4807
- results = [];
4808
- for (const stmt of statements) results.push(await stmt.run());
4809
- }
5660
+ const results = await this._cloudBatch(statements, params.user_id, [cloudGuard], /* @__PURE__ */ new Map([
5661
+ [1, { key: JSON.stringify(["task", params.uuid]), value: params.encryptedCloudMetadata, ...Object.hasOwn(params, "expectedCloudMetadata") ? { expected: params.expectedCloudMetadata } : {} }]
5662
+ ]));
4810
5663
  const superseded = (results[0].meta.changes || 0) > 0;
4811
5664
  const id = results[1].meta.last_row_id;
4812
5665
  const row = await this._db.prepare(
@@ -4843,7 +5696,7 @@ var D1Adapter = class {
4843
5696
  ).bind(uuid).first();
4844
5697
  return row ? { status: row.status } : null;
4845
5698
  }
4846
- async updateTaskById(taskId, updates) {
5699
+ async updateTaskById(taskId, updates, cloudGuard = null) {
4847
5700
  const sets = [];
4848
5701
  const values = [];
4849
5702
  for (const [key, value] of Object.entries(updates)) {
@@ -4858,9 +5711,12 @@ var D1Adapter = class {
4858
5711
  values.push(this._now());
4859
5712
  }
4860
5713
  values.push(taskId);
4861
- await this._db.prepare(
5714
+ const statement = this._db.prepare(
4862
5715
  `UPDATE scheduled_messages SET ${sets.join(", ")} WHERE id = ?`
4863
- ).bind(...values).run();
5716
+ ).bind(...values);
5717
+ const ownerUser = cloudGuard ? await this._db.prepare("SELECT user_id FROM scheduled_messages WHERE id=?").bind(taskId).first() : null;
5718
+ if (cloudGuard && !ownerUser) return null;
5719
+ await this._cloudBatch([statement], _optionalChain([ownerUser, 'optionalAccess', _46 => _46.user_id]), [cloudGuard]);
4864
5720
  return this._db.prepare("SELECT * FROM scheduled_messages WHERE id = ?").bind(taskId).first();
4865
5721
  }
4866
5722
  /**
@@ -4872,7 +5728,7 @@ var D1Adapter = class {
4872
5728
  * ——用户以为改期生效了,实际什么都没留下。正文这类字段不受这道门约束:它们
4873
5729
  * 只影响以后的触发,收尾那边本来就不会覆盖(见 run-tick 的收尾守卫)。
4874
5730
  */
4875
- async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
5731
+ async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields, cloudGuard = null) {
4876
5732
  const now = this._now();
4877
5733
  const sets = ["encrypted_payload = ?", "updated_at = ?"];
4878
5734
  const values = [encryptedPayload, now];
@@ -4891,10 +5747,11 @@ var D1Adapter = class {
4891
5747
  leaseGate = " AND (lease_until IS NULL OR lease_until <= ?)";
4892
5748
  values.push(now);
4893
5749
  }
4894
- const res = await this._db.prepare(
5750
+ const statement = this._db.prepare(
4895
5751
  `UPDATE scheduled_messages SET ${sets.join(", ")}
4896
5752
  WHERE uuid = ? AND user_id = ? AND status = 'pending'${leaseGate}`
4897
- ).bind(...values).run();
5753
+ ).bind(...values);
5754
+ const [res] = await this._cloudBatch([statement], userId, [cloudGuard]);
4898
5755
  if (!res.meta.changes) return null;
4899
5756
  return { uuid, updated_at: now };
4900
5757
  }
@@ -4955,7 +5812,7 @@ var D1Adapter = class {
4955
5812
  * @returns {Promise<boolean>} true = 领到了;false = 别人正拿着租约、同分组
4956
5813
  * 有任务正在跑、排期被改过、或行已不是 pending
4957
5814
  */
4958
- async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
5815
+ async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null, cloudGuard = null) {
4959
5816
  const expected = typeof expectedNextSendAt === "string" ? expectedNextSendAt : this._iso(expectedNextSendAt);
4960
5817
  const grouped = typeof serializeGroup === "string" && serializeGroup.length > 0;
4961
5818
  const now = this._now();
@@ -4979,9 +5836,12 @@ var D1Adapter = class {
4979
5836
  )`;
4980
5837
  values.push(serializeGroup, taskId, now);
4981
5838
  }
4982
- const res = await this._db.prepare(
5839
+ const statement = this._db.prepare(
4983
5840
  `UPDATE scheduled_messages SET ${sets.join(", ")} WHERE ${where}`
4984
- ).bind(...values).run();
5841
+ ).bind(...values);
5842
+ const ownerUser = cloudGuard ? await this._db.prepare("SELECT user_id FROM scheduled_messages WHERE id=?").bind(taskId).first() : null;
5843
+ if (cloudGuard && !ownerUser) return false;
5844
+ const [res] = await this._cloudBatch([statement], _optionalChain([ownerUser, 'optionalAccess', _47 => _47.user_id]), [cloudGuard]);
4985
5845
  return (res.meta.changes || 0) > 0;
4986
5846
  }
4987
5847
  /**
@@ -4993,11 +5853,14 @@ var D1Adapter = class {
4993
5853
  * @param {string|Date} leaseUntil - 新的租期末尾
4994
5854
  * @returns {Promise<boolean>} true = 续上了
4995
5855
  */
4996
- async renewTaskLease(taskId, leaseUntil) {
4997
- const res = await this._db.prepare(
5856
+ async renewTaskLease(taskId, leaseUntil, cloudGuard = null) {
5857
+ const statement = this._db.prepare(
4998
5858
  `UPDATE scheduled_messages SET lease_until = ?
4999
5859
  WHERE id = ? AND status = 'pending' AND lease_until IS NOT NULL`
5000
- ).bind(this._iso(leaseUntil), taskId).run();
5860
+ ).bind(this._iso(leaseUntil), taskId);
5861
+ const ownerUser = cloudGuard ? await this._db.prepare("SELECT user_id FROM scheduled_messages WHERE id=?").bind(taskId).first() : null;
5862
+ if (cloudGuard && !ownerUser) return false;
5863
+ const [res] = await this._cloudBatch([statement], _optionalChain([ownerUser, 'optionalAccess', _48 => _48.user_id]), [cloudGuard]);
5001
5864
  return (res.meta.changes || 0) > 0;
5002
5865
  }
5003
5866
  async listTasks(userId, opts = {}) {
@@ -5094,7 +5957,7 @@ var D1Adapter = class {
5094
5957
  * 回 `true` = 这个 key 的行已经不在(删掉了,或本来就没有)、`false` = 行还在
5095
5958
  * (库里那行更新,删除被条件写拦下);前缀形态不探测,回 `null`。
5096
5959
  */
5097
- async upsertClientState(userId, entries, cleanups = [], now = Date.now()) {
5960
+ async upsertClientState(userId, entries, cleanups = [], now = Date.now(), cloudGuard = null) {
5098
5961
  const UPSERT_SQL = `INSERT INTO client_state (user_id, namespace, key, value, updated_at)
5099
5962
  VALUES (?, ?, ?, ?, ?)
5100
5963
  ON CONFLICT (user_id, namespace, key) DO UPDATE SET
@@ -5111,8 +5974,12 @@ var D1Adapter = class {
5111
5974
  const PROBE_KEY_SQL = `SELECT COUNT(*) AS n FROM client_state
5112
5975
  WHERE user_id = ? AND namespace = ? AND key = ?`;
5113
5976
  const statements = [];
5977
+ const metadata = /* @__PURE__ */ new Map();
5114
5978
  const probeIndexes = [];
5115
5979
  for (const c of cleanups) {
5980
+ if (typeof c.cloudMetadataKey === "string" && Object.hasOwn(c, "expectedCloudMetadata")) {
5981
+ metadata.set(statements.length, { key: c.cloudMetadataKey, expected: c.expectedCloudMetadata });
5982
+ }
5116
5983
  if (typeof c.key === "string") {
5117
5984
  statements.push(this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt, now));
5118
5985
  probeIndexes.push(statements.length);
@@ -5130,15 +5997,17 @@ var D1Adapter = class {
5130
5997
  this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt, now)
5131
5998
  );
5132
5999
  }
5133
- let results;
5134
- if (typeof this._db.batch === "function") {
5135
- results = await this._db.batch(statements);
5136
- } else {
5137
- results = [];
5138
- for (const stmt of statements) {
5139
- results.push(await stmt.run());
5140
- }
5141
- }
6000
+ entries.forEach((entry, index) => metadata.set(upsertStart + index, {
6001
+ key: JSON.stringify(["state", entry.namespace, entry.key]),
6002
+ value: entry.encryptedCloudMetadata,
6003
+ ...Object.hasOwn(entry, "expectedCloudMetadata") ? { expected: entry.expectedCloudMetadata } : {}
6004
+ }));
6005
+ const results = await this._cloudBatch(
6006
+ statements,
6007
+ userId,
6008
+ [cloudGuard, ...entries.map((entry) => entry.cloudGuard), ...cleanups.map((entry) => entry.cloudGuard)],
6009
+ metadata
6010
+ );
5142
6011
  const outcomes = results.slice(upsertStart).map((res) => res.meta.changes > 0);
5143
6012
  let upserted = 0;
5144
6013
  let skipped = 0;
@@ -5151,7 +6020,7 @@ var D1Adapter = class {
5151
6020
  result.cleanupOutcomes = probeIndexes.map((probeAt) => {
5152
6021
  if (probeAt === null) return null;
5153
6022
  const row = results[probeAt] && Array.isArray(results[probeAt].results) ? results[probeAt].results[0] : null;
5154
- return Number(_nullishCoalesce(_optionalChain([row, 'optionalAccess', _8 => _8.n]), () => ( 0))) === 0;
6023
+ return Number(_nullishCoalesce(_optionalChain([row, 'optionalAccess', _49 => _49.n]), () => ( 0))) === 0;
5155
6024
  });
5156
6025
  }
5157
6026
  return result;
@@ -5362,7 +6231,7 @@ var D1Adapter = class {
5362
6231
  * @param {Array<{ credId: string, encryptedValue: string }>} entries
5363
6232
  * @returns {Promise<number>} 实际写入/覆盖的行数
5364
6233
  */
5365
- async upsertLlmCredentials(userId, entries) {
6234
+ async upsertLlmCredentials(userId, entries, cloudGuard = null) {
5366
6235
  if (!entries || entries.length === 0) return 0;
5367
6236
  const now = this._now();
5368
6237
  const SQL = `INSERT INTO llm_credentials (user_id, cred_id, encrypted_value, created_at, updated_at)
@@ -5373,13 +6242,12 @@ var D1Adapter = class {
5373
6242
  const statements = entries.map(
5374
6243
  (entry) => this._db.prepare(SQL).bind(userId, entry.credId, entry.encryptedValue, now, now)
5375
6244
  );
5376
- let results;
5377
- if (typeof this._db.batch === "function") {
5378
- results = await this._db.batch(statements);
5379
- } else {
5380
- results = [];
5381
- for (const stmt of statements) results.push(await stmt.run());
5382
- }
6245
+ const metadata = new Map(entries.map((entry, index) => [index, {
6246
+ key: JSON.stringify(["credential", entry.credId]),
6247
+ value: entry.encryptedCloudMetadata,
6248
+ ...Object.hasOwn(entry, "expectedCloudMetadata") ? { expected: entry.expectedCloudMetadata } : {}
6249
+ }]));
6250
+ const results = await this._cloudBatch(statements, userId, [cloudGuard, ...entries.map((entry) => entry.cloudGuard)], metadata);
5383
6251
  return results.reduce((n, res) => n + ((res.meta.changes || 0) > 0 ? 1 : 0), 0);
5384
6252
  }
5385
6253
  /**
@@ -5487,7 +6355,7 @@ var D1Adapter = class {
5487
6355
  * `payload` 是整条 push JSON 的 encryptForStorage 密文。
5488
6356
  * @returns {Promise<number>} 实际写入/更新的行数
5489
6357
  */
5490
- async appendOutboxMessages(userId, rows) {
6358
+ async appendOutboxMessages(userId, rows, cloudGuard = null) {
5491
6359
  if (!rows || rows.length === 0) return 0;
5492
6360
  const SQL = `INSERT INTO message_outbox
5493
6361
  (user_id, message_id, task_uuid, session_id, message_index, total_messages, payload, created_at)
@@ -5509,13 +6377,12 @@ var D1Adapter = class {
5509
6377
  r.created_at
5510
6378
  )
5511
6379
  );
5512
- let results;
5513
- if (typeof this._db.batch === "function") {
5514
- results = await this._db.batch(statements);
5515
- } else {
5516
- results = [];
5517
- for (const stmt of statements) results.push(await stmt.run());
5518
- }
6380
+ const metadata = new Map(rows.map((row, index) => [index, {
6381
+ key: JSON.stringify(["outbox", row.message_id]),
6382
+ value: row.encryptedCloudMetadata,
6383
+ ...Object.hasOwn(row, "expectedCloudMetadata") ? { expected: row.expectedCloudMetadata } : {}
6384
+ }]));
6385
+ const results = await this._cloudBatch(statements, userId, [cloudGuard, ...rows.map((row) => row.cloudGuard)], metadata);
5519
6386
  return results.reduce((n, res) => n + ((res.meta.changes || 0) > 0 ? 1 : 0), 0);
5520
6387
  }
5521
6388
  /**
@@ -6012,7 +6879,7 @@ function createClientStateNamespacesHandler(ctx) {
6012
6879
  }
6013
6880
 
6014
6881
  // src/server/lib/version.js
6015
- var SERVER_VERSION = true ? "2.6.0-next.32" : "0.0.0-dev";
6882
+ var SERVER_VERSION = true ? "2.6.0-next.34" : "0.0.0-dev";
6016
6883
 
6017
6884
  // src/server/handlers/capabilities.js
6018
6885
  var SERVER_FEATURES = Object.freeze([
@@ -6125,7 +6992,10 @@ var SERVER_FEATURES = Object.freeze([
6125
6992
  // 工厂配置认 maxDeliveryRetries(投递失败的重试次数上限,默认 3)。
6126
6993
  "max-delivery-retries",
6127
6994
  // Per-task pre-commit retry limit and fire receipt retry decision.
6128
- "max-generation-retries"
6995
+ "max-generation-retries",
6996
+ // onBeforeFire 认 { defer: { afterMs } }:这次不生成,过一会儿再来问(不占重试
6997
+ // 次数);onFireSettled 的 status 多一种 'deferred',带 retryAfter。
6998
+ "before-fire-defer"
6129
6999
  ]);
6130
7000
  function createCapabilitiesHandler(ctx) {
6131
7001
  async function GET(url, headers) {
@@ -6136,12 +7006,927 @@ function createCapabilitiesHandler(ctx) {
6136
7006
  }
6137
7007
  return {
6138
7008
  status: 200,
6139
- body: { success: true, serverVersion: SERVER_VERSION, features: [...SERVER_FEATURES] }
7009
+ body: { success: true, serverVersion: SERVER_VERSION, features: [...SERVER_FEATURES, ...tenantResult.context.db.cloudDataManagement ? ["cloud-data-management"] : []] }
6140
7010
  };
6141
7011
  }
6142
7012
  return { GET };
6143
7013
  }
6144
7014
 
7015
+ // src/server/lib/cloud-data-inventory.js
7016
+ var encoder = new TextEncoder();
7017
+ async function cloudDataHash(value) {
7018
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(value));
7019
+ return Array.from(
7020
+ new Uint8Array(digest),
7021
+ (byte) => byte.toString(16).padStart(2, "0")
7022
+ ).join("");
7023
+ }
7024
+ function timestamp(value) {
7025
+ if (value == null) return null;
7026
+ const parsed = typeof value === "number" ? value : Date.parse(value);
7027
+ return Number.isFinite(parsed) ? parsed : null;
7028
+ }
7029
+ function safeOwner(owner) {
7030
+ if (!owner || typeof owner.type !== "string" || !owner.type || typeof owner.id !== "string" || !owner.id)
7031
+ return null;
7032
+ return {
7033
+ type: owner.type,
7034
+ id: owner.id,
7035
+ ...typeof owner.label === "string" ? { label: owner.label } : {}
7036
+ };
7037
+ }
7038
+ function objectOrText(value) {
7039
+ try {
7040
+ return JSON.parse(value);
7041
+ } catch (e5) {
7042
+ return value;
7043
+ }
7044
+ }
7045
+ async function buildCloudDataInventory({
7046
+ db,
7047
+ userId,
7048
+ userKey,
7049
+ resolveOwner
7050
+ }) {
7051
+ const raw = await db.listCloudResourceRows(userId);
7052
+ const gaps = [...raw.gaps || []];
7053
+ const entries = [];
7054
+ const metadata = new Map(
7055
+ (raw.metadata || []).map((row) => [row.resource_key, row.encrypted_value])
7056
+ );
7057
+ const taskPayloads = /* @__PURE__ */ new Map();
7058
+ const taskByUuid = /* @__PURE__ */ new Map();
7059
+ for (const row of raw.task || []) {
7060
+ try {
7061
+ const payload = JSON.parse(
7062
+ await decryptFromStorage(row.encrypted_payload, userKey)
7063
+ );
7064
+ taskPayloads.set(row.id, payload);
7065
+ if (row.uuid) taskByUuid.set(row.uuid, payload);
7066
+ } catch (e6) {
7067
+ }
7068
+ }
7069
+ const tasks = (raw.task || []).filter((row) => taskPayloads.has(row.id)).map((row) => ({ ...taskPayloads.get(row.id), uuid: row.uuid }));
7070
+ const add = async ({
7071
+ type,
7072
+ identity,
7073
+ rows,
7074
+ value,
7075
+ payload,
7076
+ task,
7077
+ unreadable = false,
7078
+ status = null,
7079
+ namespace,
7080
+ key,
7081
+ credId
7082
+ }) => {
7083
+ const metadataKey = JSON.stringify([type, ...identity]);
7084
+ const id = await cloudDataHash(JSON.stringify([userId, metadataKey]));
7085
+ let stored = null;
7086
+ const encryptedMetadata = metadata.get(metadataKey);
7087
+ if (encryptedMetadata) {
7088
+ try {
7089
+ stored = JSON.parse(
7090
+ await decryptFromStorage(encryptedMetadata, userKey)
7091
+ );
7092
+ } catch (e7) {
7093
+ unreadable = true;
7094
+ }
7095
+ }
7096
+ let resolved = null;
7097
+ if (resolveOwner) {
7098
+ try {
7099
+ resolved = await resolveOwner({
7100
+ type,
7101
+ namespace,
7102
+ key,
7103
+ credId,
7104
+ value,
7105
+ payload,
7106
+ task,
7107
+ tasks
7108
+ });
7109
+ } catch (e8) {
7110
+ gaps.push({
7111
+ source: type,
7112
+ code: "OWNER_RESOLUTION_FAILED",
7113
+ message: "A resource owner could not be resolved"
7114
+ });
7115
+ }
7116
+ }
7117
+ const owner = safeOwner(_optionalChain([stored, 'optionalAccess', _50 => _50.owner])) || safeOwner(_optionalChain([payload, 'optionalAccess', _51 => _51.__cloudOwnerGuard, 'optionalAccess', _52 => _52.owner])) || safeOwner(_optionalChain([payload, 'optionalAccess', _53 => _53.metadata, 'optionalAccess', _54 => _54.cloudOwner])) || safeOwner(_optionalChain([payload, 'optionalAccess', _55 => _55.metadata, 'optionalAccess', _56 => _56.owner])) || safeOwner(_optionalChain([payload, 'optionalAccess', _57 => _57.owner])) || safeOwner(_optionalChain([task, 'optionalAccess', _58 => _58.__cloudOwnerGuard, 'optionalAccess', _59 => _59.owner])) || safeOwner(_optionalChain([task, 'optionalAccess', _60 => _60.metadata, 'optionalAccess', _61 => _61.cloudOwner])) || safeOwner(_optionalChain([task, 'optionalAccess', _62 => _62.metadata, 'optionalAccess', _63 => _63.owner])) || safeOwner(_optionalChain([task, 'optionalAccess', _64 => _64.owner])) || safeOwner(_optionalChain([resolved, 'optionalAccess', _65 => _65.owner]));
7118
+ const kind = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_optionalChain([stored, 'optionalAccess', _66 => _66.kind]), () => ( _optionalChain([payload, 'optionalAccess', _67 => _67.metadata, 'optionalAccess', _68 => _68.kind]))), () => ( _optionalChain([payload, 'optionalAccess', _69 => _69.kind]))), () => ( _optionalChain([resolved, 'optionalAccess', _70 => _70.kind]))), () => ( null));
7119
+ const times = rows.map((row) => timestamp(_nullishCoalesce(row.updated_at, () => ( row.created_at)))).filter((time) => time !== null);
7120
+ const byteSize = rows.reduce(
7121
+ (sum, row) => sum + encoder.encode(
7122
+ String(
7123
+ _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce(row.value, () => ( row.encrypted_payload)), () => ( row.encrypted_value)), () => ( row.payload)), () => ( row.subscription)), () => ( ""))
7124
+ )
7125
+ ).length,
7126
+ 0
7127
+ );
7128
+ const label = type === "state" ? `${namespace}/${key}` : type === "credential" ? credId : type === "task" ? rows[0].uuid || `task row ${rows[0].id}` : type === "outbox" ? rows[0].message_id : "Web Push subscription";
7129
+ entries.push({
7130
+ resource: {
7131
+ id,
7132
+ type,
7133
+ owner,
7134
+ kind: typeof kind === "string" ? kind : null,
7135
+ label,
7136
+ byteSize,
7137
+ updatedAt: times.length ? Math.max(...times) : null,
7138
+ status: unreadable ? "unreadable" : status
7139
+ },
7140
+ locator: {
7141
+ type,
7142
+ rows,
7143
+ metadataKey,
7144
+ encryptedMetadata: encryptedMetadata || null,
7145
+ ...type === "state" ? { logicalState: { namespace, key } } : {}
7146
+ },
7147
+ version: await cloudDataHash(
7148
+ JSON.stringify({ rows, encryptedMetadata: encryptedMetadata || null })
7149
+ )
7150
+ });
7151
+ };
7152
+ for (const row of raw.task || []) {
7153
+ const payload = taskPayloads.get(row.id);
7154
+ await add({
7155
+ type: "task",
7156
+ identity: [_nullishCoalesce(row.uuid, () => ( ["row", row.id]))],
7157
+ rows: [row],
7158
+ payload,
7159
+ task: payload,
7160
+ unreadable: !payload,
7161
+ status: row.status
7162
+ });
7163
+ }
7164
+ const stateRows = raw.state || [];
7165
+ const roots = stateRows.filter((row) => !isChunkNamespace(row.namespace));
7166
+ const chunks = /* @__PURE__ */ new Map();
7167
+ for (const row of stateRows.filter(
7168
+ (row2) => isChunkNamespace(row2.namespace)
7169
+ )) {
7170
+ const separator = row.key.lastIndexOf("");
7171
+ const namespace = row.namespace.slice(CHUNK_NAMESPACE_PREFIX.length);
7172
+ const key = separator >= 0 ? row.key.slice(0, separator) : row.key;
7173
+ const group = JSON.stringify([namespace, key]);
7174
+ if (!chunks.has(group)) chunks.set(group, []);
7175
+ chunks.get(group).push(row);
7176
+ }
7177
+ for (const row of roots) {
7178
+ const group = JSON.stringify([row.namespace, row.key]);
7179
+ const slices = chunks.get(group) || [];
7180
+ chunks.delete(group);
7181
+ const rows = [row, ...slices.sort((a, b) => a.key.localeCompare(b.key))];
7182
+ let value, unreadable = false;
7183
+ try {
7184
+ const count = parseChunkedRootCount(row.value);
7185
+ if (count === null) value = await decryptFromStorage(row.value, userKey);
7186
+ else {
7187
+ if (count > slices.length) throw new Error("Missing state chunks");
7188
+ const parts = [];
7189
+ for (let i = 0; i < count; i++) {
7190
+ const slice = slices.find(
7191
+ (candidate) => candidate.key === chunkKeyFor(row.key, i)
7192
+ );
7193
+ if (!slice || slice.updated_at !== row.updated_at)
7194
+ throw new Error("Mismatched state chunks");
7195
+ parts.push(await decryptFromStorage(slice.value, userKey));
7196
+ }
7197
+ value = parts.join("");
7198
+ }
7199
+ } catch (e9) {
7200
+ unreadable = true;
7201
+ }
7202
+ await add({
7203
+ type: "state",
7204
+ identity: [row.namespace, row.key],
7205
+ namespace: row.namespace,
7206
+ key: row.key,
7207
+ rows,
7208
+ value,
7209
+ unreadable,
7210
+ status: null
7211
+ });
7212
+ }
7213
+ for (const [group, rows] of chunks) {
7214
+ const [namespace, key] = JSON.parse(group);
7215
+ await add({
7216
+ type: "state",
7217
+ identity: [namespace, key],
7218
+ namespace,
7219
+ key,
7220
+ rows,
7221
+ status: "orphan-chunks"
7222
+ });
7223
+ }
7224
+ for (const row of raw.credential || []) {
7225
+ await add({
7226
+ type: "credential",
7227
+ identity: [row.cred_id],
7228
+ credId: row.cred_id,
7229
+ rows: [row],
7230
+ status: null
7231
+ });
7232
+ }
7233
+ for (const row of raw.outbox || []) {
7234
+ let payload, unreadable = false;
7235
+ try {
7236
+ payload = objectOrText(await decryptFromStorage(row.payload, userKey));
7237
+ } catch (e10) {
7238
+ unreadable = true;
7239
+ }
7240
+ await add({
7241
+ type: "outbox",
7242
+ identity: [row.message_id],
7243
+ rows: [row],
7244
+ payload,
7245
+ task: taskByUuid.get(row.task_uuid),
7246
+ unreadable,
7247
+ status: row.acked_at != null ? "acked" : row.delivered_at != null ? "delivered" : "pending"
7248
+ });
7249
+ }
7250
+ for (const row of raw.subscription || []) {
7251
+ await add({
7252
+ type: "subscription",
7253
+ identity: [userId],
7254
+ rows: [row],
7255
+ status: "registered"
7256
+ });
7257
+ }
7258
+ const ownerLabels = /* @__PURE__ */ new Map();
7259
+ for (const { resource } of entries) {
7260
+ const owner = resource.owner;
7261
+ if (!_optionalChain([owner, 'optionalAccess', _71 => _71.label, 'optionalAccess', _72 => _72.trim, 'call', _73 => _73()])) continue;
7262
+ const identity = JSON.stringify([owner.type, owner.id]);
7263
+ const current = ownerLabels.get(identity);
7264
+ if (!current || (_nullishCoalesce(resource.updatedAt, () => ( 0))) > current.updatedAt) {
7265
+ ownerLabels.set(identity, {
7266
+ label: owner.label,
7267
+ updatedAt: _nullishCoalesce(resource.updatedAt, () => ( 0))
7268
+ });
7269
+ }
7270
+ }
7271
+ for (const { resource } of entries) {
7272
+ const owner = resource.owner;
7273
+ if (!owner || _optionalChain([owner, 'access', _74 => _74.label, 'optionalAccess', _75 => _75.trim, 'call', _76 => _76()])) continue;
7274
+ const known = ownerLabels.get(JSON.stringify([owner.type, owner.id]));
7275
+ if (known) resource.owner = { ...owner, label: known.label };
7276
+ }
7277
+ entries.sort((a, b) => a.resource.id.localeCompare(b.resource.id));
7278
+ return { entries, complete: gaps.length === 0, gaps };
7279
+ }
7280
+
7281
+ // src/server/lib/cloud-data-cleanup.js
7282
+ var CLOUD_RESOURCE_TYPES = [
7283
+ "task",
7284
+ "state",
7285
+ "credential",
7286
+ "outbox",
7287
+ "subscription"
7288
+ ];
7289
+ function cloudError(code, message, status = 400) {
7290
+ return Object.assign(new Error(message), { code, status });
7291
+ }
7292
+ function validCloudOwner(owner) {
7293
+ return owner && typeof owner.type === "string" && owner.type.length > 0 && owner.type.length <= 100 && typeof owner.id === "string" && owner.id.length > 0 && owner.id.length <= 300;
7294
+ }
7295
+ var ownerEquals = (a, b) => a && b && a.type === b.type && a.id === b.id;
7296
+ async function cloudInventory(ctx, db, userId, userKey) {
7297
+ return buildCloudDataInventory({
7298
+ db,
7299
+ userId,
7300
+ userKey,
7301
+ resolveOwner: _optionalChain([ctx, 'access', _77 => _77.cloudData, 'optionalAccess', _78 => _78.resolveOwner])
7302
+ });
7303
+ }
7304
+ async function save(db, userId, key, kind, value, options) {
7305
+ return db.putCloudDataRecord(
7306
+ userId,
7307
+ kind,
7308
+ value.id,
7309
+ await encryptForStorage(
7310
+ JSON.stringify({
7311
+ ...value,
7312
+ ...value.entries ? { entries: value.entries.map(({ locator, ...entry }) => entry) } : {}
7313
+ }),
7314
+ key
7315
+ ),
7316
+ options
7317
+ );
7318
+ }
7319
+ async function readCloudRecord(db, userId, key, kind, id) {
7320
+ const record = await db.getCloudDataRecord(userId, kind, id);
7321
+ if (!record)
7322
+ throw cloudError("CLOUD_RECORD_NOT_FOUND", "\u4E91\u7AEF\u7BA1\u7406\u8BB0\u5F55\u4E0D\u5B58\u5728", 404);
7323
+ return JSON.parse(await decryptFromStorage(record.data, key));
7324
+ }
7325
+ function publicOperation(operation) {
7326
+ const {
7327
+ entries,
7328
+ cursor,
7329
+ attempts,
7330
+ nextAttemptAt,
7331
+ knownResourceIds,
7332
+ knownResourceVersions,
7333
+ ...publicValue
7334
+ } = operation;
7335
+ return publicValue;
7336
+ }
7337
+ async function makeCleanupPlan(ctx, db, userId, key, input) {
7338
+ const { mode, owner, resourceIds, types } = input;
7339
+ if (!["purge", "retire-owner"].includes(mode))
7340
+ throw cloudError("INVALID_CLEANUP_MODE", "\u6E05\u7406\u6A21\u5F0F\u65E0\u6548");
7341
+ if (owner !== void 0 && !validCloudOwner(owner))
7342
+ throw cloudError("INVALID_CLOUD_OWNER", "\u5F52\u5C5E\u683C\u5F0F\u65E0\u6548");
7343
+ if (mode === "retire-owner" && !validCloudOwner(owner))
7344
+ throw cloudError("INVALID_CLOUD_OWNER", "\u5F7B\u5E95\u79FB\u9664\u9700\u8981\u660E\u786E\u5F52\u5C5E");
7345
+ if (resourceIds !== void 0 && (!Array.isArray(resourceIds) || !resourceIds.length || resourceIds.some((id) => typeof id !== "string")))
7346
+ throw cloudError("INVALID_CLEANUP_SELECTION", "\u8BF7\u9009\u62E9\u81F3\u5C11\u4E00\u9879\u8D44\u6E90");
7347
+ if (types !== void 0 && (!Array.isArray(types) || !types.length || types.some((type) => !CLOUD_RESOURCE_TYPES.includes(type))))
7348
+ throw cloudError("INVALID_CLEANUP_SELECTION", "\u8D44\u6E90\u7C7B\u522B\u65E0\u6548");
7349
+ if (mode === "purge" && !owner && !_optionalChain([resourceIds, 'optionalAccess', _79 => _79.length]))
7350
+ throw cloudError("INVALID_CLEANUP_SELECTION", "\u6E05\u7406\u9700\u8981\u660E\u786E\u8D44\u6E90\u6216\u5F52\u5C5E");
7351
+ if (mode === "retire-owner" && (resourceIds || types))
7352
+ throw cloudError("INVALID_CLEANUP_SELECTION", "\u5F7B\u5E95\u79FB\u9664\u4E0D\u80FD\u9650\u5236\u8D44\u6E90\u7C7B\u522B");
7353
+ const ownerGeneration = mode === "retire-owner" ? (await db.getCloudOwner(userId, owner)).generation : null;
7354
+ const inventory = await cloudInventory(ctx, db, userId, key);
7355
+ if (!inventory.complete)
7356
+ throw cloudError(
7357
+ "CLOUD_INVENTORY_INCOMPLETE",
7358
+ "\u4E91\u7AEF\u6E05\u5355\u4E0D\u5B8C\u6574\uFF0C\u6682\u4E0D\u80FD\u751F\u6210\u6E05\u7406\u8BA1\u5212",
7359
+ 409
7360
+ );
7361
+ const entries = inventory.entries.filter(
7362
+ (e) => (!owner || ownerEquals(e.resource.owner, owner)) && (!resourceIds || resourceIds.includes(e.resource.id)) && (!types || types.includes(e.resource.type))
7363
+ );
7364
+ if (resourceIds && new Set(resourceIds).size !== entries.length)
7365
+ throw cloudError(
7366
+ "CLOUD_RESOURCE_NOT_FOUND",
7367
+ "\u90E8\u5206\u9009\u4E2D\u8D44\u6E90\u4E0D\u5B58\u5728\u6216\u4E0D\u5C5E\u4E8E\u5F53\u524D\u8303\u56F4",
7368
+ 409
7369
+ );
7370
+ const at = Date.now();
7371
+ const plan = {
7372
+ id: crypto.randomUUID(),
7373
+ mode,
7374
+ ownerGeneration,
7375
+ owner: owner ? { type: owner.type, id: owner.id, label: owner.label } : null,
7376
+ resources: entries.map((e) => e.resource),
7377
+ count: entries.length,
7378
+ counts: CLOUD_RESOURCE_TYPES.map((type) => ({
7379
+ type,
7380
+ count: entries.filter((e) => e.resource.type === type).length
7381
+ })),
7382
+ expiresAt: at + 15 * 60 * 1e3,
7383
+ impacts: mode === "retire-owner" ? [
7384
+ "\u505C\u7528\u6B64\u5F52\u5C5E\uFF0C\u5305\u542B\u6267\u884C\u65F6\u65B0\u589E\u7684\u5168\u90E8\u6240\u5C5E\u8D44\u6E90\u3002",
7385
+ "\u5DF2\u4EA4\u7ED9\u5916\u90E8\u63A8\u9001\u670D\u52A1\u7684\u901A\u77E5\u65E0\u6CD5\u64A4\u56DE\u3002"
7386
+ ] : ["\u4EC5\u6E05\u7406\u9884\u89C8\u4E2D\u7684\u8D44\u6E90\uFF1B\u540E\u7EED\u6B63\u5E38\u5199\u5165\u53EF\u4EE5\u91CD\u65B0\u521B\u5EFA\u3002"],
7387
+ complete: true,
7388
+ gaps: [],
7389
+ entries
7390
+ };
7391
+ await save(db, userId, key, "plan", plan);
7392
+ const { entries: privateEntries, ...publicPlan } = plan;
7393
+ return publicPlan;
7394
+ }
7395
+ async function startCleanup(ctx, db, userId, key, input) {
7396
+ if (typeof input.planId !== "string" || typeof input.idempotencyKey !== "string" || !input.idempotencyKey.trim() || input.idempotencyKey.length > 200)
7397
+ throw cloudError("INVALID_CLEANUP_OPERATION", "\u9700\u8981\u8BA1\u5212\u7F16\u53F7\u4E0E\u5E42\u7B49\u952E");
7398
+ const existing = await db.getCloudDataRecordByIdempotency(
7399
+ userId,
7400
+ "operation",
7401
+ input.idempotencyKey
7402
+ );
7403
+ if (existing) {
7404
+ const op = JSON.parse(await decryptFromStorage(existing.data, key));
7405
+ if (op.planId !== input.planId)
7406
+ throw cloudError("IDEMPOTENCY_CONFLICT", "\u6B64\u5E42\u7B49\u952E\u5DF2\u7528\u4E8E\u53E6\u4E00\u8BA1\u5212", 409);
7407
+ const resumed = await advanceCleanup(ctx, db, userId, key, op);
7408
+ assertRetirementAcknowledged(resumed);
7409
+ return publicOperation(resumed);
7410
+ }
7411
+ const plan = await readCloudRecord(db, userId, key, "plan", input.planId);
7412
+ if (plan.expiresAt < Date.now())
7413
+ throw cloudError("CLOUD_PLAN_EXPIRED", "\u6E05\u7406\u9884\u89C8\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8", 409);
7414
+ if (plan.mode === "retire-owner" && (await db.getCloudOwner(userId, plan.owner)).generation !== plan.ownerGeneration) {
7415
+ throw cloudError("CLOUD_PLAN_CHANGED", "\u5F52\u5C5E\u4EE3\u9645\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8", 409);
7416
+ }
7417
+ if (plan.mode === "purge") {
7418
+ const current = await cloudInventory(ctx, db, userId, key);
7419
+ if (!current.complete)
7420
+ throw cloudError(
7421
+ "CLOUD_INVENTORY_INCOMPLETE",
7422
+ "\u65E0\u6CD5\u786E\u8BA4\u5F53\u524D\u4E91\u7AEF\u6570\u636E",
7423
+ 409
7424
+ );
7425
+ const versions = new Map(
7426
+ current.entries.map((e) => [e.resource.id, e.version])
7427
+ );
7428
+ if (plan.entries.some((e) => versions.get(e.resource.id) !== e.version))
7429
+ throw cloudError(
7430
+ "CLOUD_PLAN_CHANGED",
7431
+ "\u9884\u89C8\u540E\u6570\u636E\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8",
7432
+ 409
7433
+ );
7434
+ }
7435
+ const at = Date.now();
7436
+ const operation = {
7437
+ id: crypto.randomUUID(),
7438
+ planId: plan.id,
7439
+ idempotencyKey: input.idempotencyKey,
7440
+ mode: plan.mode,
7441
+ owner: plan.owner,
7442
+ ownerGeneration: plan.ownerGeneration,
7443
+ fencedGeneration: null,
7444
+ status: "pending",
7445
+ counts: CLOUD_RESOURCE_TYPES.map((type) => ({
7446
+ type,
7447
+ deleted: 0,
7448
+ remaining: plan.entries.filter((e) => e.resource.type === type).length,
7449
+ failed: 0
7450
+ })),
7451
+ errors: [],
7452
+ createdAt: at,
7453
+ updatedAt: at,
7454
+ entries: plan.entries,
7455
+ knownResourceIds: plan.entries.map((entry) => entry.resource.id),
7456
+ knownResourceVersions: Object.fromEntries(
7457
+ plan.entries.map((entry) => [entry.resource.id, entry.version])
7458
+ ),
7459
+ cursor: 0,
7460
+ attempts: 0,
7461
+ nextAttemptAt: 0
7462
+ };
7463
+ const stored = await save(db, userId, key, "operation", operation, {
7464
+ idempotencyKey: input.idempotencyKey
7465
+ });
7466
+ const persisted = JSON.parse(await decryptFromStorage(stored.data, key));
7467
+ if (persisted.planId !== input.planId)
7468
+ throw cloudError("IDEMPOTENCY_CONFLICT", "\u6B64\u5E42\u7B49\u952E\u5DF2\u7528\u4E8E\u53E6\u4E00\u8BA1\u5212", 409);
7469
+ await db.deleteCloudDataRecord(userId, "plan", plan.id);
7470
+ const advanced = await advanceCleanup(ctx, db, userId, key, persisted);
7471
+ assertRetirementAcknowledged(advanced);
7472
+ return publicOperation(advanced);
7473
+ }
7474
+ function assertRetirementAcknowledged(operation) {
7475
+ if (operation.mode === "retire-owner" && operation.fencedGeneration == null) {
7476
+ throw cloudError(
7477
+ "CLOUD_RETIREMENT_NOT_CONFIRMED",
7478
+ "\u5C1A\u672A\u786E\u8BA4\u4E91\u7AEF\u505C\u7528\uFF0C\u8BF7\u4FDD\u7559\u672C\u5730\u89D2\u8272\u5E76\u91CD\u8BD5\u6216\u67E5\u770B\u4E91\u7AEF\u64CD\u4F5C\u8BB0\u5F55\u3002",
7479
+ 503
7480
+ );
7481
+ }
7482
+ }
7483
+ async function advanceCleanup(ctx, db, userId, key, operation) {
7484
+ if (operation.status === "completed" || operation.status === "failed" || operation.nextAttemptAt > Date.now())
7485
+ return operation;
7486
+ const leaseToken = crypto.randomUUID();
7487
+ const leaseMs = 12e4;
7488
+ if (typeof db.claimCloudDataRecord === "function" && !await db.claimCloudDataRecord(
7489
+ userId,
7490
+ "operation",
7491
+ operation.id,
7492
+ leaseMs,
7493
+ leaseToken
7494
+ ))
7495
+ return operation;
7496
+ const renewLease = async () => {
7497
+ if (!await db.renewCloudDataRecordLease(
7498
+ userId,
7499
+ "operation",
7500
+ operation.id,
7501
+ leaseMs,
7502
+ leaseToken
7503
+ )) {
7504
+ throw cloudError(
7505
+ "CLOUD_LEASE_LOST",
7506
+ "\u6E05\u7406\u5904\u7406\u6743\u5DF2\u4EA4\u7ED9\u53E6\u4E00\u4E2A\u5DE5\u4F5C\u5B9E\u4F8B",
7507
+ 409
7508
+ );
7509
+ }
7510
+ };
7511
+ try {
7512
+ operation = await readCloudRecord(
7513
+ db,
7514
+ userId,
7515
+ key,
7516
+ "operation",
7517
+ operation.id
7518
+ );
7519
+ if (operation.status === "completed" || operation.status === "failed")
7520
+ return operation;
7521
+ operation.status = "running";
7522
+ if (operation.mode === "retire-owner") {
7523
+ if (operation.fencedGeneration == null) {
7524
+ const state = await db.setCloudOwnerActive(
7525
+ userId,
7526
+ operation.owner,
7527
+ false,
7528
+ operation.ownerGeneration
7529
+ );
7530
+ operation.fencedGeneration = state.generation;
7531
+ await save(db, userId, key, "operation", operation, { leaseToken });
7532
+ } else {
7533
+ const state = await db.getCloudOwner(userId, operation.owner);
7534
+ if (state.active || state.generation !== operation.fencedGeneration) {
7535
+ throw cloudError(
7536
+ "CLOUD_OWNER_CHANGED",
7537
+ "\u5F52\u5C5E\u5DF2\u53D8\u5316\uFF0C\u5DF2\u505C\u6B62\u65E7\u6E05\u7406\u64CD\u4F5C",
7538
+ 409
7539
+ );
7540
+ }
7541
+ }
7542
+ const inventory = await cloudInventory(ctx, db, userId, key);
7543
+ if (!inventory.complete)
7544
+ throw cloudError(
7545
+ "CLOUD_INVENTORY_INCOMPLETE",
7546
+ "\u65E0\u6CD5\u786E\u8BA4\u5F85\u6E05\u7406\u8D44\u6E90",
7547
+ 503
7548
+ );
7549
+ const known = new Set(operation.knownResourceIds || []);
7550
+ operation.knownResourceVersions ||= {};
7551
+ operation.entries = inventory.entries.filter((entry) => {
7552
+ if (ownerEquals(entry.resource.owner, operation.owner)) return true;
7553
+ if (!known.has(entry.resource.id)) return false;
7554
+ if (entry.resource.owner || operation.knownResourceVersions[entry.resource.id] !== entry.version) {
7555
+ throw cloudError(
7556
+ "CLOUD_PLAN_CHANGED",
7557
+ "\u5DF2\u8BC6\u522B\u8D44\u6E90\u7684\u5F52\u5C5E\u6216\u5185\u5BB9\u53D1\u751F\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8",
7558
+ 409
7559
+ );
7560
+ }
7561
+ return true;
7562
+ });
7563
+ for (const entry of operation.entries) {
7564
+ known.add(entry.resource.id);
7565
+ operation.knownResourceVersions[entry.resource.id] = entry.version;
7566
+ }
7567
+ operation.knownResourceIds = [...known];
7568
+ operation.cursor = 0;
7569
+ for (const count of operation.counts)
7570
+ count.remaining = operation.entries.filter(
7571
+ (entry) => entry.resource.type === count.type
7572
+ ).length;
7573
+ await save(db, userId, key, "operation", operation, { leaseToken });
7574
+ }
7575
+ const beforeBatch = await cloudInventory(ctx, db, userId, key);
7576
+ if (!beforeBatch.complete)
7577
+ throw cloudError("CLOUD_INVENTORY_INCOMPLETE", "\u65E0\u6CD5\u786E\u8BA4\u5F85\u6E05\u7406\u8D44\u6E90", 503);
7578
+ const currentById = new Map(
7579
+ beforeBatch.entries.map((entry) => [entry.resource.id, entry])
7580
+ );
7581
+ const batch = operation.entries.slice(
7582
+ operation.cursor,
7583
+ operation.cursor + 25
7584
+ );
7585
+ for (const entry of batch) {
7586
+ await renewLease();
7587
+ const currentEntry = currentById.get(entry.resource.id);
7588
+ if (currentEntry && currentEntry.version !== entry.version) {
7589
+ throw cloudError(
7590
+ "CLOUD_PLAN_CHANGED",
7591
+ "\u8D44\u6E90\u5728\u6267\u884C\u524D\u53D8\u5316\uFF0C\u5DF2\u505C\u6B62\u6E05\u7406",
7592
+ 409
7593
+ );
7594
+ }
7595
+ const result = currentEntry ? await db.deleteCloudResourceRows(userId, [currentEntry.locator], {
7596
+ operationId: operation.id,
7597
+ leaseToken
7598
+ }) : { changed: 0 };
7599
+ if (result.changed)
7600
+ throw cloudError(
7601
+ "CLOUD_PLAN_CHANGED",
7602
+ "\u8D44\u6E90\u5728\u6267\u884C\u524D\u53D8\u5316\uFF0C\u5DF2\u505C\u6B62\u6E05\u7406",
7603
+ 409
7604
+ );
7605
+ const count = operation.counts.find(
7606
+ (c) => c.type === entry.resource.type
7607
+ );
7608
+ count.deleted++;
7609
+ count.remaining = Math.max(0, count.remaining - 1);
7610
+ delete entry.locator;
7611
+ operation.cursor++;
7612
+ operation.updatedAt = Date.now();
7613
+ await save(db, userId, key, "operation", operation, { leaseToken });
7614
+ }
7615
+ await renewLease();
7616
+ const current = await cloudInventory(ctx, db, userId, key);
7617
+ if (!current.complete)
7618
+ throw cloudError(
7619
+ "CLOUD_INVENTORY_INCOMPLETE",
7620
+ "\u6E05\u7406\u540E\u65E0\u6CD5\u786E\u8BA4\u5269\u4F59\u6570\u636E",
7621
+ 503
7622
+ );
7623
+ const selected = new Set(operation.entries.map((e) => e.resource.id));
7624
+ const remaining = current.entries.filter(
7625
+ (e) => operation.mode === "retire-owner" ? ownerEquals(e.resource.owner, operation.owner) || (operation.knownResourceIds || []).includes(e.resource.id) : selected.has(e.resource.id)
7626
+ );
7627
+ for (const count of operation.counts)
7628
+ count.remaining = remaining.filter(
7629
+ (e) => e.resource.type === count.type
7630
+ ).length;
7631
+ if (operation.mode === "purge") {
7632
+ const processed = new Set(
7633
+ operation.entries.slice(0, operation.cursor).map((entry) => entry.resource.id)
7634
+ );
7635
+ if (remaining.some((entry) => processed.has(entry.resource.id))) {
7636
+ throw cloudError(
7637
+ "CLOUD_PLAN_CHANGED",
7638
+ "\u5DF2\u6E05\u7406\u8D44\u6E90\u88AB\u91CD\u65B0\u521B\u5EFA\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8",
7639
+ 409
7640
+ );
7641
+ }
7642
+ }
7643
+ operation.status = remaining.length ? "pending" : "completed";
7644
+ if (operation.status === "completed") {
7645
+ operation.entries = [];
7646
+ operation.knownResourceIds = [];
7647
+ operation.knownResourceVersions = {};
7648
+ }
7649
+ operation.errors = [];
7650
+ operation.attempts = 0;
7651
+ } catch (error) {
7652
+ if (error.code === "CLOUD_LEASE_LOST") throw error;
7653
+ operation.attempts = (operation.attempts || 0) + 1;
7654
+ const terminal = ["CLOUD_PLAN_CHANGED", "CLOUD_OWNER_CHANGED"].includes(error.code) || operation.attempts >= 8;
7655
+ operation.status = terminal ? "failed" : "pending";
7656
+ operation.errors = [
7657
+ {
7658
+ code: error.code || "CLOUD_CLEANUP_FAILED",
7659
+ message: error.code === "CLOUD_PLAN_CHANGED" ? error.message : "\u4E91\u7AEF\u6E05\u7406\u672A\u5B8C\u6210\uFF0C\u5C06\u6309\u670D\u52A1\u7AEF\u8BB0\u5F55\u91CD\u8BD5\u3002"
7660
+ }
7661
+ ];
7662
+ operation.nextAttemptAt = Date.now() + Math.min(36e5, 3e4 * 2 ** operation.attempts);
7663
+ if (terminal)
7664
+ for (const count of operation.counts) count.failed = count.remaining;
7665
+ } finally {
7666
+ operation.updatedAt = Date.now();
7667
+ await save(db, userId, key, "operation", operation, { leaseToken });
7668
+ if (typeof db.releaseCloudDataRecord === "function")
7669
+ await db.releaseCloudDataRecord(
7670
+ userId,
7671
+ "operation",
7672
+ operation.id,
7673
+ leaseToken
7674
+ );
7675
+ }
7676
+ return operation;
7677
+ }
7678
+ async function resumeCloudDataCleanups(ctx) {
7679
+ const db = ctx.db;
7680
+ if (!_optionalChain([db, 'optionalAccess', _80 => _80.cloudDataManagement])) return;
7681
+ const rows = await db.listCloudDataRecordsAcrossUsers("operation");
7682
+ await db.cleanupCloudDataRecords("inventory", Date.now() - 36e5);
7683
+ await db.cleanupCloudDataRecords("plan", Date.now() - 36e5);
7684
+ await db.cleanupCloudResourceMetadata();
7685
+ let processed = 0;
7686
+ for (const row of rows) {
7687
+ try {
7688
+ const key = await deriveUserEncryptionKey(row.userId, ctx.masterKey);
7689
+ const operation = JSON.parse(await decryptFromStorage(row.data, key));
7690
+ if (["completed", "failed"].includes(operation.status) && operation.updatedAt < Date.now() - 30 * 864e5) {
7691
+ await db.deleteCloudDataRecord(row.userId, "operation", operation.id);
7692
+ continue;
7693
+ }
7694
+ if (["pending", "running"].includes(operation.status)) {
7695
+ await advanceCleanup(ctx, db, row.userId, key, operation);
7696
+ if (++processed >= 100) break;
7697
+ }
7698
+ } catch (e11) {
7699
+ console.warn("[amsg] A cloud cleanup operation could not be resumed");
7700
+ }
7701
+ }
7702
+ }
7703
+
7704
+ // src/server/handlers/cloud-data.js
7705
+ function createCloudDataHandler(ctx) {
7706
+ async function run(url, headers, body, method) {
7707
+ const tenant = await ctx.tenantManager.resolveTenant(headers);
7708
+ if (!tenant.ok) return tenant.error;
7709
+ const gate = requireUserId(headers);
7710
+ if (gate.error) return gate.error;
7711
+ const { db, masterKey } = tenant.context;
7712
+ const { userId } = gate;
7713
+ if (!db.cloudDataManagement)
7714
+ return {
7715
+ status: 501,
7716
+ body: {
7717
+ success: false,
7718
+ error: {
7719
+ code: "CLOUD_DATA_NOT_SUPPORTED",
7720
+ message: "\u5F53\u524D\u9002\u914D\u5668\u4E0D\u652F\u6301\u5B8C\u6574\u4E91\u7AEF\u7BA1\u7406"
7721
+ }
7722
+ }
7723
+ };
7724
+ const key = await deriveUserEncryptionKey(userId, masterKey);
7725
+ try {
7726
+ const parsedUrl = new URL(url, "https://cloud.invalid");
7727
+ const path = parsedUrl.pathname;
7728
+ const params = parsedUrl.searchParams;
7729
+ let input;
7730
+ if (method === "POST") {
7731
+ if (getHeader(headers, "x-payload-encrypted") !== "true" || getHeader(headers, "x-encryption-version") !== "1")
7732
+ throw cloudError("ENCRYPTION_REQUIRED", "\u8BF7\u6C42\u5FC5\u987B\u4F7F\u7528\u7248\u672C 1 \u52A0\u5BC6");
7733
+ const parsed = parseEncryptedBody(body);
7734
+ if (!parsed.ok) throw cloudError("INVALID_PAYLOAD", "\u8BF7\u6C42\u683C\u5F0F\u65E0\u6548");
7735
+ try {
7736
+ input = await decryptPayload(parsed.data, key);
7737
+ } catch (e12) {
7738
+ throw cloudError("DECRYPTION_FAILED", "\u8BF7\u6C42\u89E3\u5BC6\u5931\u8D25");
7739
+ }
7740
+ if (!input || typeof input !== "object" || Array.isArray(input))
7741
+ throw cloudError("INVALID_PAYLOAD", "\u8BF7\u6C42\u683C\u5F0F\u65E0\u6548");
7742
+ }
7743
+ let data;
7744
+ if (method === "GET" && (path.endsWith("/summary") || path.endsWith("/resources"))) {
7745
+ const ownerType = params.get("ownerType"), ownerId = params.get("ownerId"), type = params.get("type");
7746
+ if (Boolean(ownerType) !== Boolean(ownerId) || type && !CLOUD_RESOURCE_TYPES.includes(type))
7747
+ throw cloudError("INVALID_CLOUD_FILTER", "\u8D44\u6E90\u7B5B\u9009\u65E0\u6548");
7748
+ const filter = { ownerType, ownerId, type };
7749
+ if (path.endsWith("/summary")) {
7750
+ const inventory = await cloudInventory(ctx, db, userId, key);
7751
+ const resources = inventory.entries.map((e) => e.resource);
7752
+ data = {
7753
+ total: resources.length,
7754
+ counts: CLOUD_RESOURCE_TYPES.map((type2) => ({
7755
+ type: type2,
7756
+ count: resources.filter((r) => r.type === type2).length,
7757
+ byteSize: resources.filter((r) => r.type === type2).reduce((sum, r) => sum + (r.byteSize || 0), 0)
7758
+ })),
7759
+ complete: inventory.complete,
7760
+ gaps: inventory.gaps
7761
+ };
7762
+ } else {
7763
+ const limit = Number(params.get("limit") || 50);
7764
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200)
7765
+ throw cloudError("INVALID_CLOUD_LIMIT", "\u6BCF\u9875\u6570\u91CF\u5FC5\u987B\u4E3A 1\u2013200");
7766
+ let snapshot, offset = 0;
7767
+ const cursor = params.get("cursor");
7768
+ if (cursor) {
7769
+ const match = /^([0-9a-f-]{36}):(\d+)$/.exec(cursor);
7770
+ if (!match)
7771
+ throw cloudError("INVALID_CLOUD_CURSOR", "\u5206\u9875\u6E38\u6807\u65E0\u6548");
7772
+ snapshot = await readCloudRecord(
7773
+ db,
7774
+ userId,
7775
+ key,
7776
+ "inventory",
7777
+ match[1]
7778
+ );
7779
+ offset = Number(match[2]);
7780
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > snapshot.resources.length)
7781
+ throw cloudError("INVALID_CLOUD_CURSOR", "\u5206\u9875\u6E38\u6807\u65E0\u6548");
7782
+ if (snapshot.expiresAt < Date.now() || JSON.stringify(snapshot.filter) !== JSON.stringify(filter))
7783
+ throw cloudError(
7784
+ "CLOUD_CURSOR_EXPIRED",
7785
+ "\u5206\u9875\u5DF2\u8FC7\u671F\u6216\u7B5B\u9009\u53D8\u5316",
7786
+ 409
7787
+ );
7788
+ } else {
7789
+ const inventory = await cloudInventory(ctx, db, userId, key);
7790
+ snapshot = {
7791
+ id: crypto.randomUUID(),
7792
+ filter,
7793
+ expiresAt: Date.now() + 15 * 60 * 1e3,
7794
+ resources: inventory.entries.map((e) => e.resource).filter(
7795
+ (r) => (!type || r.type === type) && (!ownerId || _optionalChain([r, 'access', _81 => _81.owner, 'optionalAccess', _82 => _82.type]) === ownerType && r.owner.id === ownerId)
7796
+ ),
7797
+ complete: inventory.complete,
7798
+ gaps: inventory.gaps
7799
+ };
7800
+ await db.putCloudDataRecord(
7801
+ userId,
7802
+ "inventory",
7803
+ snapshot.id,
7804
+ await encryptForStorage(JSON.stringify(snapshot), key)
7805
+ );
7806
+ }
7807
+ data = {
7808
+ resources: snapshot.resources.slice(offset, offset + limit),
7809
+ nextCursor: offset + limit < snapshot.resources.length ? `${snapshot.id}:${offset + limit}` : null,
7810
+ complete: snapshot.complete,
7811
+ gaps: snapshot.gaps
7812
+ };
7813
+ }
7814
+ } else if (method === "POST" && path.endsWith("/cleanup-plans"))
7815
+ data = await makeCleanupPlan(ctx, db, userId, key, input);
7816
+ else if (method === "POST" && path.endsWith("/cleanup-operations"))
7817
+ data = await startCleanup(ctx, db, userId, key, input);
7818
+ else if (method === "GET" && path.endsWith("/cleanup-operations")) {
7819
+ const records = await db.listCloudDataRecords(userId, "operation");
7820
+ const operations = [];
7821
+ const gaps = [];
7822
+ for (const row of records) {
7823
+ try {
7824
+ operations.push(
7825
+ publicOperation(
7826
+ JSON.parse(await decryptFromStorage(row.data, key))
7827
+ )
7828
+ );
7829
+ } catch (e13) {
7830
+ gaps.push({
7831
+ source: "operation",
7832
+ code: "UNREADABLE_OPERATION",
7833
+ message: `\u65E0\u6CD5\u8BFB\u53D6\u64CD\u4F5C ${row.id}`
7834
+ });
7835
+ }
7836
+ }
7837
+ data = {
7838
+ operations: operations.sort((a, b) => b.createdAt - a.createdAt),
7839
+ complete: gaps.length === 0,
7840
+ gaps
7841
+ };
7842
+ } else if (method === "GET" && path.includes("/cleanup-operations/"))
7843
+ data = publicOperation(
7844
+ await readCloudRecord(
7845
+ db,
7846
+ userId,
7847
+ key,
7848
+ "operation",
7849
+ path.split("/").pop()
7850
+ )
7851
+ );
7852
+ else if (method === "GET" && path.endsWith("/owners")) {
7853
+ const owners = await db.listCloudOwners(userId);
7854
+ data = {
7855
+ owners: owners.map((state) => ({
7856
+ owner: state.owner,
7857
+ retired: !state.active,
7858
+ generation: state.generation,
7859
+ updatedAt: state.updatedAt,
7860
+ complete: true,
7861
+ gaps: []
7862
+ })),
7863
+ complete: true,
7864
+ gaps: []
7865
+ };
7866
+ } else if (path.endsWith("/owner")) {
7867
+ const owner = method === "POST" ? input.owner : { type: params.get("ownerType"), id: params.get("ownerId") };
7868
+ if (!validCloudOwner(owner))
7869
+ throw cloudError("INVALID_CLOUD_OWNER", "\u5F52\u5C5E\u683C\u5F0F\u65E0\u6548");
7870
+ if (method === "POST") {
7871
+ const beforeRestore = await db.getCloudOwner(userId, owner);
7872
+ if (input.action !== "restore")
7873
+ throw cloudError("INVALID_CLOUD_OWNER_ACTION", "\u5F52\u5C5E\u64CD\u4F5C\u65E0\u6548");
7874
+ const rows = await db.listCloudDataRecords(userId, "operation");
7875
+ for (const row of rows) {
7876
+ const op = JSON.parse(await decryptFromStorage(row.data, key));
7877
+ if (op.mode === "retire-owner" && _optionalChain([op, 'access', _83 => _83.owner, 'optionalAccess', _84 => _84.type]) === owner.type && _optionalChain([op, 'access', _85 => _85.owner, 'optionalAccess', _86 => _86.id]) === owner.id && ["pending", "running"].includes(op.status))
7878
+ throw cloudError(
7879
+ "CLOUD_CLEANUP_IN_PROGRESS",
7880
+ "\u6E05\u7406\u5B8C\u6210\u540E\u624D\u80FD\u91CD\u65B0\u542F\u7528",
7881
+ 409
7882
+ );
7883
+ }
7884
+ if (!beforeRestore.active)
7885
+ await db.setCloudOwnerActive(
7886
+ userId,
7887
+ owner,
7888
+ true,
7889
+ beforeRestore.generation
7890
+ );
7891
+ }
7892
+ const state = await db.getCloudOwner(userId, owner);
7893
+ data = {
7894
+ owner,
7895
+ retired: !state.active,
7896
+ generation: state.generation,
7897
+ updatedAt: _nullishCoalesce(state.updatedAt, () => ( null)),
7898
+ complete: true,
7899
+ gaps: []
7900
+ };
7901
+ } else throw cloudError("NOT_FOUND", "\u4E91\u7AEF\u7BA1\u7406\u5165\u53E3\u4E0D\u5B58\u5728", 404);
7902
+ return {
7903
+ status: 200,
7904
+ body: {
7905
+ success: true,
7906
+ encrypted: true,
7907
+ version: 1,
7908
+ data: await encryptPayload(data, key)
7909
+ }
7910
+ };
7911
+ } catch (error) {
7912
+ return {
7913
+ status: error.status || 500,
7914
+ body: {
7915
+ success: false,
7916
+ error: {
7917
+ code: error.code || "CLOUD_DATA_FAILED",
7918
+ message: error.status ? error.message : "\u4E91\u7AEF\u7BA1\u7406\u8BF7\u6C42\u5931\u8D25\uFF1B\u672A\u80FD\u786E\u8BA4\u64CD\u4F5C\u7ED3\u679C\u3002"
7919
+ }
7920
+ }
7921
+ };
7922
+ }
7923
+ }
7924
+ return {
7925
+ GET: (url, headers) => run(url, headers, null, "GET"),
7926
+ POST: (url, headers, body) => run(url, headers, body, "POST")
7927
+ };
7928
+ }
7929
+
6145
7930
  // src/server/handlers/outbox.js
6146
7931
  var MAX_OUTBOX_PAGE_SIZE = 100;
6147
7932
  var DEFAULT_OUTBOX_PAGE_SIZE = 50;
@@ -6307,7 +8092,7 @@ function createSingleUserServer(config) {
6307
8092
  if (!config.masterKey) throw new Error("[amsg-server single-user] config.masterKey is required");
6308
8093
  const vapid = config.vapid || {};
6309
8094
  const tenantManager = createSingleUserContextManager({
6310
- db: config.db,
8095
+ db: createCloudGuardedAdapter(config.db, { masterKey: config.masterKey, resolveOwner: _optionalChain([config, 'access', _87 => _87.cloudData, 'optionalAccess', _88 => _88.resolveOwner]) }),
6311
8096
  masterKey: config.masterKey,
6312
8097
  serverToken: config.serverToken
6313
8098
  });
@@ -6339,7 +8124,8 @@ function createSingleUserServer(config) {
6339
8124
  // 定时任务投递失败后的重试次数上限(默认 3)。handlers 用不到,宿主拿这个
6340
8125
  // ctx 去调 runScheduledTick 时它跟着走。
6341
8126
  maxGenerationRetries: config.maxGenerationRetries,
6342
- maxDeliveryRetries: config.maxDeliveryRetries
8127
+ maxDeliveryRetries: config.maxDeliveryRetries,
8128
+ cloudData: config.cloudData
6343
8129
  };
6344
8130
  return {
6345
8131
  ctx,
@@ -6357,13 +8143,14 @@ function createSingleUserServer(config) {
6357
8143
  pushSubscription: createPushSubscriptionHandler(ctx),
6358
8144
  llmCredentials: createLlmCredentialsHandler(ctx),
6359
8145
  capabilities: createCapabilitiesHandler(ctx),
6360
- outbox: createOutboxHandler(ctx)
8146
+ outbox: createOutboxHandler(ctx),
8147
+ cloudData: createCloudDataHandler(ctx)
6361
8148
  }
6362
8149
  };
6363
8150
  }
6364
8151
 
6365
8152
  // src/server/lib/schema-version.js
6366
- var SCHEMA_VERSION = "2.6.0";
8153
+ var SCHEMA_VERSION = "2.6.0-cloud-data.1";
6367
8154
  function requireIntrospection(db) {
6368
8155
  if (!db || typeof db.describeSchema !== "function") {
6369
8156
  throw new Error(
@@ -6492,6 +8279,7 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
6492
8279
  function buildTickContext(cfg) {
6493
8280
  return {
6494
8281
  db: cfg.db,
8282
+ cloudData: cfg.cloudData,
6495
8283
  masterKey: cfg.masterKey,
6496
8284
  vapid: cfg.vapid || {},
6497
8285
  webpush: cfg.webpush,
@@ -6579,7 +8367,9 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
6579
8367
  body = read.body;
6580
8368
  }
6581
8369
  let result;
6582
- if (method === "POST" && pathname.endsWith("/init-tenant")) {
8370
+ if (pathname.includes("/cloud-data/") && (method === "GET" || method === "POST")) {
8371
+ result = method === "GET" ? await server.handlers.cloudData.GET(url, headers) : await server.handlers.cloudData.POST(url, headers, body);
8372
+ } else if (method === "POST" && pathname.endsWith("/init-tenant")) {
6583
8373
  result = await server.handlers.init.POST(headers, body);
6584
8374
  } else if (method === "GET" && pathname.endsWith("/get-user-key")) {
6585
8375
  result = await server.handlers.getUserKey.GET(url, headers);
@@ -6644,6 +8434,13 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
6644
8434
  await reportError({ stage: "config", error, cause, path: null });
6645
8435
  return { ok: false, cause };
6646
8436
  }
8437
+ try {
8438
+ await resumeCloudDataCleanups(buildTickContext(cfg));
8439
+ } catch (error) {
8440
+ const cause = summarizeErrorCause(error, "cloud-cleanup");
8441
+ await reportError({ stage: "cloud-cleanup", error, cause, path: null });
8442
+ return { ok: false, cause };
8443
+ }
6647
8444
  if (!pushConfigured(cfg)) {
6648
8445
  console.error("[amsg single-user] scheduled(): VAPID/webpush not configured; skipping tick");
6649
8446
  const cause = summarizeErrorCause(
@@ -6753,4 +8550,5 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
6753
8550
 
6754
8551
 
6755
8552
 
6756
- exports.isValidTimeZoneId = isValidTimeZoneId; exports.advanceOccurrence = advanceOccurrence; exports.planNextOccurrence = planNextOccurrence; exports.nextFutureOccurrence = nextFutureOccurrence; exports.base64UrlToBytes = _amsgshared.base64UrlToBytes; exports.utf8 = _amsgshared.utf8; exports.utf8Decode = _amsgshared.utf8Decode; exports.bytesToBase64Url = _amsgshared.bytesToBase64Url; exports.timingSafeEqualBytes = _amsgshared.timingSafeEqualBytes; exports.deriveUserEncryptionKey = deriveUserEncryptionKey; exports.decryptPayload = decryptPayload; exports.encryptForStorage = encryptForStorage; exports.decryptFromStorage = decryptFromStorage; exports.CRED_ID_MAX_LENGTH = CRED_ID_MAX_LENGTH; exports.CRED_PUT_BATCH_MAX = CRED_PUT_BATCH_MAX; exports.CRED_ROWS_PER_USER_MAX = CRED_ROWS_PER_USER_MAX; exports.CRED_REFS_MAX_ENTRIES = CRED_REFS_MAX_ENTRIES; exports.supportsLlmCredentialsStore = supportsLlmCredentialsStore; exports.isValidCredId = isValidCredId; exports.validateCredValue = validateCredValue; exports.validateCredRefs = validateCredRefs; exports.hasChatCredRef = hasChatCredRef; exports.resolveLlmCredential = resolveLlmCredential; exports.isValidUrl = _amsgshared.isValidUrl; exports.validateAvatarUrl = _amsgshared.validateAvatarUrl; exports.isValidISO8601 = isValidISO8601; exports.isValidUUID = isValidUUID; exports.isValidUUIDv4 = isValidUUIDv4; exports.validateSplitPattern = validateSplitPattern; exports.validateLlmMessagesArray = validateLlmMessagesArray; exports.MAX_TASK_PAYLOAD_BYTES = MAX_TASK_PAYLOAD_BYTES; exports.validateScheduleMessagePayload = validateScheduleMessagePayload; exports.DEFAULT_MAX_REQUEST_BODY_BYTES = DEFAULT_MAX_REQUEST_BODY_BYTES; exports.readRequestBody = readRequestBody; exports.parseJsonBody = parseJsonBody; exports.getHeader = getHeader; exports.createGetUserKeyHandler = createGetUserKeyHandler; exports.NonRetryableError = NonRetryableError; exports.isNonRetryableError = isNonRetryableError; exports.DeploymentConfigError = DeploymentConfigError; exports.sanitizeErrorSummary = sanitizeErrorSummary; exports.summarizeErrorCause = summarizeErrorCause; exports.DEFAULT_MAX_DELIVERY_RETRIES = DEFAULT_MAX_DELIVERY_RETRIES; exports.WEB_PUSH_MAX_BODY_BYTES = WEB_PUSH_MAX_BODY_BYTES; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.MAX_PUSH_PAYLOAD_BYTES = MAX_PUSH_PAYLOAD_BYTES; exports.PUSH_ENVELOPE_RESERVED_BYTES = PUSH_ENVELOPE_RESERVED_BYTES; exports.measurePushPayload = measurePushPayload; exports.createWebCryptoWebPush = createWebCryptoWebPush; exports.DEFAULT_MAX_TOOL_ITERATIONS = DEFAULT_MAX_TOOL_ITERATIONS; exports.DEFAULT_TOTAL_TIMEOUT_MS = DEFAULT_TOTAL_TIMEOUT_MS; exports.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE; exports.MIN_SCHEDULE_LEAD_MS = MIN_SCHEDULE_LEAD_MS; exports.createScheduleMessageHandler = createScheduleMessageHandler; exports.DEFAULT_CLAIM_LEASE_MS = DEFAULT_CLAIM_LEASE_MS; exports.DEFAULT_LEASE_HEARTBEAT_MS = DEFAULT_LEASE_HEARTBEAT_MS; exports.DEFAULT_HEARTBEAT_LEASE_TTL_MS = DEFAULT_HEARTBEAT_LEASE_TTL_MS; exports.runScheduledTick = runScheduledTick; exports.runTask = runTask; exports.createUpdateMessageHandler = createUpdateMessageHandler; exports.createCancelMessageHandler = createCancelMessageHandler; exports.createMessagesHandler = createMessagesHandler; exports.createGetMessageHandler = createGetMessageHandler; exports.createPushSubscriptionHandler = createPushSubscriptionHandler; exports.createLlmCredentialsHandler = createLlmCredentialsHandler; exports.createD1Adapter = createD1Adapter; exports.createSingleUserServer = createSingleUserServer; exports.SCHEMA_VERSION = SCHEMA_VERSION; exports.getSchemaVersion = getSchemaVersion; exports.ensureSchema = ensureSchema; exports.CORS_ALLOW_HEADERS = CORS_ALLOW_HEADERS; exports.CORS_ALLOW_METHODS = CORS_ALLOW_METHODS; exports.createSingleUserCloudflareWorker = createSingleUserCloudflareWorker;
8553
+
8554
+ exports.isValidTimeZoneId = isValidTimeZoneId; exports.advanceOccurrence = advanceOccurrence; exports.planNextOccurrence = planNextOccurrence; exports.nextFutureOccurrence = nextFutureOccurrence; exports.base64UrlToBytes = _amsgshared.base64UrlToBytes; exports.utf8 = _amsgshared.utf8; exports.utf8Decode = _amsgshared.utf8Decode; exports.bytesToBase64Url = _amsgshared.bytesToBase64Url; exports.timingSafeEqualBytes = _amsgshared.timingSafeEqualBytes; exports.deriveUserEncryptionKey = deriveUserEncryptionKey; exports.decryptPayload = decryptPayload; exports.encryptForStorage = encryptForStorage; exports.decryptFromStorage = decryptFromStorage; exports.CRED_ID_MAX_LENGTH = CRED_ID_MAX_LENGTH; exports.CRED_PUT_BATCH_MAX = CRED_PUT_BATCH_MAX; exports.CRED_ROWS_PER_USER_MAX = CRED_ROWS_PER_USER_MAX; exports.CRED_REFS_MAX_ENTRIES = CRED_REFS_MAX_ENTRIES; exports.supportsLlmCredentialsStore = supportsLlmCredentialsStore; exports.isValidCredId = isValidCredId; exports.validateCredValue = validateCredValue; exports.validateCredRefs = validateCredRefs; exports.hasChatCredRef = hasChatCredRef; exports.resolveLlmCredential = resolveLlmCredential; exports.isValidUrl = _amsgshared.isValidUrl; exports.validateAvatarUrl = _amsgshared.validateAvatarUrl; exports.isValidISO8601 = isValidISO8601; exports.isValidUUID = isValidUUID; exports.isValidUUIDv4 = isValidUUIDv4; exports.validateSplitPattern = validateSplitPattern; exports.validateLlmMessagesArray = validateLlmMessagesArray; exports.MAX_TASK_PAYLOAD_BYTES = MAX_TASK_PAYLOAD_BYTES; exports.validateScheduleMessagePayload = validateScheduleMessagePayload; exports.DEFAULT_MAX_REQUEST_BODY_BYTES = DEFAULT_MAX_REQUEST_BODY_BYTES; exports.readRequestBody = readRequestBody; exports.parseJsonBody = parseJsonBody; exports.getHeader = getHeader; exports.createGetUserKeyHandler = createGetUserKeyHandler; exports.NonRetryableError = NonRetryableError; exports.isNonRetryableError = isNonRetryableError; exports.DeploymentConfigError = DeploymentConfigError; exports.sanitizeErrorSummary = sanitizeErrorSummary; exports.summarizeErrorCause = summarizeErrorCause; exports.DEFAULT_MAX_DELIVERY_RETRIES = DEFAULT_MAX_DELIVERY_RETRIES; exports.WEB_PUSH_MAX_BODY_BYTES = WEB_PUSH_MAX_BODY_BYTES; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.MAX_PUSH_PAYLOAD_BYTES = MAX_PUSH_PAYLOAD_BYTES; exports.PUSH_ENVELOPE_RESERVED_BYTES = PUSH_ENVELOPE_RESERVED_BYTES; exports.measurePushPayload = measurePushPayload; exports.createWebCryptoWebPush = createWebCryptoWebPush; exports.DEFAULT_MAX_TOOL_ITERATIONS = DEFAULT_MAX_TOOL_ITERATIONS; exports.DEFAULT_TOTAL_TIMEOUT_MS = DEFAULT_TOTAL_TIMEOUT_MS; exports.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE; exports.MIN_SCHEDULE_LEAD_MS = MIN_SCHEDULE_LEAD_MS; exports.MAX_DEFER_AFTER_MS = MAX_DEFER_AFTER_MS; exports.createScheduleMessageHandler = createScheduleMessageHandler; exports.DEFAULT_CLAIM_LEASE_MS = DEFAULT_CLAIM_LEASE_MS; exports.DEFAULT_LEASE_HEARTBEAT_MS = DEFAULT_LEASE_HEARTBEAT_MS; exports.DEFAULT_HEARTBEAT_LEASE_TTL_MS = DEFAULT_HEARTBEAT_LEASE_TTL_MS; exports.runScheduledTick = runScheduledTick; exports.runTask = runTask; exports.createUpdateMessageHandler = createUpdateMessageHandler; exports.createCancelMessageHandler = createCancelMessageHandler; exports.createMessagesHandler = createMessagesHandler; exports.createGetMessageHandler = createGetMessageHandler; exports.createPushSubscriptionHandler = createPushSubscriptionHandler; exports.createLlmCredentialsHandler = createLlmCredentialsHandler; exports.createD1Adapter = createD1Adapter; exports.createSingleUserServer = createSingleUserServer; exports.SCHEMA_VERSION = SCHEMA_VERSION; exports.getSchemaVersion = getSchemaVersion; exports.ensureSchema = ensureSchema; exports.CORS_ALLOW_HEADERS = CORS_ALLOW_HEADERS; exports.CORS_ALLOW_METHODS = CORS_ALLOW_METHODS; exports.createSingleUserCloudflareWorker = createSingleUserCloudflareWorker;