@rei-standard/amsg-server 2.6.0-next.2 → 2.6.0-next.21
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 +599 -24
- package/dist/adapters/d1.d.ts +416 -0
- package/dist/adapters/factory.d.ts +39 -0
- package/dist/adapters/interface.d.ts +306 -0
- package/dist/adapters/neon.d.ts +88 -0
- package/dist/adapters/pg-shared.d.ts +161 -0
- package/dist/adapters/pg.d.ts +84 -0
- package/dist/adapters/schema.d.ts +57 -0
- package/dist/adapters/schema.sqlite.d.ts +49 -0
- package/dist/chunk-7NMQFTDJ.cjs +148 -0
- package/dist/chunk-E7OWP3VL.cjs +135 -0
- package/dist/chunk-GN44PST5.mjs +148 -0
- package/dist/chunk-KOJQVOYS.mjs +5847 -0
- package/dist/chunk-QHVID3V2.cjs +5847 -0
- package/dist/chunk-ZGGF4GMA.mjs +135 -0
- package/dist/cloudflare/single-user-worker.d.ts +37 -0
- package/dist/cloudflare.cjs +39 -2
- package/dist/cloudflare.d.ts +10 -2
- package/dist/cloudflare.mjs +40 -3
- package/dist/handlers/cancel-message.d.ts +3 -0
- package/dist/handlers/capabilities.d.ts +4 -0
- package/dist/handlers/client-state.d.ts +7 -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 +7 -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 +131 -766
- package/dist/index.d.ts +131 -766
- package/dist/index.mjs +125 -36
- package/dist/lib/agentic-fire.d.ts +108 -0
- package/dist/lib/client-state-store.d.ts +80 -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 +215 -0
- package/dist/lib/llm-credentials-store.d.ts +147 -0
- package/dist/lib/llm.d.ts +1 -0
- package/dist/lib/message-processor.d.ts +73 -0
- package/dist/lib/outbox-store.d.ts +95 -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 +54 -0
- package/dist/lib/run-tick.d.ts +115 -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 +55 -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-BFUS25UX.cjs → neon-KP2CPA57.cjs} +110 -10
- package/dist/{neon-CU5N3CSW.mjs → neon-ZIMHALYI.mjs} +106 -6
- package/dist/{pg-QO6NKTGL.mjs → pg-QGC2XHUT.mjs} +94 -5
- package/dist/{pg-IIH3M4OM.cjs → pg-RZPQGXR3.cjs} +98 -9
- package/dist/single-user.d.ts +95 -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-5ENJGVZX.mjs +0 -72
- package/dist/chunk-GN5QTMCL.cjs +0 -1870
- package/dist/chunk-PVNLB6TF.mjs +0 -1870
- package/dist/chunk-RGECD4OH.cjs +0 -72
- package/dist/cloudflare-B-E51VjP.d.cts +0 -2848
- package/dist/cloudflare-B-E51VjP.d.ts +0 -2848
- package/dist/cloudflare.d.cts +0 -2
- package/dist/neon-CJl66EGy.d.cts +0 -251
- package/dist/neon-DYvGnCzx.d.ts +0 -251
- package/dist/pg-07S-u_H4.d.cts +0 -246
- package/dist/pg-Du-pN_UT.d.ts +0 -246
- package/dist/schema-C8OnYk6j.d.cts +0 -74
- package/dist/schema-C8OnYk6j.d.ts +0 -74
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
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_REQUEST_BODY_BYTES,
|
|
12
|
+
DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE,
|
|
13
|
+
DEFAULT_MAX_TOOL_ITERATIONS,
|
|
14
|
+
DEFAULT_TOTAL_TIMEOUT_MS,
|
|
15
|
+
DeploymentConfigError,
|
|
16
|
+
MAX_PUSH_PAYLOAD_BYTES,
|
|
17
|
+
MAX_TASK_PAYLOAD_BYTES,
|
|
18
|
+
MIN_SCHEDULE_LEAD_MS,
|
|
19
|
+
NonRetryableError,
|
|
20
|
+
PUSH_ENVELOPE_RESERVED_BYTES,
|
|
21
|
+
SCHEMA_VERSION,
|
|
22
|
+
WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
|
|
23
|
+
WEB_PUSH_MAX_BODY_BYTES,
|
|
24
|
+
advanceOccurrence,
|
|
25
|
+
base64UrlToBytes,
|
|
26
|
+
bytesToBase64Url,
|
|
2
27
|
createCancelMessageHandler,
|
|
3
28
|
createD1Adapter,
|
|
29
|
+
createGetMessageHandler,
|
|
4
30
|
createGetUserKeyHandler,
|
|
31
|
+
createLlmCredentialsHandler,
|
|
5
32
|
createMessagesHandler,
|
|
33
|
+
createPushSubscriptionHandler,
|
|
6
34
|
createScheduleMessageHandler,
|
|
7
35
|
createSingleUserCloudflareWorker,
|
|
8
36
|
createSingleUserServer,
|
|
@@ -12,18 +40,39 @@ import {
|
|
|
12
40
|
decryptPayload,
|
|
13
41
|
deriveUserEncryptionKey,
|
|
14
42
|
encryptForStorage,
|
|
43
|
+
ensureSchema,
|
|
15
44
|
getHeader,
|
|
45
|
+
getSchemaVersion,
|
|
46
|
+
hasChatCredRef,
|
|
47
|
+
isNonRetryableError,
|
|
48
|
+
isValidCredId,
|
|
16
49
|
isValidISO8601,
|
|
50
|
+
isValidTimeZoneId,
|
|
17
51
|
isValidUUID,
|
|
18
52
|
isValidUUIDv4,
|
|
19
53
|
isValidUrl,
|
|
54
|
+
measurePushPayload,
|
|
55
|
+
nextFutureOccurrence,
|
|
20
56
|
parseJsonBody,
|
|
57
|
+
planNextOccurrence,
|
|
58
|
+
readRequestBody,
|
|
59
|
+
resolveLlmCredential,
|
|
21
60
|
runScheduledTick,
|
|
61
|
+
runTask,
|
|
62
|
+
sanitizeErrorSummary,
|
|
63
|
+
summarizeErrorCause,
|
|
64
|
+
supportsLlmCredentialsStore,
|
|
65
|
+
timingSafeEqualBytes,
|
|
66
|
+
utf8,
|
|
67
|
+
utf8Decode,
|
|
22
68
|
validateAvatarUrl,
|
|
69
|
+
validateCredRefs,
|
|
70
|
+
validateCredValue,
|
|
23
71
|
validateLlmMessagesArray,
|
|
24
72
|
validateScheduleMessagePayload,
|
|
25
73
|
validateSplitPattern
|
|
26
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-KOJQVOYS.mjs";
|
|
75
|
+
import "./chunk-GN44PST5.mjs";
|
|
27
76
|
|
|
28
77
|
// src/server/adapters/factory.js
|
|
29
78
|
async function createAdapter(config) {
|
|
@@ -39,11 +88,11 @@ async function createAdapter(config) {
|
|
|
39
88
|
}
|
|
40
89
|
switch (config.driver) {
|
|
41
90
|
case "neon": {
|
|
42
|
-
const { NeonAdapter } = await import("./neon-
|
|
91
|
+
const { NeonAdapter } = await import("./neon-ZIMHALYI.mjs");
|
|
43
92
|
return new NeonAdapter(config.connectionString);
|
|
44
93
|
}
|
|
45
94
|
case "pg": {
|
|
46
|
-
const { PgAdapter } = await import("./pg-
|
|
95
|
+
const { PgAdapter } = await import("./pg-QGC2XHUT.mjs");
|
|
47
96
|
return new PgAdapter(config.connectionString);
|
|
48
97
|
}
|
|
49
98
|
default:
|
|
@@ -212,14 +261,6 @@ function createSendNotificationsHandler(ctx) {
|
|
|
212
261
|
// src/server/tenant/blob-store.js
|
|
213
262
|
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
|
|
214
263
|
var inMemoryNamespaces = /* @__PURE__ */ new Map();
|
|
215
|
-
function base64UrlEncode(input) {
|
|
216
|
-
return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
217
|
-
}
|
|
218
|
-
function base64UrlDecode(input) {
|
|
219
|
-
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
220
|
-
const padLength = (4 - normalized.length % 4) % 4;
|
|
221
|
-
return Buffer.from(normalized + "=".repeat(padLength), "base64");
|
|
222
|
-
}
|
|
223
264
|
function getKekBuffer(kek) {
|
|
224
265
|
const value = String(kek || "").trim();
|
|
225
266
|
if (!value) {
|
|
@@ -233,16 +274,16 @@ function encryptConfig(config, kekBuffer) {
|
|
|
233
274
|
const plaintext = Buffer.from(JSON.stringify(config), "utf8");
|
|
234
275
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
235
276
|
const authTag = cipher.getAuthTag();
|
|
236
|
-
return `v1.${
|
|
277
|
+
return `v1.${bytesToBase64Url(iv)}.${bytesToBase64Url(authTag)}.${bytesToBase64Url(ciphertext)}`;
|
|
237
278
|
}
|
|
238
279
|
function decryptConfig(encrypted, kekBuffer) {
|
|
239
280
|
const parts = String(encrypted || "").split(".");
|
|
240
281
|
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
241
282
|
throw new Error("INVALID_TENANT_CONFIG");
|
|
242
283
|
}
|
|
243
|
-
const iv =
|
|
244
|
-
const authTag =
|
|
245
|
-
const ciphertext =
|
|
284
|
+
const iv = base64UrlToBytes(parts[1]);
|
|
285
|
+
const authTag = base64UrlToBytes(parts[2]);
|
|
286
|
+
const ciphertext = base64UrlToBytes(parts[3]);
|
|
246
287
|
const decipher = createDecipheriv("aes-256-gcm", kekBuffer, iv);
|
|
247
288
|
decipher.setAuthTag(authTag);
|
|
248
289
|
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
@@ -308,17 +349,9 @@ function createTenantBlobStore(options) {
|
|
|
308
349
|
import { createHash as createHash2, randomBytes as randomBytes2, randomUUID } from "crypto";
|
|
309
350
|
|
|
310
351
|
// src/server/tenant/token.js
|
|
311
|
-
import { createHmac
|
|
312
|
-
function base64UrlEncode2(input) {
|
|
313
|
-
return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
314
|
-
}
|
|
315
|
-
function base64UrlDecode2(input) {
|
|
316
|
-
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
317
|
-
const padLength = (4 - normalized.length % 4) % 4;
|
|
318
|
-
return Buffer.from(normalized + "=".repeat(padLength), "base64");
|
|
319
|
-
}
|
|
352
|
+
import { createHmac } from "crypto";
|
|
320
353
|
function sign(input, secret) {
|
|
321
|
-
return
|
|
354
|
+
return bytesToBase64Url(createHmac("sha256", secret).update(input).digest());
|
|
322
355
|
}
|
|
323
356
|
function nowEpochSeconds() {
|
|
324
357
|
return Math.floor(Date.now() / 1e3);
|
|
@@ -334,8 +367,8 @@ function createTenantToken(params, secret) {
|
|
|
334
367
|
v: 1
|
|
335
368
|
};
|
|
336
369
|
const header = { alg: "HS256", typ: "JWT" };
|
|
337
|
-
const encodedHeader =
|
|
338
|
-
const encodedPayload =
|
|
370
|
+
const encodedHeader = bytesToBase64Url(utf8(JSON.stringify(header)));
|
|
371
|
+
const encodedPayload = bytesToBase64Url(utf8(JSON.stringify(payload)));
|
|
339
372
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
340
373
|
const signature = sign(signingInput, secret);
|
|
341
374
|
return `${signingInput}.${signature}`;
|
|
@@ -351,20 +384,20 @@ function verifyTenantToken(token, secret, options = {}) {
|
|
|
351
384
|
const [encodedHeader, encodedPayload, receivedSignature] = parts;
|
|
352
385
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
353
386
|
const expectedSignature = sign(signingInput, secret);
|
|
354
|
-
let
|
|
355
|
-
let
|
|
387
|
+
let receivedBytes;
|
|
388
|
+
let expectedBytes;
|
|
356
389
|
try {
|
|
357
|
-
|
|
358
|
-
|
|
390
|
+
receivedBytes = base64UrlToBytes(receivedSignature);
|
|
391
|
+
expectedBytes = base64UrlToBytes(expectedSignature);
|
|
359
392
|
} catch {
|
|
360
393
|
throw new Error("INVALID_TENANT_AUTH");
|
|
361
394
|
}
|
|
362
|
-
if (
|
|
395
|
+
if (!timingSafeEqualBytes(receivedBytes, expectedBytes)) {
|
|
363
396
|
throw new Error("INVALID_TENANT_AUTH");
|
|
364
397
|
}
|
|
365
398
|
let payload;
|
|
366
399
|
try {
|
|
367
|
-
payload = JSON.parse(
|
|
400
|
+
payload = JSON.parse(utf8Decode(base64UrlToBytes(encodedPayload)));
|
|
368
401
|
} catch {
|
|
369
402
|
throw new Error("INVALID_TENANT_AUTH");
|
|
370
403
|
}
|
|
@@ -438,8 +471,17 @@ function createTenantContextManager(options) {
|
|
|
438
471
|
async function getOrCreateAdapter(dbConfig) {
|
|
439
472
|
const cacheKey = `${dbConfig.driver}:${dbConfig.connectionString}`;
|
|
440
473
|
if (!adapterCache.has(cacheKey)) {
|
|
441
|
-
const
|
|
442
|
-
|
|
474
|
+
const created = (async () => {
|
|
475
|
+
const adapter = await adapterFactory(dbConfig);
|
|
476
|
+
if (typeof adapter.initSchema === "function") {
|
|
477
|
+
await adapter.initSchema();
|
|
478
|
+
}
|
|
479
|
+
return adapter;
|
|
480
|
+
})().catch((error) => {
|
|
481
|
+
adapterCache.delete(cacheKey);
|
|
482
|
+
throw error;
|
|
483
|
+
});
|
|
484
|
+
adapterCache.set(cacheKey, created);
|
|
443
485
|
}
|
|
444
486
|
return adapterCache.get(cacheKey);
|
|
445
487
|
}
|
|
@@ -567,6 +609,10 @@ async function createReiServer(config) {
|
|
|
567
609
|
privateKey: vapid.privateKey || ""
|
|
568
610
|
},
|
|
569
611
|
webpush: webpushModule,
|
|
612
|
+
// 分片传输的限额(与 installReiSW 的 multipart 同一份)。instant 消息在
|
|
613
|
+
// schedule-message 里就地投递、定时消息走 send-notifications 的 tick,两条路
|
|
614
|
+
// 都从这个 ctx 展开,所以配一次两边都认。
|
|
615
|
+
multipart: config.multipart || null,
|
|
570
616
|
tenant: {
|
|
571
617
|
initSecret
|
|
572
618
|
},
|
|
@@ -580,11 +626,37 @@ async function createReiServer(config) {
|
|
|
580
626
|
sendNotifications: createSendNotificationsHandler(ctx),
|
|
581
627
|
updateMessage: createUpdateMessageHandler(ctx),
|
|
582
628
|
cancelMessage: createCancelMessageHandler(ctx),
|
|
583
|
-
messages: createMessagesHandler(ctx)
|
|
629
|
+
messages: createMessagesHandler(ctx),
|
|
630
|
+
getMessage: createGetMessageHandler(ctx),
|
|
631
|
+
pushSubscription: createPushSubscriptionHandler(ctx),
|
|
632
|
+
llmCredentials: createLlmCredentialsHandler(ctx)
|
|
584
633
|
}
|
|
585
634
|
};
|
|
586
635
|
}
|
|
587
636
|
export {
|
|
637
|
+
CORS_ALLOW_HEADERS,
|
|
638
|
+
CORS_ALLOW_METHODS,
|
|
639
|
+
CRED_ID_MAX_LENGTH,
|
|
640
|
+
CRED_PUT_BATCH_MAX,
|
|
641
|
+
CRED_REFS_MAX_ENTRIES,
|
|
642
|
+
CRED_ROWS_PER_USER_MAX,
|
|
643
|
+
DEFAULT_CLAIM_LEASE_MS,
|
|
644
|
+
DEFAULT_HEARTBEAT_LEASE_TTL_MS,
|
|
645
|
+
DEFAULT_LEASE_HEARTBEAT_MS,
|
|
646
|
+
DEFAULT_MAX_REQUEST_BODY_BYTES,
|
|
647
|
+
DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE,
|
|
648
|
+
DEFAULT_MAX_TOOL_ITERATIONS,
|
|
649
|
+
DEFAULT_TOTAL_TIMEOUT_MS,
|
|
650
|
+
DeploymentConfigError,
|
|
651
|
+
MAX_PUSH_PAYLOAD_BYTES,
|
|
652
|
+
MAX_TASK_PAYLOAD_BYTES,
|
|
653
|
+
MIN_SCHEDULE_LEAD_MS,
|
|
654
|
+
NonRetryableError,
|
|
655
|
+
PUSH_ENVELOPE_RESERVED_BYTES,
|
|
656
|
+
SCHEMA_VERSION,
|
|
657
|
+
WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
|
|
658
|
+
WEB_PUSH_MAX_BODY_BYTES,
|
|
659
|
+
advanceOccurrence,
|
|
588
660
|
createAdapter,
|
|
589
661
|
createD1Adapter,
|
|
590
662
|
createReiServer,
|
|
@@ -596,12 +668,29 @@ export {
|
|
|
596
668
|
decryptPayload,
|
|
597
669
|
deriveUserEncryptionKey,
|
|
598
670
|
encryptForStorage,
|
|
671
|
+
ensureSchema,
|
|
672
|
+
getSchemaVersion,
|
|
673
|
+
hasChatCredRef,
|
|
674
|
+
isNonRetryableError,
|
|
675
|
+
isValidCredId,
|
|
599
676
|
isValidISO8601,
|
|
677
|
+
isValidTimeZoneId,
|
|
600
678
|
isValidUUID,
|
|
601
679
|
isValidUUIDv4,
|
|
602
680
|
isValidUrl,
|
|
681
|
+
measurePushPayload,
|
|
682
|
+
nextFutureOccurrence,
|
|
683
|
+
planNextOccurrence,
|
|
684
|
+
readRequestBody,
|
|
685
|
+
resolveLlmCredential,
|
|
603
686
|
runScheduledTick,
|
|
687
|
+
runTask,
|
|
688
|
+
sanitizeErrorSummary,
|
|
689
|
+
summarizeErrorCause,
|
|
690
|
+
supportsLlmCredentialsStore,
|
|
604
691
|
validateAvatarUrl,
|
|
692
|
+
validateCredRefs,
|
|
693
|
+
validateCredValue,
|
|
605
694
|
validateLlmMessagesArray,
|
|
606
695
|
validateScheduleMessagePayload,
|
|
607
696
|
validateSplitPattern,
|
|
@@ -0,0 +1,108 @@
|
|
|
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 } | { handled: true, result: { success: true, messagesSent: number, status: 'finished'|'skipped', iterations: number } }>}
|
|
61
|
+
* `handled: false` → caller falls back to the legacy frozen-prompt path
|
|
62
|
+
* (onBeforeFire returned null).
|
|
63
|
+
* `onBeforeFire` may also return `{ skip: true }` to complete the fire
|
|
64
|
+
* before the first LLM call → `status: 'skipped', iterations: 0`, same
|
|
65
|
+
* success handling as the post-LLM skip-push path.
|
|
66
|
+
* Failures (timeout / loop exceeded / config errors) throw — the caller's
|
|
67
|
+
* existing error handling turns them into task retry/failure.
|
|
68
|
+
*
|
|
69
|
+
* 收尾回执:onBeforeFire 一旦被调用,上面每一种结局(含抛错)都会调一次
|
|
70
|
+
* `ctx.onFireSettled`,见 notifyFireSettled。
|
|
71
|
+
*/
|
|
72
|
+
export function runAgenticFire({ task, decryptedPayload, userKey, ctx }: {
|
|
73
|
+
task: import("../adapters/interface.js").TaskRow;
|
|
74
|
+
decryptedPayload: any;
|
|
75
|
+
userKey: string;
|
|
76
|
+
ctx: any;
|
|
77
|
+
}): Promise<{
|
|
78
|
+
handled: false;
|
|
79
|
+
} | {
|
|
80
|
+
handled: true;
|
|
81
|
+
result: {
|
|
82
|
+
success: true;
|
|
83
|
+
messagesSent: number;
|
|
84
|
+
status: "finished" | "skipped";
|
|
85
|
+
iterations: number;
|
|
86
|
+
};
|
|
87
|
+
}>;
|
|
88
|
+
export const DEFAULT_MAX_TOOL_ITERATIONS: 5;
|
|
89
|
+
export const DEFAULT_TOTAL_TIMEOUT_MS: 240000;
|
|
90
|
+
export const DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE: 2;
|
|
91
|
+
export const MIN_SCHEDULE_LEAD_MS: 60000;
|
|
92
|
+
/**
|
|
93
|
+
* 把任务的调度身份投影进一条 push。
|
|
94
|
+
*
|
|
95
|
+
* 客户端收到推送时要知道「这是哪条任务、哪一次触发、它还会不会再来」——不然
|
|
96
|
+
* 角色在 fire 里自排的任务客户端从没见过,只能靠猜。这三个字段由库统一补,
|
|
97
|
+
* 宿主不用再往 metadata 里抄一遍(抄漏一个就够坏事了)。
|
|
98
|
+
*
|
|
99
|
+
* 放在 push 顶层,和 messageId / sessionId / timestamp / messageIndex /
|
|
100
|
+
* totalMessages 一样,不塞进 metadata:metadata 是宿主自己的地盘,库往里写
|
|
101
|
+
* 会和宿主的键打架。
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} push - 待发送的 push(原地补字段)
|
|
104
|
+
* @param {Object} task - 数据库任务行
|
|
105
|
+
* @param {Object} decryptedPayload
|
|
106
|
+
* @param {number|null} occurrenceMs
|
|
107
|
+
*/
|
|
108
|
+
export function stampTaskIdentity(push: any, task: any, decryptedPayload: any, occurrenceMs: number | null): void;
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
*
|
|
34
|
+
* 条件写护栏:条目可带可选的 `version`(毫秒时间戳或单调递增整数)。带了它,
|
|
35
|
+
* 比较用的就是这个值而不是 `updatedAt`——同一个 key 有多个写入方时(例如
|
|
36
|
+
* fire_pack 由常规 flush 和 instant-chat 两条路径写),谁的**内容**新谁赢,
|
|
37
|
+
* 而不是谁的请求后到谁赢:慢网下晚到的旧包带着旧 `version`,盖不掉先到的新
|
|
38
|
+
* 包。护栏值落在行的 updated_at 列上(client_state 的比较列本来就是它),
|
|
39
|
+
* 没带 `version` 的写入照旧按 `updatedAt` 比。
|
|
40
|
+
*
|
|
41
|
+
* 条目本身的合法性(namespace / key 字符、value 大小)由调用方先校验好:
|
|
42
|
+
* HTTP handler 逐条拒绝并把原因回给客户端,`writeState()` 直接抛错给 hook。
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} args
|
|
45
|
+
* @param {{ upsertClientState: Function }} args.db
|
|
46
|
+
* @param {string} args.userId
|
|
47
|
+
* @param {CryptoKey|string} args.userKey - 该用户的存储密钥(value 用它加密)
|
|
48
|
+
* @param {Array<{ namespace: string, key: string, value: string|null, updatedAt: number, version?: number }>} args.entries
|
|
49
|
+
* `version`(可选):条件写护栏值,见文件头。带了它,这条的 last-write-wins
|
|
50
|
+
* 比较用它(写进行的 updated_at 列);没带照旧用 `updatedAt`。
|
|
51
|
+
* @returns {Promise<{ upserted: number, skipped: number, deleted: number, skippedEntries: Array<{ namespace: string, key: string }> }>}
|
|
52
|
+
* `upserted` / `skipped` 按逻辑条目计(切片行不计);`deleted` 是请求删除的
|
|
53
|
+
* key 数,不代表这些 key 原本一定存在。`skippedEntries` 逐条列出被
|
|
54
|
+
* last-write-wins 拦下的 key(适配器不回 outcomes 时为空数组——分不清是哪条)。
|
|
55
|
+
*/
|
|
56
|
+
export function writeClientStateEntries({ db, userId, userKey, entries }: {
|
|
57
|
+
db: {
|
|
58
|
+
upsertClientState: Function;
|
|
59
|
+
};
|
|
60
|
+
userId: string;
|
|
61
|
+
userKey: CryptoKey | string;
|
|
62
|
+
entries: Array<{
|
|
63
|
+
namespace: string;
|
|
64
|
+
key: string;
|
|
65
|
+
value: string | null;
|
|
66
|
+
updatedAt: number;
|
|
67
|
+
version?: number;
|
|
68
|
+
}>;
|
|
69
|
+
}): Promise<{
|
|
70
|
+
upserted: number;
|
|
71
|
+
skipped: number;
|
|
72
|
+
deleted: number;
|
|
73
|
+
skippedEntries: Array<{
|
|
74
|
+
namespace: string;
|
|
75
|
+
key: string;
|
|
76
|
+
}>;
|
|
77
|
+
}>;
|
|
78
|
+
export const MAX_STATE_ENTRIES_PER_BATCH: 200;
|
|
79
|
+
export const MAX_NAMESPACE_CHARS: 128;
|
|
80
|
+
export const MAX_KEY_CHARS: 256;
|
|
@@ -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>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a user-specific encryption key from the master key.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} userId - Unique user identifier.
|
|
5
|
+
* @param {string} masterKey - 64-char hex master key.
|
|
6
|
+
* @returns {Promise<string>} 64-char hex key.
|
|
7
|
+
*/
|
|
8
|
+
export function deriveUserEncryptionKey(userId: string, masterKey: string): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Decrypt a client-encrypted request body (AES-256-GCM, base64 encoded).
|
|
11
|
+
*
|
|
12
|
+
* @param {{ iv: string, authTag: string, encryptedData: string }} encryptedPayload
|
|
13
|
+
* @param {string} encryptionKey - 64-char hex key.
|
|
14
|
+
* @returns {Promise<Object>} Decrypted JSON object.
|
|
15
|
+
*/
|
|
16
|
+
export function decryptPayload(encryptedPayload: {
|
|
17
|
+
iv: string;
|
|
18
|
+
authTag: string;
|
|
19
|
+
encryptedData: string;
|
|
20
|
+
}, encryptionKey: string): Promise<any>;
|
|
21
|
+
/**
|
|
22
|
+
* Encrypt a JSON payload for API transfer (AES-256-GCM, base64 encoded).
|
|
23
|
+
*
|
|
24
|
+
* @param {string|Object} payload
|
|
25
|
+
* @param {string} encryptionKey - 64-char hex key.
|
|
26
|
+
* @returns {Promise<{ iv: string, authTag: string, encryptedData: string }>}
|
|
27
|
+
*/
|
|
28
|
+
export function encryptPayload(payload: string | any, encryptionKey: string): Promise<{
|
|
29
|
+
iv: string;
|
|
30
|
+
authTag: string;
|
|
31
|
+
encryptedData: string;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Encrypt data for database storage (hex encoded, colon-separated).
|
|
35
|
+
*
|
|
36
|
+
* @param {string} text - Plaintext string.
|
|
37
|
+
* @param {string} encryptionKey - 64-char hex key.
|
|
38
|
+
* @returns {Promise<string>} Format: iv:authTag:encryptedData
|
|
39
|
+
*/
|
|
40
|
+
export function encryptForStorage(text: string, encryptionKey: string): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Decrypt data from database storage format.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} encryptedText - Format: iv:authTag:encryptedData
|
|
45
|
+
* @param {string} encryptionKey - 64-char hex key.
|
|
46
|
+
* @returns {Promise<string>} Plaintext string.
|
|
47
|
+
*/
|
|
48
|
+
export function decryptFromStorage(encryptedText: string, encryptionKey: string): Promise<string>;
|