@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.
- package/README.md +64 -2
- package/dist/adapters/d1.d.ts +84 -9
- package/dist/adapters/schema.sqlite.d.ts +1 -0
- package/dist/{chunk-KZT3YC62.cjs → chunk-5GRXWKQR.cjs} +1894 -96
- package/dist/{chunk-INNZZFGJ.mjs → chunk-BACG4KJQ.mjs} +1887 -89
- package/dist/cloudflare.cjs +4 -2
- package/dist/cloudflare.d.ts +1 -0
- package/dist/cloudflare.mjs +3 -1
- package/dist/handlers/cloud-data.d.ts +4 -0
- package/dist/index.cjs +27 -25
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +3 -1
- package/dist/lib/agentic-fire.d.ts +43 -1
- package/dist/lib/cloud-data-cleanup.d.ts +36 -0
- package/dist/lib/cloud-data-guard.d.ts +14 -0
- package/dist/lib/cloud-data-inventory.d.ts +15 -0
- package/dist/lib/message-processor.d.ts +9 -1
- package/dist/lib/run-tick.d.ts +6 -2
- package/dist/lib/schema-version.d.ts +1 -1
- package/dist/single-user.d.ts +5 -0
- package/package.json +1 -1
|
@@ -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 (
|
|
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 (
|
|
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',
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3725
|
+
return updateLifecycleTask(taskId, fields);
|
|
3303
3726
|
}
|
|
3304
3727
|
const { last_error: _lastError, ...stateFields } = fields;
|
|
3305
|
-
if (adaptersWithoutLastErrorColumn.has(db)) return
|
|
3728
|
+
if (adaptersWithoutLastErrorColumn.has(db)) return updateLifecycleTask(taskId, stateFields);
|
|
3306
3729
|
let combinedError;
|
|
3307
3730
|
try {
|
|
3308
|
-
const result2 = await
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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)
|
|
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
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
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
|
-
|
|
5714
|
+
const statement = this._db.prepare(
|
|
4862
5715
|
`UPDATE scheduled_messages SET ${sets.join(", ")} WHERE id = ?`
|
|
4863
|
-
).bind(...values)
|
|
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
|
|
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)
|
|
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
|
|
5839
|
+
const statement = this._db.prepare(
|
|
4983
5840
|
`UPDATE scheduled_messages SET ${sets.join(", ")} WHERE ${where}`
|
|
4984
|
-
).bind(...values)
|
|
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
|
|
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)
|
|
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
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
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',
|
|
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
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
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
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
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.
|
|
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 === "
|
|
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
|
-
|
|
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;
|