@rei-standard/amsg-server 2.6.0-next.9 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +822 -41
- package/dist/adapters/d1.d.ts +693 -0
- package/dist/adapters/factory.d.ts +39 -0
- package/dist/adapters/interface.d.ts +384 -0
- package/dist/adapters/neon.d.ts +105 -0
- package/dist/adapters/pg-shared.d.ts +194 -0
- package/dist/adapters/pg.d.ts +101 -0
- package/dist/adapters/schema.d.ts +57 -0
- package/dist/adapters/schema.sqlite.d.ts +80 -0
- package/dist/chunk-7NMQFTDJ.cjs +148 -0
- package/dist/{chunk-GOBCWCJP.mjs → chunk-GN44PST5.mjs} +68 -1
- package/dist/chunk-RL7V7VSS.mjs +8784 -0
- package/dist/chunk-TKDISKCF.cjs +163 -0
- package/dist/chunk-XKUWMYVF.mjs +163 -0
- package/dist/chunk-YBDQJVFQ.cjs +8784 -0
- package/dist/cloudflare/single-user-worker.d.ts +39 -0
- package/dist/cloudflare.cjs +35 -2
- package/dist/cloudflare.d.ts +11 -2
- package/dist/cloudflare.mjs +36 -3
- package/dist/handlers/cancel-message.d.ts +3 -0
- package/dist/handlers/capabilities.d.ts +4 -0
- package/dist/handlers/client-state-namespaces.d.ts +5 -0
- package/dist/handlers/client-state.d.ts +7 -0
- package/dist/handlers/cloud-data.d.ts +4 -0
- package/dist/handlers/get-message.d.ts +3 -0
- package/dist/handlers/get-user-key.d.ts +3 -0
- package/dist/handlers/init-tenant.d.ts +23 -0
- package/dist/handlers/llm-credentials.d.ts +5 -0
- package/dist/handlers/messages.d.ts +3 -0
- package/dist/handlers/outbox.d.ts +9 -0
- package/dist/handlers/push-subscription.d.ts +5 -0
- package/dist/handlers/schedule-message.d.ts +3 -0
- package/dist/handlers/send-notifications.d.ts +3 -0
- package/dist/handlers/single-user-init.d.ts +13 -0
- package/dist/handlers/update-message.d.ts +3 -0
- package/dist/handlers/vapid-public-key.d.ts +19 -0
- package/dist/index.cjs +133 -44
- package/dist/index.d.cts +145 -766
- package/dist/index.d.ts +145 -766
- package/dist/index.mjs +125 -36
- package/dist/lib/agentic-fire.d.ts +177 -0
- package/dist/lib/client-state-store.d.ts +88 -0
- 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/constant-time.d.ts +14 -0
- package/dist/lib/db-errors.d.ts +10 -0
- package/dist/lib/encryption.d.ts +48 -0
- package/dist/lib/errors.d.ts +273 -0
- package/dist/lib/llm-credentials-store.d.ts +147 -0
- package/dist/lib/llm.d.ts +12 -0
- package/dist/lib/message-processor.d.ts +82 -0
- package/dist/lib/outbox-store.d.ts +162 -0
- package/dist/lib/push-policy.d.ts +11 -0
- package/dist/lib/push-subscription-store.d.ts +94 -0
- package/dist/lib/recurrence.d.ts +51 -0
- package/dist/lib/request.d.ts +182 -0
- package/dist/lib/result-emitter.d.ts +60 -0
- package/dist/lib/retry-policy.d.ts +11 -0
- package/dist/lib/run-tick.d.ts +121 -0
- package/dist/lib/schema-version.d.ts +50 -0
- package/dist/lib/state-accessors.d.ts +70 -0
- package/dist/lib/state-chunks.d.ts +65 -0
- package/dist/lib/task-projection.d.ts +92 -0
- package/dist/lib/validation.d.ts +97 -0
- package/dist/lib/version.d.ts +7 -0
- package/dist/lib/webcrypto-utils.d.ts +1 -0
- package/dist/lib/webpush-webcrypto.d.ts +78 -0
- package/dist/{neon-QJ7KYHEX.mjs → neon-GKLT4UY3.mjs} +116 -44
- package/dist/{neon-V4VOH55N.cjs → neon-XHPJZESV.cjs} +121 -49
- package/dist/{pg-6X74HKMX.cjs → pg-FHXQ6BJP.cjs} +109 -48
- package/dist/{pg-DFJSFOJQ.mjs → pg-J4YXRQP5.mjs} +104 -43
- package/dist/single-user.d.ts +106 -0
- package/dist/tenant/blob-store.d.ts +30 -0
- package/dist/tenant/context.d.ts +47 -0
- package/dist/tenant/single-user-context.d.ts +29 -0
- package/dist/tenant/token.d.ts +26 -0
- package/package.json +3 -3
- package/dist/chunk-3QRVP4JE.cjs +0 -2834
- package/dist/chunk-DCKCAGFV.cjs +0 -81
- package/dist/chunk-S6O3MXQO.mjs +0 -2834
- package/dist/cloudflare-kUY6z-k5.d.cts +0 -4460
- package/dist/cloudflare-kUY6z-k5.d.ts +0 -4460
- package/dist/cloudflare.d.cts +0 -2
- package/dist/neon-D8Z2Nn97.d.cts +0 -295
- package/dist/neon-PqNFJIz2.d.ts +0 -295
- package/dist/pg-C48FwCWW.d.cts +0 -289
- package/dist/pg-C5gtWUfU.d.ts +0 -289
- package/dist/schema-BHuyH_6Z.d.cts +0 -88
- package/dist/schema-BHuyH_6Z.d.ts +0 -88
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CORS_ALLOW_HEADERS,
|
|
3
|
+
CORS_ALLOW_METHODS,
|
|
4
|
+
CRED_ID_MAX_LENGTH,
|
|
5
|
+
CRED_PUT_BATCH_MAX,
|
|
6
|
+
CRED_REFS_MAX_ENTRIES,
|
|
7
|
+
CRED_ROWS_PER_USER_MAX,
|
|
8
|
+
DEFAULT_CLAIM_LEASE_MS,
|
|
9
|
+
DEFAULT_HEARTBEAT_LEASE_TTL_MS,
|
|
10
|
+
DEFAULT_LEASE_HEARTBEAT_MS,
|
|
11
|
+
DEFAULT_MAX_DELIVERY_RETRIES,
|
|
12
|
+
DEFAULT_MAX_REQUEST_BODY_BYTES,
|
|
13
|
+
DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE,
|
|
14
|
+
DEFAULT_MAX_TOOL_ITERATIONS,
|
|
15
|
+
DEFAULT_TOTAL_TIMEOUT_MS,
|
|
16
|
+
DeploymentConfigError,
|
|
17
|
+
MAX_DEFER_AFTER_MS,
|
|
2
18
|
MAX_PUSH_PAYLOAD_BYTES,
|
|
19
|
+
MAX_TASK_PAYLOAD_BYTES,
|
|
20
|
+
MIN_SCHEDULE_LEAD_MS,
|
|
21
|
+
NonRetryableError,
|
|
22
|
+
PUSH_ENVELOPE_RESERVED_BYTES,
|
|
23
|
+
SCHEMA_VERSION,
|
|
3
24
|
WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
|
|
4
25
|
WEB_PUSH_MAX_BODY_BYTES,
|
|
26
|
+
advanceOccurrence,
|
|
27
|
+
base64UrlToBytes,
|
|
28
|
+
bytesToBase64Url,
|
|
5
29
|
createCancelMessageHandler,
|
|
6
30
|
createD1Adapter,
|
|
31
|
+
createGetMessageHandler,
|
|
7
32
|
createGetUserKeyHandler,
|
|
33
|
+
createLlmCredentialsHandler,
|
|
8
34
|
createMessagesHandler,
|
|
35
|
+
createPushSubscriptionHandler,
|
|
9
36
|
createScheduleMessageHandler,
|
|
10
37
|
createSingleUserCloudflareWorker,
|
|
11
38
|
createSingleUserServer,
|
|
@@ -15,19 +42,39 @@ import {
|
|
|
15
42
|
decryptPayload,
|
|
16
43
|
deriveUserEncryptionKey,
|
|
17
44
|
encryptForStorage,
|
|
45
|
+
ensureSchema,
|
|
18
46
|
getHeader,
|
|
47
|
+
getSchemaVersion,
|
|
48
|
+
hasChatCredRef,
|
|
49
|
+
isNonRetryableError,
|
|
50
|
+
isValidCredId,
|
|
19
51
|
isValidISO8601,
|
|
52
|
+
isValidTimeZoneId,
|
|
20
53
|
isValidUUID,
|
|
21
54
|
isValidUUIDv4,
|
|
22
55
|
isValidUrl,
|
|
23
56
|
measurePushPayload,
|
|
57
|
+
nextFutureOccurrence,
|
|
24
58
|
parseJsonBody,
|
|
59
|
+
planNextOccurrence,
|
|
60
|
+
readRequestBody,
|
|
61
|
+
resolveLlmCredential,
|
|
25
62
|
runScheduledTick,
|
|
63
|
+
runTask,
|
|
64
|
+
sanitizeErrorSummary,
|
|
65
|
+
summarizeErrorCause,
|
|
66
|
+
supportsLlmCredentialsStore,
|
|
67
|
+
timingSafeEqualBytes,
|
|
68
|
+
utf8,
|
|
69
|
+
utf8Decode,
|
|
26
70
|
validateAvatarUrl,
|
|
71
|
+
validateCredRefs,
|
|
72
|
+
validateCredValue,
|
|
27
73
|
validateLlmMessagesArray,
|
|
28
74
|
validateScheduleMessagePayload,
|
|
29
75
|
validateSplitPattern
|
|
30
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-RL7V7VSS.mjs";
|
|
77
|
+
import "./chunk-GN44PST5.mjs";
|
|
31
78
|
|
|
32
79
|
// src/server/adapters/factory.js
|
|
33
80
|
async function createAdapter(config) {
|
|
@@ -43,11 +90,11 @@ async function createAdapter(config) {
|
|
|
43
90
|
}
|
|
44
91
|
switch (config.driver) {
|
|
45
92
|
case "neon": {
|
|
46
|
-
const { NeonAdapter } = await import("./neon-
|
|
93
|
+
const { NeonAdapter } = await import("./neon-GKLT4UY3.mjs");
|
|
47
94
|
return new NeonAdapter(config.connectionString);
|
|
48
95
|
}
|
|
49
96
|
case "pg": {
|
|
50
|
-
const { PgAdapter } = await import("./pg-
|
|
97
|
+
const { PgAdapter } = await import("./pg-J4YXRQP5.mjs");
|
|
51
98
|
return new PgAdapter(config.connectionString);
|
|
52
99
|
}
|
|
53
100
|
default:
|
|
@@ -216,14 +263,6 @@ function createSendNotificationsHandler(ctx) {
|
|
|
216
263
|
// src/server/tenant/blob-store.js
|
|
217
264
|
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
|
|
218
265
|
var inMemoryNamespaces = /* @__PURE__ */ new Map();
|
|
219
|
-
function base64UrlEncode(input) {
|
|
220
|
-
return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
221
|
-
}
|
|
222
|
-
function base64UrlDecode(input) {
|
|
223
|
-
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
224
|
-
const padLength = (4 - normalized.length % 4) % 4;
|
|
225
|
-
return Buffer.from(normalized + "=".repeat(padLength), "base64");
|
|
226
|
-
}
|
|
227
266
|
function getKekBuffer(kek) {
|
|
228
267
|
const value = String(kek || "").trim();
|
|
229
268
|
if (!value) {
|
|
@@ -237,16 +276,16 @@ function encryptConfig(config, kekBuffer) {
|
|
|
237
276
|
const plaintext = Buffer.from(JSON.stringify(config), "utf8");
|
|
238
277
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
239
278
|
const authTag = cipher.getAuthTag();
|
|
240
|
-
return `v1.${
|
|
279
|
+
return `v1.${bytesToBase64Url(iv)}.${bytesToBase64Url(authTag)}.${bytesToBase64Url(ciphertext)}`;
|
|
241
280
|
}
|
|
242
281
|
function decryptConfig(encrypted, kekBuffer) {
|
|
243
282
|
const parts = String(encrypted || "").split(".");
|
|
244
283
|
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
245
284
|
throw new Error("INVALID_TENANT_CONFIG");
|
|
246
285
|
}
|
|
247
|
-
const iv =
|
|
248
|
-
const authTag =
|
|
249
|
-
const ciphertext =
|
|
286
|
+
const iv = base64UrlToBytes(parts[1]);
|
|
287
|
+
const authTag = base64UrlToBytes(parts[2]);
|
|
288
|
+
const ciphertext = base64UrlToBytes(parts[3]);
|
|
250
289
|
const decipher = createDecipheriv("aes-256-gcm", kekBuffer, iv);
|
|
251
290
|
decipher.setAuthTag(authTag);
|
|
252
291
|
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
@@ -312,17 +351,9 @@ function createTenantBlobStore(options) {
|
|
|
312
351
|
import { createHash as createHash2, randomBytes as randomBytes2, randomUUID } from "crypto";
|
|
313
352
|
|
|
314
353
|
// src/server/tenant/token.js
|
|
315
|
-
import { createHmac
|
|
316
|
-
function base64UrlEncode2(input) {
|
|
317
|
-
return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
318
|
-
}
|
|
319
|
-
function base64UrlDecode2(input) {
|
|
320
|
-
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
321
|
-
const padLength = (4 - normalized.length % 4) % 4;
|
|
322
|
-
return Buffer.from(normalized + "=".repeat(padLength), "base64");
|
|
323
|
-
}
|
|
354
|
+
import { createHmac } from "crypto";
|
|
324
355
|
function sign(input, secret) {
|
|
325
|
-
return
|
|
356
|
+
return bytesToBase64Url(createHmac("sha256", secret).update(input).digest());
|
|
326
357
|
}
|
|
327
358
|
function nowEpochSeconds() {
|
|
328
359
|
return Math.floor(Date.now() / 1e3);
|
|
@@ -338,8 +369,8 @@ function createTenantToken(params, secret) {
|
|
|
338
369
|
v: 1
|
|
339
370
|
};
|
|
340
371
|
const header = { alg: "HS256", typ: "JWT" };
|
|
341
|
-
const encodedHeader =
|
|
342
|
-
const encodedPayload =
|
|
372
|
+
const encodedHeader = bytesToBase64Url(utf8(JSON.stringify(header)));
|
|
373
|
+
const encodedPayload = bytesToBase64Url(utf8(JSON.stringify(payload)));
|
|
343
374
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
344
375
|
const signature = sign(signingInput, secret);
|
|
345
376
|
return `${signingInput}.${signature}`;
|
|
@@ -355,20 +386,20 @@ function verifyTenantToken(token, secret, options = {}) {
|
|
|
355
386
|
const [encodedHeader, encodedPayload, receivedSignature] = parts;
|
|
356
387
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
357
388
|
const expectedSignature = sign(signingInput, secret);
|
|
358
|
-
let
|
|
359
|
-
let
|
|
389
|
+
let receivedBytes;
|
|
390
|
+
let expectedBytes;
|
|
360
391
|
try {
|
|
361
|
-
|
|
362
|
-
|
|
392
|
+
receivedBytes = base64UrlToBytes(receivedSignature);
|
|
393
|
+
expectedBytes = base64UrlToBytes(expectedSignature);
|
|
363
394
|
} catch {
|
|
364
395
|
throw new Error("INVALID_TENANT_AUTH");
|
|
365
396
|
}
|
|
366
|
-
if (
|
|
397
|
+
if (!timingSafeEqualBytes(receivedBytes, expectedBytes)) {
|
|
367
398
|
throw new Error("INVALID_TENANT_AUTH");
|
|
368
399
|
}
|
|
369
400
|
let payload;
|
|
370
401
|
try {
|
|
371
|
-
payload = JSON.parse(
|
|
402
|
+
payload = JSON.parse(utf8Decode(base64UrlToBytes(encodedPayload)));
|
|
372
403
|
} catch {
|
|
373
404
|
throw new Error("INVALID_TENANT_AUTH");
|
|
374
405
|
}
|
|
@@ -442,8 +473,17 @@ function createTenantContextManager(options) {
|
|
|
442
473
|
async function getOrCreateAdapter(dbConfig) {
|
|
443
474
|
const cacheKey = `${dbConfig.driver}:${dbConfig.connectionString}`;
|
|
444
475
|
if (!adapterCache.has(cacheKey)) {
|
|
445
|
-
const
|
|
446
|
-
|
|
476
|
+
const created = (async () => {
|
|
477
|
+
const adapter = await adapterFactory(dbConfig);
|
|
478
|
+
if (typeof adapter.initSchema === "function") {
|
|
479
|
+
await adapter.initSchema();
|
|
480
|
+
}
|
|
481
|
+
return adapter;
|
|
482
|
+
})().catch((error) => {
|
|
483
|
+
adapterCache.delete(cacheKey);
|
|
484
|
+
throw error;
|
|
485
|
+
});
|
|
486
|
+
adapterCache.set(cacheKey, created);
|
|
447
487
|
}
|
|
448
488
|
return adapterCache.get(cacheKey);
|
|
449
489
|
}
|
|
@@ -571,6 +611,14 @@ async function createReiServer(config) {
|
|
|
571
611
|
privateKey: vapid.privateKey || ""
|
|
572
612
|
},
|
|
573
613
|
webpush: webpushModule,
|
|
614
|
+
// 分片传输的限额(与 installReiSW 的 multipart 同一份)。instant 消息在
|
|
615
|
+
// schedule-message 里就地投递、定时消息走 send-notifications 的 tick,两条路
|
|
616
|
+
// 都从这个 ctx 展开,所以配一次两边都认。
|
|
617
|
+
multipart: config.multipart || null,
|
|
618
|
+
// 定时任务投递失败后的重试次数上限(默认 3),send-notifications 展开 ctx
|
|
619
|
+
// 时带进 runScheduledTick。
|
|
620
|
+
maxGenerationRetries: config.maxGenerationRetries,
|
|
621
|
+
maxDeliveryRetries: config.maxDeliveryRetries,
|
|
574
622
|
tenant: {
|
|
575
623
|
initSecret
|
|
576
624
|
},
|
|
@@ -584,14 +632,39 @@ async function createReiServer(config) {
|
|
|
584
632
|
sendNotifications: createSendNotificationsHandler(ctx),
|
|
585
633
|
updateMessage: createUpdateMessageHandler(ctx),
|
|
586
634
|
cancelMessage: createCancelMessageHandler(ctx),
|
|
587
|
-
messages: createMessagesHandler(ctx)
|
|
635
|
+
messages: createMessagesHandler(ctx),
|
|
636
|
+
getMessage: createGetMessageHandler(ctx),
|
|
637
|
+
pushSubscription: createPushSubscriptionHandler(ctx),
|
|
638
|
+
llmCredentials: createLlmCredentialsHandler(ctx)
|
|
588
639
|
}
|
|
589
640
|
};
|
|
590
641
|
}
|
|
591
642
|
export {
|
|
643
|
+
CORS_ALLOW_HEADERS,
|
|
644
|
+
CORS_ALLOW_METHODS,
|
|
645
|
+
CRED_ID_MAX_LENGTH,
|
|
646
|
+
CRED_PUT_BATCH_MAX,
|
|
647
|
+
CRED_REFS_MAX_ENTRIES,
|
|
648
|
+
CRED_ROWS_PER_USER_MAX,
|
|
649
|
+
DEFAULT_CLAIM_LEASE_MS,
|
|
650
|
+
DEFAULT_HEARTBEAT_LEASE_TTL_MS,
|
|
651
|
+
DEFAULT_LEASE_HEARTBEAT_MS,
|
|
652
|
+
DEFAULT_MAX_DELIVERY_RETRIES,
|
|
653
|
+
DEFAULT_MAX_REQUEST_BODY_BYTES,
|
|
654
|
+
DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE,
|
|
655
|
+
DEFAULT_MAX_TOOL_ITERATIONS,
|
|
656
|
+
DEFAULT_TOTAL_TIMEOUT_MS,
|
|
657
|
+
DeploymentConfigError,
|
|
658
|
+
MAX_DEFER_AFTER_MS,
|
|
592
659
|
MAX_PUSH_PAYLOAD_BYTES,
|
|
660
|
+
MAX_TASK_PAYLOAD_BYTES,
|
|
661
|
+
MIN_SCHEDULE_LEAD_MS,
|
|
662
|
+
NonRetryableError,
|
|
663
|
+
PUSH_ENVELOPE_RESERVED_BYTES,
|
|
664
|
+
SCHEMA_VERSION,
|
|
593
665
|
WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
|
|
594
666
|
WEB_PUSH_MAX_BODY_BYTES,
|
|
667
|
+
advanceOccurrence,
|
|
595
668
|
createAdapter,
|
|
596
669
|
createD1Adapter,
|
|
597
670
|
createReiServer,
|
|
@@ -603,13 +676,29 @@ export {
|
|
|
603
676
|
decryptPayload,
|
|
604
677
|
deriveUserEncryptionKey,
|
|
605
678
|
encryptForStorage,
|
|
679
|
+
ensureSchema,
|
|
680
|
+
getSchemaVersion,
|
|
681
|
+
hasChatCredRef,
|
|
682
|
+
isNonRetryableError,
|
|
683
|
+
isValidCredId,
|
|
606
684
|
isValidISO8601,
|
|
685
|
+
isValidTimeZoneId,
|
|
607
686
|
isValidUUID,
|
|
608
687
|
isValidUUIDv4,
|
|
609
688
|
isValidUrl,
|
|
610
689
|
measurePushPayload,
|
|
690
|
+
nextFutureOccurrence,
|
|
691
|
+
planNextOccurrence,
|
|
692
|
+
readRequestBody,
|
|
693
|
+
resolveLlmCredential,
|
|
611
694
|
runScheduledTick,
|
|
695
|
+
runTask,
|
|
696
|
+
sanitizeErrorSummary,
|
|
697
|
+
summarizeErrorCause,
|
|
698
|
+
supportsLlmCredentialsStore,
|
|
612
699
|
validateAvatarUrl,
|
|
700
|
+
validateCredRefs,
|
|
701
|
+
validateCredValue,
|
|
613
702
|
validateLlmMessagesArray,
|
|
614
703
|
validateScheduleMessagePayload,
|
|
615
704
|
validateSplitPattern,
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认 messageId / sessionId 里掺的名义触发时刻后缀(`@<epoch-ms>`)。
|
|
3
|
+
*
|
|
4
|
+
* 循环任务跨天复用同一条任务行,id 只用 task.id 的话,离线设备一次性收到
|
|
5
|
+
* 多天积压的推送(push TTL 有四周)时会在 service worker 端互相去重、在
|
|
6
|
+
* 收件箱按 messageId 覆盖,几天的消息只剩一条。掺入名义触发时刻后每个
|
|
7
|
+
* occurrence 一套 id;同一 occurrence 的重试仍是同一套——重投已送达的段
|
|
8
|
+
* 会被去重,这正是想要的。行上没有可解析的 next_send_at(比如直接喂进来
|
|
9
|
+
* 的内存任务对象)时返回 '',等价于旧格式。
|
|
10
|
+
*
|
|
11
|
+
* @param {{ next_send_at?: string }} task
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function occurrenceSuffix(task: {
|
|
15
|
+
next_send_at?: string;
|
|
16
|
+
}): string;
|
|
17
|
+
/**
|
|
18
|
+
* 本次触发的名义时刻(epoch 毫秒)。行上没有可解析的 next_send_at(比如直接
|
|
19
|
+
* 喂进来的内存任务对象)时返回 null。
|
|
20
|
+
*
|
|
21
|
+
* @param {{ next_send_at?: string }} task
|
|
22
|
+
* @returns {number|null}
|
|
23
|
+
*/
|
|
24
|
+
export function occurrenceMsOf(task: {
|
|
25
|
+
next_send_at?: string;
|
|
26
|
+
}): number | null;
|
|
27
|
+
/**
|
|
28
|
+
* Does this task need the LLM at fire time? Fixed text never does, so it
|
|
29
|
+
* always stays on the legacy path regardless of hooks.
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} decryptedPayload
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export function taskNeedsLlm(decryptedPayload: any): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Frozen, credential-free view of the task for hook authors.
|
|
37
|
+
*
|
|
38
|
+
* nextSendAt 是这条任务原本的触发时刻。run-tick 领取任务时写的是 lease_until,
|
|
39
|
+
* next_send_at 那一列不动,所以这里给出去的和库里的是同一个值——宿主拿它当
|
|
40
|
+
* 时间锚点(窗口判断、缓存键)时对得上。
|
|
41
|
+
*
|
|
42
|
+
* 导出给 run-tick 用:`serializeBy` 要从任务内容里取分组 key,拿到的就该是
|
|
43
|
+
* 这一份(凭据剔掉、和 onBeforeFire 的 ctx.task 同一个形状),宿主不用为了
|
|
44
|
+
* 分组再学第二套字段。
|
|
45
|
+
*/
|
|
46
|
+
export function buildHookTask(task: any, decryptedPayload: any): Readonly<{
|
|
47
|
+
id: any;
|
|
48
|
+
uuid: any;
|
|
49
|
+
nextSendAt: any;
|
|
50
|
+
retryCount: any;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Try the hook-driven fire path for one task.
|
|
54
|
+
*
|
|
55
|
+
* @param {Object} args
|
|
56
|
+
* @param {import('../adapters/interface.js').TaskRow} args.task
|
|
57
|
+
* @param {Object} args.decryptedPayload - decrypted task payload (has credentials; they stop here)
|
|
58
|
+
* @param {string} args.userKey - per-user storage key (for readState decryption)
|
|
59
|
+
* @param {Object} args.ctx - processor ctx ({ db, webpush, vapid, hooks, maxToolIterations, totalTimeoutMs, maxScheduledTasksPerFire })
|
|
60
|
+
* @returns {Promise<{ handled: false }
|
|
61
|
+
* | { handled: true, result: { success: true, messagesSent: number, status: 'finished'|'skipped', iterations: number } }
|
|
62
|
+
* | { handled: true, result: { success: false, deferred: true, retryAfter: string, messagesSent: 0, status: 'deferred', iterations: 0 } }>}
|
|
63
|
+
* `handled: false` → caller falls back to the legacy frozen-prompt path
|
|
64
|
+
* (onBeforeFire returned null).
|
|
65
|
+
* `onBeforeFire` may also return `{ skip: true }` to complete the fire
|
|
66
|
+
* before the first LLM call → `status: 'skipped', iterations: 0`, same
|
|
67
|
+
* success handling as the post-LLM skip-push path.
|
|
68
|
+
* `{ defer: { afterMs } }` → `status: 'deferred'`,`retryAfter` 是「什么时候
|
|
69
|
+
* 再来问」的 ISO 时刻。`success` 给 false 而不是 true:这次什么都没发,调用
|
|
70
|
+
* 方得看 `deferred` 走推迟的收尾;万一有调用方不认识这个字段,也只会把它当
|
|
71
|
+
* 成没发成,不会当成发完了把任务删掉。
|
|
72
|
+
* Failures (timeout / loop exceeded / config errors) throw — the caller's
|
|
73
|
+
* existing error handling turns them into task retry/failure.
|
|
74
|
+
*
|
|
75
|
+
* 收尾回执:onBeforeFire 一旦被调用,上面每一种结局(含抛错)都会调一次
|
|
76
|
+
* `ctx.onFireSettled`,见 notifyFireSettled。
|
|
77
|
+
*/
|
|
78
|
+
export function runAgenticFire({ task, decryptedPayload, userKey, ctx }: {
|
|
79
|
+
task: import("../adapters/interface.js").TaskRow;
|
|
80
|
+
decryptedPayload: any;
|
|
81
|
+
userKey: string;
|
|
82
|
+
ctx: any;
|
|
83
|
+
}): Promise<{
|
|
84
|
+
handled: false;
|
|
85
|
+
} | {
|
|
86
|
+
handled: true;
|
|
87
|
+
result: {
|
|
88
|
+
success: true;
|
|
89
|
+
messagesSent: number;
|
|
90
|
+
status: "finished" | "skipped";
|
|
91
|
+
iterations: number;
|
|
92
|
+
};
|
|
93
|
+
} | {
|
|
94
|
+
handled: true;
|
|
95
|
+
result: {
|
|
96
|
+
success: false;
|
|
97
|
+
deferred: true;
|
|
98
|
+
retryAfter: string;
|
|
99
|
+
messagesSent: 0;
|
|
100
|
+
status: "deferred";
|
|
101
|
+
iterations: 0;
|
|
102
|
+
};
|
|
103
|
+
}>;
|
|
104
|
+
/**
|
|
105
|
+
* 把这一轮的 usage 累加进整次 fire 的合计,返回新的合计(不改传入的对象)。
|
|
106
|
+
*
|
|
107
|
+
* 合计的形状固定是 `{ prompt_tokens, completion_tokens, total_tokens }`,每个字
|
|
108
|
+
* 段是「报过这一项的那几轮」相加的结果,一轮都没报过的那一项是 null。各家供应
|
|
109
|
+
* 商的 usage 形状不齐,能加的尽量加:
|
|
110
|
+
* - prompt / completion 认 OpenAI 的 `prompt_tokens` / `completion_tokens`,
|
|
111
|
+
* 没有就认 Anthropic 风格的 `input_tokens` / `output_tokens`;
|
|
112
|
+
* - 这一轮没报 `total_tokens`、但 prompt 和 completion 都有,就拿两者之和补上;
|
|
113
|
+
* - 只收有限的非负数,别的一律当没报。
|
|
114
|
+
*
|
|
115
|
+
* 这一轮三项都拿不到(没有 usage,或者形状完全认不出)就原样返回之前的合计——
|
|
116
|
+
* 所有轮次都这样的话合计保持 null。
|
|
117
|
+
*
|
|
118
|
+
* @param {{ prompt_tokens: number|null, completion_tokens: number|null, total_tokens: number|null } | null} total
|
|
119
|
+
* @param {unknown} usage - 这一轮 LLM 响应的 usage
|
|
120
|
+
* @returns {{ prompt_tokens: number|null, completion_tokens: number|null, total_tokens: number|null } | null}
|
|
121
|
+
*/
|
|
122
|
+
export function accumulateUsage(total: {
|
|
123
|
+
prompt_tokens: number | null;
|
|
124
|
+
completion_tokens: number | null;
|
|
125
|
+
total_tokens: number | null;
|
|
126
|
+
} | null, usage: unknown): {
|
|
127
|
+
prompt_tokens: number | null;
|
|
128
|
+
completion_tokens: number | null;
|
|
129
|
+
total_tokens: number | null;
|
|
130
|
+
} | null;
|
|
131
|
+
export const DEFAULT_MAX_TOOL_ITERATIONS: 5;
|
|
132
|
+
export const DEFAULT_TOTAL_TIMEOUT_MS: 240000;
|
|
133
|
+
export const DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE: 2;
|
|
134
|
+
export const MIN_SCHEDULE_LEAD_MS: 60000;
|
|
135
|
+
export const MAX_DEFER_AFTER_MS: number;
|
|
136
|
+
/**
|
|
137
|
+
* - 消息数组 / `{ messages, ... }`:用这份 prompt 生成;
|
|
138
|
+
* - `{ skip: true }`:这次不发,按零推送的成功收场(一次性任务删行、循环任务
|
|
139
|
+
* 推进到下一次);
|
|
140
|
+
* - `{ defer: { afterMs } }`:现在不合适,afterMs 毫秒后再来问。afterMs 必须
|
|
141
|
+
* 是有限正数,且不超过 MAX_DEFER_AFTER_MS(24 小时);
|
|
142
|
+
* - `null` / `undefined`:交还给排程时冻结的 prompt 老链路。
|
|
143
|
+
*/
|
|
144
|
+
export type BeforeFireResult = Array<any> | {
|
|
145
|
+
messages: Array<any>;
|
|
146
|
+
maxToolIterations?: number;
|
|
147
|
+
totalTimeoutMs?: number;
|
|
148
|
+
tools?: Array<any>;
|
|
149
|
+
toolChoice?: any;
|
|
150
|
+
} | {
|
|
151
|
+
skip: true;
|
|
152
|
+
} | {
|
|
153
|
+
defer: {
|
|
154
|
+
afterMs: number;
|
|
155
|
+
};
|
|
156
|
+
} | null | undefined;
|
|
157
|
+
/**
|
|
158
|
+
* 收尾回执(onFireSettled)的 status。
|
|
159
|
+
*/
|
|
160
|
+
export type FireSettledStatus = "sent" | "skipped" | "deferred" | "failed" | "cancelled" | "not-handled";
|
|
161
|
+
/**
|
|
162
|
+
* 把任务的调度身份投影进一条 push。
|
|
163
|
+
*
|
|
164
|
+
* 客户端收到推送时要知道「这是哪条任务、哪一次触发、它还会不会再来」——不然
|
|
165
|
+
* 角色在 fire 里自排的任务客户端从没见过,只能靠猜。这三个字段由库统一补,
|
|
166
|
+
* 宿主不用再往 metadata 里抄一遍(抄漏一个就够坏事了)。
|
|
167
|
+
*
|
|
168
|
+
* 放在 push 顶层,和 messageId / sessionId / timestamp / messageIndex /
|
|
169
|
+
* totalMessages 一样,不塞进 metadata:metadata 是宿主自己的地盘,库往里写
|
|
170
|
+
* 会和宿主的键打架。
|
|
171
|
+
*
|
|
172
|
+
* @param {Object} push - 待发送的 push(原地补字段)
|
|
173
|
+
* @param {Object} task - 数据库任务行
|
|
174
|
+
* @param {Object} decryptedPayload
|
|
175
|
+
* @param {number|null} occurrenceMs
|
|
176
|
+
*/
|
|
177
|
+
export function stampTaskIdentity(push: any, task: any, decryptedPayload: any, occurrenceMs: number | null): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** value 的 UTF-8 字节数——各处限额判断用的都是这个口径。 */
|
|
2
|
+
export function stateValueBytes(value: any): number;
|
|
3
|
+
/**
|
|
4
|
+
* 把 `clientStateTtl` 配置(`{ 命名空间: 天数 }`)算成清理指令。
|
|
5
|
+
*
|
|
6
|
+
* 一个命名空间出两条:它自己,加上它的切片命名空间(大值分块存在那儿,见
|
|
7
|
+
* lib/state-chunks.js)。两边共用同一个截止时刻——同一次写入的根行和切片行
|
|
8
|
+
* `updated_at` 相同,所以要么一起留、要么一起走,不会留下半截数据。
|
|
9
|
+
*
|
|
10
|
+
* 天数不是正数的条目跳过并告警(每个键只说一次):配错了就该看得见,但不能
|
|
11
|
+
* 让 cron 每分钟刷一条。
|
|
12
|
+
*
|
|
13
|
+
* @param {Record<string, number>|null|undefined} ttl - `{ 命名空间: 天数 }`;天数可带小数
|
|
14
|
+
* @param {number} now - 当前时刻(epoch 毫秒)
|
|
15
|
+
* @returns {Array<{ namespace: string, updatedBefore: number }>}
|
|
16
|
+
* `updatedBefore` 是 epoch 毫秒:这个命名空间下 `updated_at` 早于它的条目该清掉。
|
|
17
|
+
*/
|
|
18
|
+
export function planClientStateCleanup(ttl: Record<string, number> | null | undefined, now: number): Array<{
|
|
19
|
+
namespace: string;
|
|
20
|
+
updatedBefore: number;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* 把逻辑条目写进 client_state。条目要么是覆盖写(`value` 是字符串),要么是
|
|
24
|
+
* 删除(`value` 为 `null`):
|
|
25
|
+
*
|
|
26
|
+
* - 覆盖写:≤ 200KB 单行存;超过的切片跨行存(见 lib/state-chunks.js)。
|
|
27
|
+
* 同一批里先清掉这个 key 上次写入留下的切片行,所以覆盖成更小的值、或者
|
|
28
|
+
* 块数变少,都不会留下尾巴。
|
|
29
|
+
* - 删除:根行按精确 key 删,切片行按 key 前缀删,一个 key 的数据清干净。
|
|
30
|
+
*
|
|
31
|
+
* 两者都受同一套 last-write-wins 约束:`updatedAt` 比库里已有值旧的写入
|
|
32
|
+
* (或删除)不生效,陈旧批次盖不掉新数据。唯一的例外是库里那行「来自未来」
|
|
33
|
+
* (`updated_at` 晚于服务端当前时刻)——那种行只可能出自跑偏的设备时钟,不当
|
|
34
|
+
* 比较基准,写入放行,详见 adapters/d1.js 的 `upsertClientState`。
|
|
35
|
+
*
|
|
36
|
+
* 条件写护栏:条目可带可选的 `version`(毫秒时间戳或单调递增整数)。带了它,
|
|
37
|
+
* 比较用的就是这个值而不是 `updatedAt`——同一个 key 有多个写入方时(例如
|
|
38
|
+
* fire_pack 由常规 flush 和 instant-chat 两条路径写),谁的**内容**新谁赢,
|
|
39
|
+
* 而不是谁的请求后到谁赢:慢网下晚到的旧包带着旧 `version`,盖不掉先到的新
|
|
40
|
+
* 包。护栏值落在行的 updated_at 列上(client_state 的比较列本来就是它),
|
|
41
|
+
* 没带 `version` 的写入照旧按 `updatedAt` 比。
|
|
42
|
+
*
|
|
43
|
+
* 条目本身的合法性(namespace / key 字符、value 大小)由调用方先校验好:
|
|
44
|
+
* HTTP handler 逐条拒绝并把原因回给客户端,`writeState()` 直接抛错给 hook。
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} args
|
|
47
|
+
* @param {{ upsertClientState: Function }} args.db
|
|
48
|
+
* @param {string} args.userId
|
|
49
|
+
* @param {CryptoKey|string} args.userKey - 该用户的存储密钥(value 用它加密)
|
|
50
|
+
* @param {Array<{ namespace: string, key: string, value: string|null, updatedAt: number, version?: number }>} args.entries
|
|
51
|
+
* `version`(可选):条件写护栏值,见文件头。带了它,这条的 last-write-wins
|
|
52
|
+
* 比较用它(写进行的 updated_at 列);没带照旧用 `updatedAt`。
|
|
53
|
+
* @param {() => number} [args.now] - 取服务端当前时刻(测试可注入假时钟)。适配器
|
|
54
|
+
* 拿它认出库里「来自未来」的脏行。
|
|
55
|
+
* @returns {Promise<{ upserted: number, skipped: number, deleted: number, skippedEntries: Array<{ namespace: string, key: string }> }>}
|
|
56
|
+
* `upserted` / `skipped` / `deleted` 按逻辑条目计(切片行不计),三者之和等于
|
|
57
|
+
* 条目数。`deleted` 是删除之后 key 已不在库里的条数——删掉了、或本来就没有,
|
|
58
|
+
* 两种都算;被 last-write-wins 拦下的删除(库里那行更新)计入 `skipped`。
|
|
59
|
+
* `skippedEntries` 逐条列出被拦下的 key,写入与删除都在里面(适配器不回
|
|
60
|
+
* outcomes / cleanupOutcomes 时分不清是哪条:写入按物理行计数兜底,删除一律
|
|
61
|
+
* 按「已删」计)。
|
|
62
|
+
*/
|
|
63
|
+
export function writeClientStateEntries({ db, userId, userKey, entries, now }: {
|
|
64
|
+
db: {
|
|
65
|
+
upsertClientState: Function;
|
|
66
|
+
};
|
|
67
|
+
userId: string;
|
|
68
|
+
userKey: CryptoKey | string;
|
|
69
|
+
entries: Array<{
|
|
70
|
+
namespace: string;
|
|
71
|
+
key: string;
|
|
72
|
+
value: string | null;
|
|
73
|
+
updatedAt: number;
|
|
74
|
+
version?: number;
|
|
75
|
+
}>;
|
|
76
|
+
now?: () => number;
|
|
77
|
+
}): Promise<{
|
|
78
|
+
upserted: number;
|
|
79
|
+
skipped: number;
|
|
80
|
+
deleted: number;
|
|
81
|
+
skippedEntries: Array<{
|
|
82
|
+
namespace: string;
|
|
83
|
+
key: string;
|
|
84
|
+
}>;
|
|
85
|
+
}>;
|
|
86
|
+
export const MAX_STATE_ENTRIES_PER_BATCH: 200;
|
|
87
|
+
export const MAX_NAMESPACE_CHARS: 128;
|
|
88
|
+
export const MAX_KEY_CHARS: 256;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function cloudError(code: any, message: any, status?: number): Error & {
|
|
2
|
+
code: any;
|
|
3
|
+
status: number;
|
|
4
|
+
};
|
|
5
|
+
export function validCloudOwner(owner: any): boolean;
|
|
6
|
+
export function cloudInventory(ctx: any, db: any, userId: any, userKey: any): Promise<{
|
|
7
|
+
entries: any[];
|
|
8
|
+
complete: boolean;
|
|
9
|
+
gaps: any[];
|
|
10
|
+
}>;
|
|
11
|
+
export function readCloudRecord(db: any, userId: any, key: any, kind: any, id: any): Promise<any>;
|
|
12
|
+
export function publicOperation(operation: any): any;
|
|
13
|
+
export function makeCleanupPlan(ctx: any, db: any, userId: any, key: any, input: any): Promise<{
|
|
14
|
+
id: `${string}-${string}-${string}-${string}-${string}`;
|
|
15
|
+
mode: any;
|
|
16
|
+
ownerGeneration: any;
|
|
17
|
+
owner: {
|
|
18
|
+
type: any;
|
|
19
|
+
id: any;
|
|
20
|
+
label: any;
|
|
21
|
+
};
|
|
22
|
+
resources: any[];
|
|
23
|
+
count: number;
|
|
24
|
+
counts: {
|
|
25
|
+
type: string;
|
|
26
|
+
count: number;
|
|
27
|
+
}[];
|
|
28
|
+
expiresAt: number;
|
|
29
|
+
impacts: string[];
|
|
30
|
+
complete: boolean;
|
|
31
|
+
gaps: any[];
|
|
32
|
+
}>;
|
|
33
|
+
export function startCleanup(ctx: any, db: any, userId: any, key: any, input: any): Promise<any>;
|
|
34
|
+
export function advanceCleanup(ctx: any, db: any, userId: any, key: any, operation: any): Promise<any>;
|
|
35
|
+
export function resumeCloudDataCleanups(ctx: any): Promise<void>;
|
|
36
|
+
export const CLOUD_RESOURCE_TYPES: string[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Check at task start and immediately before external delivery. Writes must
|
|
2
|
+
* also use the adapter's transactional guard; this read is not a write lock. */
|
|
3
|
+
export function assertCloudGuard(db: any, userId: any, guard: any): Promise<void>;
|
|
4
|
+
/** Capture persisted task identity once. Missing generations deliberately mean
|
|
5
|
+
* zero, never the current generation: restoring must not revive old requests. */
|
|
6
|
+
export function captureCloudTaskGuard(db: any, userId: any, payload: any, resolveOwner: any, task: any): Promise<{
|
|
7
|
+
owner: any;
|
|
8
|
+
generation: any;
|
|
9
|
+
}>;
|
|
10
|
+
/** Add ownership and transactional guard arguments at the storage boundary.
|
|
11
|
+
* A task-scoped inherited guard always wins over caller-provided output fields. */
|
|
12
|
+
export function createCloudGuardedAdapter(db: any, { masterKey, resolveOwner, guard: inheritedGuard, userId: scopedUserId }?: {
|
|
13
|
+
guard?: any;
|
|
14
|
+
}): any;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function cloudDataHash(value: any): Promise<string>;
|
|
2
|
+
/**
|
|
3
|
+
* Internal entries retain exact encrypted snapshots for conditional deletion.
|
|
4
|
+
* Only `entry.resource` may cross the API response boundary.
|
|
5
|
+
*/
|
|
6
|
+
export function buildCloudDataInventory({ db, userId, userKey, resolveOwner }: {
|
|
7
|
+
db: any;
|
|
8
|
+
userId: any;
|
|
9
|
+
userKey: any;
|
|
10
|
+
resolveOwner: any;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
entries: any[];
|
|
13
|
+
complete: boolean;
|
|
14
|
+
gaps: any[];
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable constant-time string comparison.
|
|
3
|
+
*
|
|
4
|
+
* Runs identically on Node (tests) and Cloudflare Workers (prod). We avoid
|
|
5
|
+
* both node:crypto's timingSafeEqual (undefined on Workers historically) and
|
|
6
|
+
* crypto.subtle.timingSafeEqual (absent on Node). Instead we HMAC-SHA256 both
|
|
7
|
+
* inputs under one fresh random key, then compare the two digests. Because the
|
|
8
|
+
* key is random per call, an attacker can't precompute or replay a stable
|
|
9
|
+
* timing oracle; digests are a fixed 32 bytes, so the XOR-accumulate compare
|
|
10
|
+
* runs the same number of steps regardless of input length, with no early-out.
|
|
11
|
+
*
|
|
12
|
+
* globalThis.crypto (Web Crypto) is available on Node >= 20 and on Workers.
|
|
13
|
+
*/
|
|
14
|
+
export function constantTimeEqual(a: any, b: any): Promise<boolean>;
|