@rei-standard/amsg-server 2.6.0-next.1 → 2.6.0-next.3

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.
@@ -106,10 +106,10 @@ function validateScheduleMessagePayload(payload) {
106
106
  };
107
107
  }
108
108
  if (hasMessages) {
109
- const err = validateLlmMessagesArray(payload.messages);
110
- if (err) {
109
+ const err2 = validateLlmMessagesArray(payload.messages);
110
+ if (err2) {
111
111
  return {
112
- error: { code: "INVALID_PARAMETERS", message: err, details: { invalidFields: ["messages"] } },
112
+ error: { code: "INVALID_PARAMETERS", message: err2, details: { invalidFields: ["messages"] } },
113
113
  hasCompletePrompt: false,
114
114
  hasMessages: true
115
115
  };
@@ -162,53 +162,127 @@ function validateScheduleMessagePayload(payload) {
162
162
  return { valid: true };
163
163
  }
164
164
 
165
+ // src/server/lib/webcrypto-utils.js
166
+
167
+ var TEXT_ENCODER = new TextEncoder();
168
+ var TEXT_DECODER = new TextDecoder("utf-8", { fatal: false });
169
+ function utf8(str) {
170
+ return TEXT_ENCODER.encode(String(str));
171
+ }
172
+ function utf8Decode(buf) {
173
+ return TEXT_DECODER.decode(_amsgshared.toUint8.call(void 0, buf));
174
+ }
175
+ function bytesToBase64(buf) {
176
+ const bytes = _amsgshared.toUint8.call(void 0, buf);
177
+ let bin = "";
178
+ for (let i = 0; i < bytes.length; i++) {
179
+ bin += String.fromCharCode(bytes[i]);
180
+ }
181
+ return typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
182
+ }
183
+ function bytesToBase64Url(buf) {
184
+ return bytesToBase64(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
185
+ }
186
+ function bytesToHex(buf) {
187
+ const bytes = _amsgshared.toUint8.call(void 0, buf);
188
+ let out = "";
189
+ for (let i = 0; i < bytes.length; i++) {
190
+ out += bytes[i].toString(16).padStart(2, "0");
191
+ }
192
+ return out;
193
+ }
194
+ function hexToBytes(hex) {
195
+ const out = new Uint8Array(hex.length / 2);
196
+ for (let i = 0; i < out.length; i++) {
197
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
198
+ }
199
+ return out;
200
+ }
201
+ function jsonToBase64Url(value) {
202
+ return bytesToBase64Url(utf8(JSON.stringify(value)));
203
+ }
204
+ function randomUUID() {
205
+ return globalThis.crypto.randomUUID();
206
+ }
207
+ function randomBytes(n) {
208
+ const out = new Uint8Array(n);
209
+ globalThis.crypto.getRandomValues(out);
210
+ return out;
211
+ }
212
+
165
213
  // src/server/lib/encryption.js
166
- var _crypto = require('crypto');
167
- function deriveUserEncryptionKey(userId, masterKey) {
168
- return _crypto.createHash.call(void 0, "sha256").update(masterKey + userId).digest("hex").slice(0, 64);
214
+ var base64ToBytes = _amsgshared.base64UrlToBytes;
215
+ var TAG_LENGTH_BYTES = 16;
216
+ function importAesKey(hexKey, usage) {
217
+ return globalThis.crypto.subtle.importKey(
218
+ "raw",
219
+ hexToBytes(hexKey),
220
+ { name: "AES-GCM" },
221
+ false,
222
+ [usage]
223
+ );
169
224
  }
170
- function decryptPayload(encryptedPayload, encryptionKey) {
225
+ async function aesGcmSeal(hexKey, iv, plaintextBytes) {
226
+ const key = await importAesKey(hexKey, "encrypt");
227
+ const sealed = new Uint8Array(await globalThis.crypto.subtle.encrypt(
228
+ { name: "AES-GCM", iv, tagLength: TAG_LENGTH_BYTES * 8 },
229
+ key,
230
+ plaintextBytes
231
+ ));
232
+ return {
233
+ ciphertext: sealed.slice(0, sealed.length - TAG_LENGTH_BYTES),
234
+ authTag: sealed.slice(sealed.length - TAG_LENGTH_BYTES)
235
+ };
236
+ }
237
+ async function aesGcmOpen(hexKey, iv, ciphertext, authTag) {
238
+ const key = await importAesKey(hexKey, "decrypt");
239
+ const tagBits = authTag.length * 8;
240
+ const tagLength = tagBits >= 96 && tagBits <= 128 ? tagBits : 128;
241
+ const plain = await globalThis.crypto.subtle.decrypt(
242
+ { name: "AES-GCM", iv, tagLength },
243
+ key,
244
+ _amsgshared.concatBytes.call(void 0, ciphertext, authTag)
245
+ );
246
+ return new Uint8Array(plain);
247
+ }
248
+ async function deriveUserEncryptionKey(userId, masterKey) {
249
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8(masterKey + userId));
250
+ return bytesToHex(new Uint8Array(digest)).slice(0, 64);
251
+ }
252
+ async function decryptPayload(encryptedPayload, encryptionKey) {
171
253
  const { iv, authTag, encryptedData } = encryptedPayload;
172
- const decipher = _crypto.createDecipheriv.call(void 0,
173
- "aes-256-gcm",
174
- Buffer.from(encryptionKey, "hex"),
175
- Buffer.from(iv, "base64")
254
+ const plain = await aesGcmOpen(
255
+ encryptionKey,
256
+ base64ToBytes(iv),
257
+ base64ToBytes(encryptedData),
258
+ base64ToBytes(authTag)
176
259
  );
177
- decipher.setAuthTag(Buffer.from(authTag, "base64"));
178
- const decrypted = Buffer.concat([
179
- decipher.update(Buffer.from(encryptedData, "base64")),
180
- decipher.final()
181
- ]);
182
- return JSON.parse(decrypted.toString("utf8"));
183
- }
184
- function encryptPayload(payload, encryptionKey) {
260
+ return JSON.parse(utf8Decode(plain));
261
+ }
262
+ async function encryptPayload(payload, encryptionKey) {
185
263
  const plaintext = typeof payload === "string" ? payload : JSON.stringify(payload);
186
- const iv = _crypto.randomBytes.call(void 0, 12);
187
- const cipher = _crypto.createCipheriv.call(void 0, "aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
188
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
189
- const authTag = cipher.getAuthTag();
264
+ const iv = randomBytes(12);
265
+ const { ciphertext, authTag } = await aesGcmSeal(encryptionKey, iv, utf8(plaintext));
190
266
  return {
191
- iv: iv.toString("base64"),
192
- authTag: authTag.toString("base64"),
193
- encryptedData: encrypted.toString("base64")
267
+ iv: bytesToBase64(iv),
268
+ authTag: bytesToBase64(authTag),
269
+ encryptedData: bytesToBase64(ciphertext)
194
270
  };
195
271
  }
196
- function encryptForStorage(text, encryptionKey) {
197
- const iv = _crypto.randomBytes.call(void 0, 16);
198
- const cipher = _crypto.createCipheriv.call(void 0, "aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
199
- const encrypted = cipher.update(text, "utf8", "hex") + cipher.final("hex");
200
- const authTag = cipher.getAuthTag();
201
- return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
272
+ async function encryptForStorage(text, encryptionKey) {
273
+ const iv = randomBytes(16);
274
+ const { ciphertext, authTag } = await aesGcmSeal(encryptionKey, iv, utf8(text));
275
+ return `${bytesToHex(iv)}:${bytesToHex(authTag)}:${bytesToHex(ciphertext)}`;
202
276
  }
203
- function decryptFromStorage(encryptedText, encryptionKey) {
277
+ async function decryptFromStorage(encryptedText, encryptionKey) {
204
278
  const [ivHex, authTagHex, encryptedDataHex] = encryptedText.split(":");
205
- const decipher = _crypto.createDecipheriv.call(void 0,
206
- "aes-256-gcm",
207
- Buffer.from(encryptionKey, "hex"),
208
- Buffer.from(ivHex, "hex")
279
+ const plain = await aesGcmOpen(
280
+ encryptionKey,
281
+ hexToBytes(ivHex),
282
+ hexToBytes(encryptedDataHex),
283
+ hexToBytes(authTagHex)
209
284
  );
210
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
211
- return decipher.update(encryptedDataHex, "hex", "utf8") + decipher.final("utf8");
285
+ return utf8Decode(plain);
212
286
  }
213
287
 
214
288
  // src/server/lib/request.js
@@ -300,7 +374,7 @@ function createGetUserKeyHandler(ctx) {
300
374
  body: {
301
375
  success: true,
302
376
  data: {
303
- userKey: deriveUserEncryptionKey(userId, masterKey),
377
+ userKey: await deriveUserEncryptionKey(userId, masterKey),
304
378
  version: 1
305
379
  }
306
380
  }
@@ -309,9 +383,6 @@ function createGetUserKeyHandler(ctx) {
309
383
  return { GET };
310
384
  }
311
385
 
312
- // src/server/handlers/schedule-message.js
313
-
314
-
315
386
  // src/server/lib/db-errors.js
316
387
  function isUniqueViolation(error) {
317
388
  if (!error || typeof error !== "object") return false;
@@ -329,8 +400,269 @@ function isUniqueViolation(error) {
329
400
 
330
401
 
331
402
 
332
- var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
403
+ // src/server/lib/llm.js
404
+ async function callLlm(payload, options = {}) {
405
+ const requireContent = options.requireContent !== false;
406
+ const timeoutMs = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 3e5;
407
+ const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
408
+ const requestBody = buildAiRequestBody(payload);
409
+ const aiResponse = await fetch(normalizedApiUrl, {
410
+ method: "POST",
411
+ headers: {
412
+ "Content-Type": "application/json",
413
+ "Authorization": `Bearer ${payload.apiKey}`
414
+ },
415
+ body: JSON.stringify(requestBody),
416
+ signal: AbortSignal.timeout(timeoutMs)
417
+ });
418
+ if (!aiResponse.ok) {
419
+ if (aiResponse.status === 405) {
420
+ throw new Error(
421
+ `AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`
422
+ );
423
+ }
424
+ throw new Error(
425
+ `AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`
426
+ );
427
+ }
428
+ const aiData = await aiResponse.json();
429
+ const rawContent = _optionalChain([aiData, 'optionalAccess', _2 => _2.choices, 'optionalAccess', _3 => _3[0], 'optionalAccess', _4 => _4.message, 'optionalAccess', _5 => _5.content]);
430
+ if (requireContent && (typeof rawContent !== "string" || !rawContent.trim())) {
431
+ throw new Error("AI API error: response missing choices[0].message.content");
432
+ }
433
+ return { response: aiData, content: typeof rawContent === "string" ? rawContent.trim() : "" };
434
+ }
435
+ function buildAiRequestBody(payload) {
436
+ const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
437
+ const requestBody = {
438
+ model: payload.primaryModel,
439
+ messages: llmMessages
440
+ };
441
+ if (payload.temperature !== void 0 && payload.temperature !== null) {
442
+ requestBody.temperature = payload.temperature;
443
+ } else if (!Array.isArray(payload.messages)) {
444
+ requestBody.temperature = 0.8;
445
+ }
446
+ if (payload.maxTokens === void 0 || payload.maxTokens === null) {
447
+ return requestBody;
448
+ }
449
+ if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
450
+ throw new Error("Invalid maxTokens: maxTokens must be a positive integer when provided.");
451
+ }
452
+ requestBody.max_tokens = payload.maxTokens;
453
+ return requestBody;
454
+ }
455
+ function normalizeAiApiUrl(apiUrl) {
456
+ if (typeof apiUrl !== "string" || !apiUrl.trim()) {
457
+ throw new Error(
458
+ "Invalid apiUrl: apiUrl is required. Please provide a chat endpoint URL (for example: https://api.openai.com or https://api.openai.com/v1/chat/completions)."
459
+ );
460
+ }
461
+ const trimmedApiUrl = apiUrl.trim();
462
+ let parsedUrl;
463
+ try {
464
+ parsedUrl = new URL(trimmedApiUrl);
465
+ } catch (e3) {
466
+ throw new Error(
467
+ `Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL.`
468
+ );
469
+ }
470
+ let path = parsedUrl.pathname.replace(/\/+$/, "") || "/";
471
+ if (/\/chat\/completions$/.test(path)) {
472
+ } else if (path === "/") {
473
+ path = "/v1/chat/completions";
474
+ } else if (/\/v\d+$/.test(path)) {
475
+ path = `${path}/chat/completions`;
476
+ }
477
+ parsedUrl.pathname = path;
478
+ return parsedUrl.toString();
479
+ }
480
+
481
+ // src/server/lib/agentic-fire.js
482
+
483
+
484
+
485
+
486
+
487
+
488
+ var DEFAULT_MAX_TOOL_ITERATIONS = 5;
489
+ var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
333
490
  var SLEEP_BETWEEN_MESSAGES_MS = 1500;
491
+ var CREDENTIAL_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["apiKey", "pushSubscription"]);
492
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
493
+ function taskNeedsLlm(decryptedPayload) {
494
+ const type = decryptedPayload.messageType;
495
+ if (type === "prompted" || type === "auto") return true;
496
+ if (type === "instant") {
497
+ return !!(decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel);
498
+ }
499
+ return false;
500
+ }
501
+ function buildHookTask(task, decryptedPayload) {
502
+ const safe = {};
503
+ for (const [key, value] of Object.entries(decryptedPayload)) {
504
+ if (!CREDENTIAL_PAYLOAD_KEYS.has(key)) safe[key] = value;
505
+ }
506
+ return Object.freeze({
507
+ ...safe,
508
+ id: _nullishCoalesce(task.id, () => ( null)),
509
+ uuid: _nullishCoalesce(task.uuid, () => ( null)),
510
+ nextSendAt: _nullishCoalesce(task.next_send_at, () => ( null)),
511
+ retryCount: _nullishCoalesce(task.retry_count, () => ( 0))
512
+ });
513
+ }
514
+ function normalizeBeforeFireResult(result) {
515
+ if (Array.isArray(result)) {
516
+ return { messages: result };
517
+ }
518
+ if (result && typeof result === "object" && Array.isArray(result.messages)) {
519
+ return {
520
+ messages: result.messages,
521
+ maxToolIterations: result.maxToolIterations,
522
+ totalTimeoutMs: result.totalTimeoutMs
523
+ };
524
+ }
525
+ throw new TypeError(
526
+ "AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs? } | null"
527
+ );
528
+ }
529
+ function firstPositiveInt(values, fallback) {
530
+ for (const v of values) {
531
+ if (Number.isInteger(v) && v > 0) return v;
532
+ }
533
+ return fallback;
534
+ }
535
+ function firstPositiveNumber(values, fallback) {
536
+ for (const v of values) {
537
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
538
+ }
539
+ return fallback;
540
+ }
541
+ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
542
+ const hooks = ctx.hooks;
543
+ if (typeof hooks.onLLMOutput !== "function") {
544
+ throw new Error("AGENTIC_CONFIG_ERROR: hooks.onBeforeFire requires hooks.onLLMOutput to classify LLM rounds");
545
+ }
546
+ const nowFn = typeof ctx._agenticNow === "function" ? ctx._agenticNow : Date.now;
547
+ const sleep = typeof ctx._agenticSleep === "function" ? ctx._agenticSleep : defaultSleep;
548
+ const readState = async (namespace) => {
549
+ if (typeof namespace !== "string" || !namespace.trim()) {
550
+ throw new TypeError("readState(namespace) requires a non-empty string");
551
+ }
552
+ if (!ctx.db || typeof ctx.db.getClientState !== "function") return [];
553
+ const rows = await ctx.db.getClientState(task.user_id, namespace);
554
+ return Promise.all(rows.map(async (row) => ({
555
+ namespace: row.namespace,
556
+ key: row.key,
557
+ value: await decryptFromStorage(row.value, userKey),
558
+ updatedAt: row.updated_at
559
+ })));
560
+ };
561
+ const fireCtx = Object.freeze({
562
+ task: buildHookTask(task, decryptedPayload),
563
+ userId: task.user_id,
564
+ readState,
565
+ now: new Date(nowFn())
566
+ });
567
+ const before = await hooks.onBeforeFire(fireCtx);
568
+ if (before == null) return { handled: false };
569
+ const normalized = normalizeBeforeFireResult(before);
570
+ const maxToolIterations = firstPositiveInt(
571
+ [normalized.maxToolIterations, ctx.maxToolIterations],
572
+ DEFAULT_MAX_TOOL_ITERATIONS
573
+ );
574
+ const totalTimeoutMs = firstPositiveNumber(
575
+ [normalized.totalTimeoutMs, ctx.totalTimeoutMs],
576
+ DEFAULT_TOTAL_TIMEOUT_MS
577
+ );
578
+ const deadline = nowFn() + totalTimeoutMs;
579
+ const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${randomUUID()}`;
580
+ let messages = normalized.messages.slice();
581
+ for (let iteration = 0; iteration < maxToolIterations; iteration++) {
582
+ if (nowFn() >= deadline) {
583
+ throw new Error(`AGENTIC_TOTAL_TIMEOUT: fire chain exceeded ${totalTimeoutMs}ms after ${iteration} LLM round(s)`);
584
+ }
585
+ const roundTimeoutMs = Math.max(1, Math.min(3e5, deadline - nowFn()));
586
+ const { response: llmResponse } = await callLlm(
587
+ { ...decryptedPayload, messages },
588
+ { requireContent: false, timeoutMs: roundTimeoutMs }
589
+ );
590
+ const assistantMessage = _amsgshared.extractAssistantMessage.call(void 0, llmResponse);
591
+ messages = [...messages, assistantMessage];
592
+ const sessionCtx = _amsgshared.buildSessionContext.call(void 0, {
593
+ sessionId,
594
+ messages,
595
+ llmResponse,
596
+ iteration,
597
+ contactName: decryptedPayload.contactName,
598
+ avatarUrl: decryptedPayload.avatarUrl || void 0,
599
+ charId: decryptedPayload.charId,
600
+ metadata: decryptedPayload.metadata
601
+ });
602
+ const decision = await hooks.onLLMOutput(sessionCtx);
603
+ _amsgshared.assertValidDecision.call(void 0, decision, { inlineToolCalls: true });
604
+ if (decision.decision === "continue") {
605
+ messages = decision.nextHistory.slice();
606
+ continue;
607
+ }
608
+ if (decision.decision === "skip-push") {
609
+ return { handled: true, result: { success: true, messagesSent: 0, status: "skipped", iterations: iteration + 1 } };
610
+ }
611
+ if (decision.decision === "finish") {
612
+ const messagesSent = await sendHookPushPayloads(decision.pushPayloads, decryptedPayload, ctx, sessionId, task, sleep);
613
+ return { handled: true, result: { success: true, messagesSent, status: "finished", iterations: iteration + 1 } };
614
+ }
615
+ const toolCalls = _amsgshared.extractToolCallsFromDecision.call(void 0, decision);
616
+ if (toolCalls.length === 0) {
617
+ throw new Error("AGENTIC_EMPTY_TOOL_REQUEST: tool-request decision carried no toolCalls (neither decision.toolCalls nor pushPayloads[].toolCalls)");
618
+ }
619
+ if (typeof hooks.executeToolCalls !== "function") {
620
+ throw new Error("AGENTIC_CONFIG_ERROR: onLLMOutput returned tool-request but hooks.executeToolCalls is not configured");
621
+ }
622
+ if (iteration === maxToolIterations - 1) {
623
+ break;
624
+ }
625
+ let toolResults;
626
+ try {
627
+ toolResults = await hooks.executeToolCalls(toolCalls, sessionCtx);
628
+ if (!Array.isArray(toolResults)) {
629
+ throw new TypeError('executeToolCalls must resolve to an array of { tool_call_id, role: "tool", content }');
630
+ }
631
+ } catch (error) {
632
+ toolResults = toolCalls.map((toolCall) => ({
633
+ tool_call_id: toolCall && typeof toolCall === "object" && typeof toolCall.id === "string" ? toolCall.id : "",
634
+ role: "tool",
635
+ content: `Tool execution failed: ${_nullishCoalesce(_optionalChain([error, 'optionalAccess', _6 => _6.message]), () => ( String(error)))}`
636
+ }));
637
+ }
638
+ const assistantWithTools = Array.isArray(assistantMessage.tool_calls) && assistantMessage.tool_calls.length > 0 ? assistantMessage : { ...assistantMessage, tool_calls: toolCalls };
639
+ messages = [...messages.slice(0, -1), assistantWithTools, ...toolResults];
640
+ }
641
+ throw new Error(`AGENTIC_LOOP_EXCEEDED: no finish/skip-push decision within ${maxToolIterations} LLM round(s)`);
642
+ }
643
+ async function sendHookPushPayloads(pushPayloads, decryptedPayload, ctx, sessionId, task, sleep) {
644
+ if (!ctx.vapid || !ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
645
+ throw new Error("VAPID configuration missing - push notifications cannot be sent");
646
+ }
647
+ const pushSubscription = decryptedPayload.pushSubscription;
648
+ const total = pushPayloads.length;
649
+ const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${randomUUID()}`;
650
+ for (let i = 0; i < total; i++) {
651
+ const push = { ...pushPayloads[i] };
652
+ if (typeof push.messageId !== "string" || !push.messageId) push.messageId = `${messageIdBase}_hook_${i}`;
653
+ if (typeof push.sessionId !== "string" || !push.sessionId) push.sessionId = sessionId;
654
+ if (typeof push.timestamp !== "string" || !push.timestamp) push.timestamp = (/* @__PURE__ */ new Date()).toISOString();
655
+ push.messageIndex = i + 1;
656
+ push.totalMessages = total;
657
+ await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(push));
658
+ if (i < total - 1) await sleep(SLEEP_BETWEEN_MESSAGES_MS);
659
+ }
660
+ return total;
661
+ }
662
+
663
+ // src/server/lib/message-processor.js
664
+ var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
665
+ var SLEEP_BETWEEN_MESSAGES_MS2 = 1500;
334
666
  function splitOnceByRegex(chunk, regex) {
335
667
  const out = chunk.split(regex).reduce((acc, part, i, arr) => {
336
668
  if (i % 2 === 0 && part.trim()) {
@@ -356,8 +688,12 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
356
688
  if (!masterKey) {
357
689
  return { success: false, messagesSent: 0, error: "TENANT_MASTER_KEY_MISSING" };
358
690
  }
359
- const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
360
- const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
691
+ const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
692
+ const decryptedPayload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
693
+ if (ctx.hooks && typeof ctx.hooks.onBeforeFire === "function" && taskNeedsLlm(decryptedPayload)) {
694
+ const agentic = await runAgenticFire({ task, decryptedPayload, userKey, ctx });
695
+ if (agentic.handled) return agentic.result;
696
+ }
361
697
  let messageContent;
362
698
  let llmResponse = null;
363
699
  if (decryptedPayload.messageType === "fixed") {
@@ -365,7 +701,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
365
701
  } else if (decryptedPayload.messageType === "instant") {
366
702
  const hasPrompt = !!decryptedPayload.completePrompt || Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0;
367
703
  if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
368
- const aiResult = await _callAI(decryptedPayload);
704
+ const aiResult = await callLlm(decryptedPayload);
369
705
  messageContent = aiResult.content;
370
706
  llmResponse = aiResult.response;
371
707
  } else if (decryptedPayload.userMessage) {
@@ -374,7 +710,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
374
710
  throw new Error("Invalid instant message: no content source available");
375
711
  }
376
712
  } else if (decryptedPayload.messageType === "prompted" || decryptedPayload.messageType === "auto") {
377
- const aiResult = await _callAI(decryptedPayload);
713
+ const aiResult = await callLlm(decryptedPayload);
378
714
  messageContent = aiResult.content;
379
715
  llmResponse = aiResult.response;
380
716
  } else {
@@ -389,12 +725,12 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
389
725
  throw new Error("VAPID configuration missing - push notifications cannot be sent");
390
726
  }
391
727
  const pushSubscription = decryptedPayload.pushSubscription;
392
- const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${_crypto.randomUUID.call(void 0, )}`;
728
+ const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${randomUUID()}`;
393
729
  const source = decryptedPayload.messageType === "instant" ? "instant" : "scheduled";
394
730
  const messageSubtype = decryptedPayload.messageSubtype || "chat";
395
731
  const avatarUrl = decryptedPayload.avatarUrl || null;
396
732
  const metadata = decryptedPayload.metadata || {};
397
- const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${_crypto.randomUUID.call(void 0, )}_instant`;
733
+ const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${randomUUID()}_instant`;
398
734
  if (reasoning) {
399
735
  const reasoningPush = _amsgshared.buildReasoningPush.call(void 0, {
400
736
  messageType: decryptedPayload.messageType,
@@ -410,7 +746,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
410
746
  metadata
411
747
  });
412
748
  await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
413
- await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
749
+ await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS2));
414
750
  }
415
751
  for (let i = 0; i < messages.length; i++) {
416
752
  const contentPush = _amsgshared.buildContentPush.call(void 0, {
@@ -431,7 +767,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
431
767
  });
432
768
  await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
433
769
  if (i < messages.length - 1) {
434
- await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
770
+ await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS2));
435
771
  }
436
772
  }
437
773
  return { success: true, messagesSent: messages.length };
@@ -501,80 +837,6 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
501
837
  return { success: true, messagesSent: result.messagesSent, retriesUsed: retryCount };
502
838
  }
503
839
  }
504
- async function _callAI(payload) {
505
- const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
506
- const requestBody = buildAiRequestBody(payload);
507
- const aiResponse = await fetch(normalizedApiUrl, {
508
- method: "POST",
509
- headers: {
510
- "Content-Type": "application/json",
511
- "Authorization": `Bearer ${payload.apiKey}`
512
- },
513
- body: JSON.stringify(requestBody),
514
- signal: AbortSignal.timeout(3e5)
515
- });
516
- if (!aiResponse.ok) {
517
- if (aiResponse.status === 405) {
518
- throw new Error(
519
- `AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`
520
- );
521
- }
522
- throw new Error(
523
- `AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`
524
- );
525
- }
526
- const aiData = await aiResponse.json();
527
- const content = _optionalChain([aiData, 'optionalAccess', _2 => _2.choices, 'optionalAccess', _3 => _3[0], 'optionalAccess', _4 => _4.message, 'optionalAccess', _5 => _5.content]);
528
- if (typeof content !== "string" || !content.trim()) {
529
- throw new Error("AI API error: response missing choices[0].message.content");
530
- }
531
- return { response: aiData, content: content.trim() };
532
- }
533
- function buildAiRequestBody(payload) {
534
- const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
535
- const requestBody = {
536
- model: payload.primaryModel,
537
- messages: llmMessages
538
- };
539
- if (payload.temperature !== void 0 && payload.temperature !== null) {
540
- requestBody.temperature = payload.temperature;
541
- } else if (!Array.isArray(payload.messages)) {
542
- requestBody.temperature = 0.8;
543
- }
544
- if (payload.maxTokens === void 0 || payload.maxTokens === null) {
545
- return requestBody;
546
- }
547
- if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
548
- throw new Error("Invalid maxTokens: maxTokens must be a positive integer when provided.");
549
- }
550
- requestBody.max_tokens = payload.maxTokens;
551
- return requestBody;
552
- }
553
- function normalizeAiApiUrl(apiUrl) {
554
- if (typeof apiUrl !== "string" || !apiUrl.trim()) {
555
- throw new Error(
556
- "Invalid apiUrl: apiUrl is required. Please provide a chat endpoint URL (for example: https://api.openai.com or https://api.openai.com/v1/chat/completions)."
557
- );
558
- }
559
- const trimmedApiUrl = apiUrl.trim();
560
- let parsedUrl;
561
- try {
562
- parsedUrl = new URL(trimmedApiUrl);
563
- } catch (e3) {
564
- throw new Error(
565
- `Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL.`
566
- );
567
- }
568
- let path = parsedUrl.pathname.replace(/\/+$/, "") || "/";
569
- if (/\/chat\/completions$/.test(path)) {
570
- } else if (path === "/") {
571
- path = "/v1/chat/completions";
572
- } else if (/\/v\d+$/.test(path)) {
573
- path = `${path}/chat/completions`;
574
- }
575
- parsedUrl.pathname = path;
576
- return parsedUrl.toString();
577
- }
578
840
 
579
841
  // src/server/handlers/schedule-message.js
580
842
  function createScheduleMessageHandler(ctx) {
@@ -606,10 +868,10 @@ function createScheduleMessageHandler(ctx) {
606
868
  return { status: 400, body: { success: false, error: parsedBody.error } };
607
869
  }
608
870
  const encryptedBody = parsedBody.data;
871
+ const userKey = await deriveUserEncryptionKey(userId, masterKey);
609
872
  let payload;
610
873
  try {
611
- const userKey2 = deriveUserEncryptionKey(userId, masterKey);
612
- payload = decryptPayload(encryptedBody, userKey2);
874
+ payload = await decryptPayload(encryptedBody, userKey);
613
875
  } catch (error) {
614
876
  if (error instanceof SyntaxError) {
615
877
  return { status: 400, body: { success: false, error: { code: "INVALID_PAYLOAD_FORMAT", message: "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u4E0D\u662F\u6709\u6548 JSON" } } };
@@ -627,8 +889,7 @@ function createScheduleMessageHandler(ctx) {
627
889
  if (!validationResult.valid) {
628
890
  return { status: 400, body: { success: false, error: { code: validationResult.errorCode, message: validationResult.errorMessage, details: validationResult.details } } };
629
891
  }
630
- const taskUuid = payload.uuid || _crypto.randomUUID.call(void 0, );
631
- const userKey = deriveUserEncryptionKey(userId, masterKey);
892
+ const taskUuid = payload.uuid || randomUUID();
632
893
  const fullTaskData = {
633
894
  contactName: payload.contactName,
634
895
  avatarUrl: payload.avatarUrl || null,
@@ -654,7 +915,7 @@ function createScheduleMessageHandler(ctx) {
654
915
  pushSubscription: payload.pushSubscription,
655
916
  metadata: payload.metadata || {}
656
917
  };
657
- const encryptedPayload = encryptForStorage(JSON.stringify(fullTaskData), userKey);
918
+ const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
658
919
  if (payload.messageType === "instant") {
659
920
  if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
660
921
  return {
@@ -808,8 +1069,8 @@ async function runScheduledTick(ctx) {
808
1069
  return;
809
1070
  }
810
1071
  try {
811
- const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
812
- const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
1072
+ const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
1073
+ const decryptedPayload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
813
1074
  if (decryptedPayload.recurrenceType === "none") {
814
1075
  await db.deleteTaskById(task.id);
815
1076
  results.deletedOnceOffTasks++;
@@ -896,10 +1157,10 @@ function createUpdateMessageHandler(ctx) {
896
1157
  return { status: 400, body: { success: false, error: parsedBody.error } };
897
1158
  }
898
1159
  const encryptedBody = parsedBody.data;
899
- const userKey = deriveUserEncryptionKey(userId, masterKey);
1160
+ const userKey = await deriveUserEncryptionKey(userId, masterKey);
900
1161
  let updates;
901
1162
  try {
902
- updates = decryptPayload(encryptedBody, userKey);
1163
+ updates = await decryptPayload(encryptedBody, userKey);
903
1164
  } catch (_error) {
904
1165
  return { status: 400, body: { success: false, error: { code: "DECRYPTION_FAILED", message: "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25" } } };
905
1166
  }
@@ -948,7 +1209,7 @@ function createUpdateMessageHandler(ctx) {
948
1209
  }
949
1210
  return { status: 409, body: { success: false, error: { code: "TASK_ALREADY_COMPLETED", message: "\u4EFB\u52A1\u5DF2\u5B8C\u6210\u6216\u5DF2\u5931\u8D25\uFF0C\u65E0\u6CD5\u66F4\u65B0" } } };
950
1211
  }
951
- const existingData = JSON.parse(decryptFromStorage(existingTask.encrypted_payload, userKey));
1212
+ const existingData = JSON.parse(await decryptFromStorage(existingTask.encrypted_payload, userKey));
952
1213
  const promptUpdates = {};
953
1214
  if (updates.completePrompt) {
954
1215
  promptUpdates.completePrompt = updates.completePrompt;
@@ -971,7 +1232,7 @@ function createUpdateMessageHandler(ctx) {
971
1232
  // string fields above are.
972
1233
  ...Object.prototype.hasOwnProperty.call(updates, "splitPattern") && { splitPattern: _nullishCoalesce(updates.splitPattern, () => ( null)) }
973
1234
  };
974
- const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
1235
+ const encryptedPayload = await encryptForStorage(JSON.stringify(updatedData), userKey);
975
1236
  const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
976
1237
  const result = await db.updateTaskByUuid(taskUuid, userId, encryptedPayload, extraFields);
977
1238
  if (!result) {
@@ -1064,9 +1325,9 @@ function createMessagesHandler(ctx) {
1064
1325
  return { status: 400, body: { success: false, error: { code: "INVALID_PARAMETERS", message: "offset \u53C2\u6570\u65E0\u6548\uFF0C\u5FC5\u987B\u4E3A\u975E\u8D1F\u6574\u6570" } } };
1065
1326
  }
1066
1327
  const { tasks, total } = await db.listTasks(userId, { status, limit, offset });
1067
- const userKey = deriveUserEncryptionKey(userId, masterKey);
1068
- const decryptedTasks = tasks.map((task) => {
1069
- const decrypted = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
1328
+ const userKey = await deriveUserEncryptionKey(userId, masterKey);
1329
+ const decryptedTasks = await Promise.all(tasks.map(async (task) => {
1330
+ const decrypted = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
1070
1331
  return {
1071
1332
  id: task.id,
1072
1333
  uuid: task.uuid,
@@ -1080,12 +1341,12 @@ function createMessagesHandler(ctx) {
1080
1341
  createdAt: task.created_at,
1081
1342
  updatedAt: task.updated_at
1082
1343
  };
1083
- });
1344
+ }));
1084
1345
  const responsePayload = {
1085
1346
  tasks: decryptedTasks,
1086
1347
  pagination: { total, limit, offset, hasMore: offset + limit < total }
1087
1348
  };
1088
- const encryptedResponse = encryptPayload(responsePayload, userKey);
1349
+ const encryptedResponse = await encryptPayload(responsePayload, userKey);
1089
1350
  return {
1090
1351
  status: 200,
1091
1352
  body: {
@@ -1155,6 +1416,16 @@ var SQLITE_INDEXES = [
1155
1416
  critical: true
1156
1417
  }
1157
1418
  ];
1419
+ var CLIENT_STATE_TABLE_SQL = `
1420
+ CREATE TABLE IF NOT EXISTS client_state (
1421
+ user_id TEXT NOT NULL,
1422
+ namespace TEXT NOT NULL,
1423
+ key TEXT NOT NULL,
1424
+ value TEXT NOT NULL,
1425
+ updated_at INTEGER NOT NULL,
1426
+ PRIMARY KEY (user_id, namespace, key)
1427
+ )
1428
+ `;
1158
1429
 
1159
1430
  // src/server/adapters/d1.js
1160
1431
  var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
@@ -1187,6 +1458,7 @@ var D1Adapter = class {
1187
1458
  }
1188
1459
  async initSchema() {
1189
1460
  await this._db.prepare(SQLITE_TABLE_SQL).run();
1461
+ await this._db.prepare(CLIENT_STATE_TABLE_SQL).run();
1190
1462
  const indexResults = [];
1191
1463
  for (const index of SQLITE_INDEXES) {
1192
1464
  try {
@@ -1213,6 +1485,7 @@ var D1Adapter = class {
1213
1485
  }
1214
1486
  async dropSchema() {
1215
1487
  await this._db.prepare("DROP TABLE IF EXISTS scheduled_messages").run();
1488
+ await this._db.prepare("DROP TABLE IF EXISTS client_state").run();
1216
1489
  }
1217
1490
  async createTask(params) {
1218
1491
  const now = this._now();
@@ -1341,6 +1614,77 @@ var D1Adapter = class {
1341
1614
  ).bind(uuid, userId).first();
1342
1615
  return row ? row.status : null;
1343
1616
  }
1617
+ // ── client_state (single-user cloud state mirror) ──────────────────────
1618
+ /**
1619
+ * Batch upsert. Last-write-wins per (namespace, key): an entry older
1620
+ * than the stored row (updatedAt strictly lower) is skipped; equal or
1621
+ * newer overwrites. Values arrive pre-encrypted (the handler encrypts).
1622
+ *
1623
+ * Uses D1's batch() — one network round trip for the whole set (implicit
1624
+ * transaction). The client calls this endpoint inside its few-seconds
1625
+ * background window, so N sequential round trips could eat the whole
1626
+ * window. Bindings without batch() (e.g. the sqlite test shim, custom
1627
+ * adapters) fall back to a sequential loop.
1628
+ *
1629
+ * @param {string} userId
1630
+ * @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
1631
+ * @returns {Promise<{ upserted: number, skipped: number }>}
1632
+ */
1633
+ async upsertClientState(userId, entries) {
1634
+ const UPSERT_SQL = `INSERT INTO client_state (user_id, namespace, key, value, updated_at)
1635
+ VALUES (?, ?, ?, ?, ?)
1636
+ ON CONFLICT (user_id, namespace, key) DO UPDATE SET
1637
+ value = excluded.value,
1638
+ updated_at = excluded.updated_at
1639
+ WHERE excluded.updated_at >= client_state.updated_at`;
1640
+ let results;
1641
+ if (typeof this._db.batch === "function") {
1642
+ const statements = entries.map(
1643
+ (entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt)
1644
+ );
1645
+ results = await this._db.batch(statements);
1646
+ } else {
1647
+ results = [];
1648
+ for (const entry of entries) {
1649
+ results.push(
1650
+ await this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt).run()
1651
+ );
1652
+ }
1653
+ }
1654
+ let upserted = 0;
1655
+ let skipped = 0;
1656
+ for (const res of results) {
1657
+ if (res.meta.changes > 0) upserted++;
1658
+ else skipped++;
1659
+ }
1660
+ return { upserted, skipped };
1661
+ }
1662
+ /**
1663
+ * All entries of one namespace (values still encrypted).
1664
+ *
1665
+ * @param {string} userId
1666
+ * @param {string} namespace
1667
+ * @returns {Promise<Array<{ namespace: string, key: string, value: string, updated_at: number }>>}
1668
+ */
1669
+ async getClientState(userId, namespace) {
1670
+ const res = await this._db.prepare(
1671
+ `SELECT namespace, key, value, updated_at
1672
+ FROM client_state
1673
+ WHERE user_id = ? AND namespace = ?
1674
+ ORDER BY key ASC`
1675
+ ).bind(userId, namespace).all();
1676
+ return res.results || [];
1677
+ }
1678
+ /**
1679
+ * Wipe every entry of this user.
1680
+ *
1681
+ * @param {string} userId
1682
+ * @returns {Promise<number>} rows deleted
1683
+ */
1684
+ async clearClientState(userId) {
1685
+ const res = await this._db.prepare("DELETE FROM client_state WHERE user_id = ?").bind(userId).run();
1686
+ return res.meta.changes || 0;
1687
+ }
1344
1688
  };
1345
1689
  function createD1Adapter(db) {
1346
1690
  if (!db || typeof db.prepare !== "function") {
@@ -1454,6 +1798,131 @@ function createVapidPublicKeyHandler(ctx) {
1454
1798
  return { GET };
1455
1799
  }
1456
1800
 
1801
+ // src/server/handlers/client-state.js
1802
+ var MAX_STATE_VALUE_BYTES = 200 * 1024;
1803
+ var MAX_STATE_ENTRIES_PER_REQUEST = 200;
1804
+ var MAX_NAMESPACE_CHARS = 128;
1805
+ var MAX_KEY_CHARS = 256;
1806
+ var utf82 = new TextEncoder();
1807
+ function err(status, code, message, details) {
1808
+ const error = details === void 0 ? { code, message } : { code, message, details };
1809
+ return { status, body: { success: false, error } };
1810
+ }
1811
+ function requireUserId(headers) {
1812
+ const userId = getHeader(headers, "x-user-id");
1813
+ if (!userId) return { error: err(400, "USER_ID_REQUIRED", "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26") };
1814
+ if (!isValidUUIDv4(userId)) return { error: err(400, "INVALID_USER_ID_FORMAT", "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F") };
1815
+ return { userId };
1816
+ }
1817
+ function validateEntry(entry, index) {
1818
+ if (!isPlainObject(entry)) return err(400, "INVALID_STATE_ENTRY", `entries[${index}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
1819
+ if (typeof entry.namespace !== "string" || !entry.namespace.trim() || entry.namespace.length > MAX_NAMESPACE_CHARS) {
1820
+ return err(400, "INVALID_STATE_NAMESPACE", `entries[${index}].namespace \u5FC5\u987B\u662F 1-${MAX_NAMESPACE_CHARS} \u5B57\u7B26\u7684\u5B57\u7B26\u4E32`);
1821
+ }
1822
+ if (typeof entry.key !== "string" || !entry.key.trim() || entry.key.length > MAX_KEY_CHARS) {
1823
+ return err(400, "INVALID_STATE_KEY", `entries[${index}].key \u5FC5\u987B\u662F 1-${MAX_KEY_CHARS} \u5B57\u7B26\u7684\u5B57\u7B26\u4E32`);
1824
+ }
1825
+ if (typeof entry.value !== "string") {
1826
+ return err(400, "INVALID_STATE_VALUE", `entries[${index}].value \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u5BBF\u4E3B\u81EA\u884C\u5E8F\u5217\u5316\uFF09`);
1827
+ }
1828
+ const bytes = utf82.encode(entry.value).length;
1829
+ if (bytes > MAX_STATE_VALUE_BYTES) {
1830
+ return err(413, "STATE_VALUE_TOO_LARGE", `entries[${index}].value \u8D85\u8FC7\u5355\u6761\u4E0A\u9650`, { index, key: entry.key, bytes, maxBytes: MAX_STATE_VALUE_BYTES });
1831
+ }
1832
+ if (!Number.isInteger(entry.updatedAt) || entry.updatedAt <= 0) {
1833
+ return err(400, "INVALID_STATE_UPDATED_AT", `entries[${index}].updatedAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF08epoch \u6BEB\u79D2\uFF09`);
1834
+ }
1835
+ return null;
1836
+ }
1837
+ function createClientStateHandler(ctx) {
1838
+ async function PUT(headers, body) {
1839
+ const tenantResult = await ctx.tenantManager.resolveTenant(headers);
1840
+ if (!tenantResult.ok) return tenantResult.error;
1841
+ const { db, masterKey } = tenantResult.context;
1842
+ if (getHeader(headers, "x-payload-encrypted") !== "true") {
1843
+ return err(400, "ENCRYPTION_REQUIRED", "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6");
1844
+ }
1845
+ const gate = requireUserId(headers);
1846
+ if (gate.error) return gate.error;
1847
+ const { userId } = gate;
1848
+ if (getHeader(headers, "x-encryption-version") !== "1") {
1849
+ return err(400, "UNSUPPORTED_ENCRYPTION_VERSION", "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301");
1850
+ }
1851
+ const parsedBody = parseEncryptedBody(body);
1852
+ if (!parsedBody.ok) return { status: 400, body: { success: false, error: parsedBody.error } };
1853
+ const userKey = await deriveUserEncryptionKey(userId, masterKey);
1854
+ let payload;
1855
+ try {
1856
+ payload = await decryptPayload(parsedBody.data, userKey);
1857
+ } catch (error) {
1858
+ if (error instanceof SyntaxError) {
1859
+ return err(400, "INVALID_PAYLOAD_FORMAT", "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u4E0D\u662F\u6709\u6548 JSON");
1860
+ }
1861
+ return err(400, "DECRYPTION_FAILED", "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25");
1862
+ }
1863
+ if (!isPlainObject(payload)) return err(400, "INVALID_PAYLOAD_FORMAT", "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u5FC5\u987B\u662F JSON \u5BF9\u8C61");
1864
+ const entries = payload.entries;
1865
+ if (!Array.isArray(entries) || entries.length === 0) {
1866
+ return err(400, "INVALID_STATE_ENTRIES", "entries \u5FC5\u987B\u662F\u975E\u7A7A\u6570\u7EC4");
1867
+ }
1868
+ if (entries.length > MAX_STATE_ENTRIES_PER_REQUEST) {
1869
+ return err(400, "TOO_MANY_STATE_ENTRIES", `\u5355\u6B21\u6700\u591A ${MAX_STATE_ENTRIES_PER_REQUEST} \u6761`, { count: entries.length });
1870
+ }
1871
+ for (let i = 0; i < entries.length; i++) {
1872
+ const invalid = validateEntry(entries[i], i);
1873
+ if (invalid) return invalid;
1874
+ }
1875
+ if (typeof db.upsertClientState !== "function") {
1876
+ return err(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
1877
+ }
1878
+ const encryptedEntries = await Promise.all(entries.map(async (entry) => ({
1879
+ namespace: entry.namespace,
1880
+ key: entry.key,
1881
+ value: await encryptForStorage(entry.value, userKey),
1882
+ updatedAt: entry.updatedAt
1883
+ })));
1884
+ const { upserted, skipped } = await db.upsertClientState(userId, encryptedEntries);
1885
+ return { status: 200, body: { success: true, data: { upserted, skipped } } };
1886
+ }
1887
+ async function GET(url, headers) {
1888
+ const tenantResult = await ctx.tenantManager.resolveTenant(headers);
1889
+ if (!tenantResult.ok) return tenantResult.error;
1890
+ const { db, masterKey } = tenantResult.context;
1891
+ const gate = requireUserId(headers);
1892
+ if (gate.error) return gate.error;
1893
+ const { userId } = gate;
1894
+ const namespace = new URL(url, "https://dummy").searchParams.get("namespace") || "";
1895
+ if (!namespace.trim()) return err(400, "NAMESPACE_REQUIRED", "\u5FC5\u987B\u63D0\u4F9B namespace \u67E5\u8BE2\u53C2\u6570");
1896
+ if (typeof db.getClientState !== "function") {
1897
+ return err(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
1898
+ }
1899
+ const userKey = await deriveUserEncryptionKey(userId, masterKey);
1900
+ const rows = await db.getClientState(userId, namespace);
1901
+ const decrypted = await Promise.all(rows.map(async (row) => ({
1902
+ namespace: row.namespace,
1903
+ key: row.key,
1904
+ value: await decryptFromStorage(row.value, userKey),
1905
+ updatedAt: row.updated_at
1906
+ })));
1907
+ const encryptedResponse = await encryptPayload({ namespace, entries: decrypted }, userKey);
1908
+ return { status: 200, body: { success: true, encrypted: true, version: 1, data: encryptedResponse } };
1909
+ }
1910
+ async function DELETE(url, headers) {
1911
+ const tenantResult = await ctx.tenantManager.resolveTenant(headers);
1912
+ if (!tenantResult.ok) return tenantResult.error;
1913
+ const { db } = tenantResult.context;
1914
+ const gate = requireUserId(headers);
1915
+ if (gate.error) return gate.error;
1916
+ const { userId } = gate;
1917
+ if (typeof db.clearClientState !== "function") {
1918
+ return err(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
1919
+ }
1920
+ const deleted = await db.clearClientState(userId);
1921
+ return { status: 200, body: { success: true, data: { deleted } } };
1922
+ }
1923
+ return { PUT, GET, DELETE };
1924
+ }
1925
+
1457
1926
  // src/server/single-user.js
1458
1927
  function createSingleUserServer(config) {
1459
1928
  if (!config || !config.db) throw new Error("[amsg-server single-user] config.db is required");
@@ -1471,7 +1940,13 @@ function createSingleUserServer(config) {
1471
1940
  privateKey: vapid.privateKey || ""
1472
1941
  },
1473
1942
  webpush: config.webpush || null,
1474
- tenantManager
1943
+ tenantManager,
1944
+ // Fire-time hooks (optional): the in-server instant path fires through
1945
+ // processMessagesByUuid with this ctx, so instant-type tasks can take
1946
+ // the agentic path too.
1947
+ hooks: config.hooks || null,
1948
+ maxToolIterations: config.maxToolIterations,
1949
+ totalTimeoutMs: config.totalTimeoutMs
1475
1950
  };
1476
1951
  return {
1477
1952
  ctx,
@@ -1482,36 +1957,12 @@ function createSingleUserServer(config) {
1482
1957
  updateMessage: createUpdateMessageHandler(ctx),
1483
1958
  cancelMessage: createCancelMessageHandler(ctx),
1484
1959
  messages: createMessagesHandler(ctx),
1485
- vapidPublicKey: createVapidPublicKeyHandler(ctx)
1960
+ vapidPublicKey: createVapidPublicKeyHandler(ctx),
1961
+ clientState: createClientStateHandler(ctx)
1486
1962
  }
1487
1963
  };
1488
1964
  }
1489
1965
 
1490
- // src/server/lib/webcrypto-utils.js
1491
-
1492
- var TEXT_ENCODER = new TextEncoder();
1493
- var TEXT_DECODER = new TextDecoder("utf-8", { fatal: false });
1494
- function utf8(str) {
1495
- return TEXT_ENCODER.encode(String(str));
1496
- }
1497
- function bytesToBase64Url(buf) {
1498
- const bytes = _amsgshared.toUint8.call(void 0, buf);
1499
- let bin = "";
1500
- for (let i = 0; i < bytes.length; i++) {
1501
- bin += String.fromCharCode(bytes[i]);
1502
- }
1503
- const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
1504
- return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
1505
- }
1506
- function jsonToBase64Url(value) {
1507
- return bytesToBase64Url(utf8(JSON.stringify(value)));
1508
- }
1509
- function randomBytes2(n) {
1510
- const out = new Uint8Array(n);
1511
- globalThis.crypto.getRandomValues(out);
1512
- return out;
1513
- }
1514
-
1515
1966
  // src/server/lib/webpush-webcrypto.js
1516
1967
 
1517
1968
  var KEY_INFO_PREFIX = utf8("WebPush: info\0");
@@ -1561,12 +2012,12 @@ async function sendWebPush({ subscription, payload, vapid, ttl, fetch: fetchImpl
1561
2012
  });
1562
2013
  if (!res.ok) {
1563
2014
  const text = await safeReadText(res);
1564
- const err = new Error(
2015
+ const err2 = new Error(
1565
2016
  `Web Push delivery failed: ${res.status} ${res.statusText || ""}${text ? ` \u2014 ${text}` : ""}`
1566
2017
  );
1567
- err.code = "PUSH_SEND_FAILED";
1568
- err.statusCode = res.status;
1569
- throw err;
2018
+ err2.code = "PUSH_SEND_FAILED";
2019
+ err2.statusCode = res.status;
2020
+ throw err2;
1570
2021
  }
1571
2022
  return {
1572
2023
  statusCode: res.status,
@@ -1599,7 +2050,7 @@ async function encryptPushPayload({ plaintext, uaPublicKey, authSecret }) {
1599
2050
  );
1600
2051
  const keyInfo = _amsgshared.concatBytes.call(void 0, KEY_INFO_PREFIX, uaPublicKey, asPublicRaw);
1601
2052
  const ikm = await hkdfSha256(authSecret, ecdhSecret, keyInfo, 32);
1602
- const salt = randomBytes2(16);
2053
+ const salt = randomBytes(16);
1603
2054
  const cekBytes = await hkdfSha256(salt, ikm, CEK_INFO, 16);
1604
2055
  const cek = await globalThis.crypto.subtle.importKey(
1605
2056
  "raw",
@@ -1773,6 +2224,12 @@ function createSingleUserCloudflareWorker(buildConfig) {
1773
2224
  result = await server.handlers.cancelMessage.DELETE(url, headers);
1774
2225
  } else if (method === "GET" && pathname.endsWith("/vapid-public-key")) {
1775
2226
  result = await server.handlers.vapidPublicKey.GET(url, headers);
2227
+ } else if (method === "PUT" && pathname.endsWith("/client-state")) {
2228
+ result = await server.handlers.clientState.PUT(headers, await request.text());
2229
+ } else if (method === "GET" && pathname.endsWith("/client-state")) {
2230
+ result = await server.handlers.clientState.GET(url, headers);
2231
+ } else if (method === "DELETE" && pathname.endsWith("/client-state")) {
2232
+ result = await server.handlers.clientState.DELETE(url, headers);
1776
2233
  } else {
1777
2234
  result = { status: 404, body: { success: false, error: { code: "NOT_FOUND", message: "Unknown route" } } };
1778
2235
  }
@@ -1790,7 +2247,17 @@ function createSingleUserCloudflareWorker(buildConfig) {
1790
2247
  return;
1791
2248
  }
1792
2249
  try {
1793
- await runScheduledTick({ db: cfg.db, masterKey: cfg.masterKey, vapid, webpush: cfg.webpush });
2250
+ await runScheduledTick({
2251
+ db: cfg.db,
2252
+ masterKey: cfg.masterKey,
2253
+ vapid,
2254
+ webpush: cfg.webpush,
2255
+ // Fire-time hooks (optional; see lib/agentic-fire.js). runScheduledTick
2256
+ // spreads its ctx into processSingleMessage, so these ride along.
2257
+ hooks: cfg.hooks || null,
2258
+ maxToolIterations: cfg.maxToolIterations,
2259
+ totalTimeoutMs: cfg.totalTimeoutMs
2260
+ });
1794
2261
  } catch (error) {
1795
2262
  console.error("[amsg single-user] scheduled(): tick failed:", error && error.message);
1796
2263
  }