@rei-standard/amsg-server 2.6.0-next.1 → 2.6.0-next.2
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/dist/{chunk-S5DV7AUH.cjs → chunk-GN5QTMCL.cjs} +130 -86
- package/dist/{chunk-6WA3XDPP.mjs → chunk-PVNLB6TF.mjs} +126 -82
- package/dist/{cloudflare-5Ryuxz10.d.cts → cloudflare-B-E51VjP.d.cts} +187 -114
- package/dist/{cloudflare-5Ryuxz10.d.ts → cloudflare-B-E51VjP.d.ts} +187 -114
- package/dist/cloudflare.cjs +2 -2
- package/dist/cloudflare.d.cts +1 -2
- package/dist/cloudflare.d.ts +1 -2
- package/dist/cloudflare.mjs +1 -1
- package/dist/index.cjs +11 -11
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { validateAvatarUrl, readReasoningContent, stripReasoningTags, buildReasoningPush, buildContentPush, toUint8, base64UrlToBytes, normalizeVapidSubject, concatBytes } from '@rei-standard/amsg-shared';
|
|
1
|
+
import { validateAvatarUrl, toUint8, concatBytes, base64UrlToBytes, readReasoningContent, stripReasoningTags, buildReasoningPush, buildContentPush, normalizeVapidSubject } from '@rei-standard/amsg-shared';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Validation utility library (SDK version)
|
|
@@ -407,26 +406,160 @@ function getHeader(headers = {}, name) {
|
|
|
407
406
|
return '';
|
|
408
407
|
}
|
|
409
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Runtime-neutral crypto + encoding helpers.
|
|
411
|
+
*
|
|
412
|
+
* Everything in here is implemented on top of WHATWG-standard primitives
|
|
413
|
+
* (`globalThis.crypto.subtle`, `TextEncoder`, `Uint8Array`) so the package
|
|
414
|
+
* runs on Cloudflare Workers, Vercel Edge, Netlify Edge, Deno, Bun, and
|
|
415
|
+
* Node ≥ 19 with zero polyfills. The Node adapter polyfills
|
|
416
|
+
* `globalThis.crypto` for Node 18 deployments before any of these are
|
|
417
|
+
* touched.
|
|
418
|
+
*/
|
|
419
|
+
|
|
420
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
421
|
+
const TEXT_DECODER = new TextDecoder('utf-8', { fatal: false });
|
|
422
|
+
|
|
423
|
+
/** UTF-8 encode a string into a Uint8Array. */
|
|
424
|
+
function utf8(str) {
|
|
425
|
+
return TEXT_ENCODER.encode(String(str));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** UTF-8 decode a Uint8Array / ArrayBuffer into a string. */
|
|
429
|
+
function utf8Decode(buf) {
|
|
430
|
+
return TEXT_DECODER.decode(toUint8(buf));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
/** Encode bytes as standard base64 (with padding). */
|
|
435
|
+
function bytesToBase64(buf) {
|
|
436
|
+
const bytes = toUint8(buf);
|
|
437
|
+
let bin = '';
|
|
438
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
439
|
+
bin += String.fromCharCode(bytes[i]);
|
|
440
|
+
}
|
|
441
|
+
// btoa is available in all Web Crypto runtimes (browsers, Workers, Node 16+).
|
|
442
|
+
return (typeof btoa === 'function')
|
|
443
|
+
? btoa(bin)
|
|
444
|
+
: Buffer.from(bin, 'binary').toString('base64');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Encode bytes as base64url (no padding). */
|
|
448
|
+
function bytesToBase64Url(buf) {
|
|
449
|
+
return bytesToBase64(buf).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Encode bytes as lowercase hex. */
|
|
453
|
+
function bytesToHex(buf) {
|
|
454
|
+
const bytes = toUint8(buf);
|
|
455
|
+
let out = '';
|
|
456
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
457
|
+
out += bytes[i].toString(16).padStart(2, '0');
|
|
458
|
+
}
|
|
459
|
+
return out;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Decode a hex string into a Uint8Array. */
|
|
463
|
+
function hexToBytes(hex) {
|
|
464
|
+
const out = new Uint8Array(hex.length / 2);
|
|
465
|
+
for (let i = 0; i < out.length; i++) {
|
|
466
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
467
|
+
}
|
|
468
|
+
return out;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
/** Encode a JSON-serializable value as base64url (UTF-8 JSON). */
|
|
473
|
+
function jsonToBase64Url(value) {
|
|
474
|
+
return bytesToBase64Url(utf8(JSON.stringify(value)));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** `crypto.randomUUID()`. The Node adapter polyfills `globalThis.crypto`. */
|
|
478
|
+
function randomUUID() {
|
|
479
|
+
return globalThis.crypto.randomUUID();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Cryptographically random bytes. */
|
|
483
|
+
function randomBytes(n) {
|
|
484
|
+
const out = new Uint8Array(n);
|
|
485
|
+
globalThis.crypto.getRandomValues(out);
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
|
|
410
489
|
/**
|
|
411
490
|
* Encryption utility library (SDK version)
|
|
412
|
-
* ReiStandard SDK v2.0.1
|
|
413
491
|
*
|
|
414
492
|
* Wraps AES-256-GCM operations for request/response and storage encryption.
|
|
493
|
+
* Implemented on `globalThis.crypto.subtle` (Web Crypto) so the module runs
|
|
494
|
+
* on Cloudflare Workers and other edge runtimes without `nodejs_compat`, as
|
|
495
|
+
* well as on Node ≥ 19. All exports are async because SubtleCrypto is.
|
|
496
|
+
*
|
|
497
|
+
* Wire formats are byte-identical to the previous node:crypto implementation
|
|
498
|
+
* (pinned by test/encryption-compat.test.mjs):
|
|
499
|
+
* - payload: 12-byte IV, base64 `{ iv, authTag, encryptedData }`
|
|
500
|
+
* - storage: 16-byte IV, hex `iv:authTag:encryptedData`
|
|
501
|
+
* Web Crypto appends the GCM auth tag to the ciphertext, while these formats
|
|
502
|
+
* carry it detached — aesGcmSeal / aesGcmOpen below own that seam.
|
|
415
503
|
*/
|
|
416
504
|
|
|
417
505
|
|
|
506
|
+
// Decoder for the wire formats' base64 fields. base64UrlToBytes normalizes
|
|
507
|
+
// the url-safe alphabet and padding first, so it accepts both standard and
|
|
508
|
+
// url-safe base64 — same leniency as node's Buffer.from(x, 'base64').
|
|
509
|
+
const base64ToBytes = base64UrlToBytes;
|
|
510
|
+
|
|
511
|
+
const TAG_LENGTH_BYTES = 16; // AES-GCM auth tag emitted on encrypt
|
|
512
|
+
|
|
513
|
+
function importAesKey(hexKey, usage) {
|
|
514
|
+
return globalThis.crypto.subtle.importKey(
|
|
515
|
+
'raw',
|
|
516
|
+
hexToBytes(hexKey),
|
|
517
|
+
{ name: 'AES-GCM' },
|
|
518
|
+
false,
|
|
519
|
+
[usage]
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** AES-256-GCM encrypt; returns the ciphertext and the detached 16-byte tag. */
|
|
524
|
+
async function aesGcmSeal(hexKey, iv, plaintextBytes) {
|
|
525
|
+
const key = await importAesKey(hexKey, 'encrypt');
|
|
526
|
+
const sealed = new Uint8Array(await globalThis.crypto.subtle.encrypt(
|
|
527
|
+
{ name: 'AES-GCM', iv, tagLength: TAG_LENGTH_BYTES * 8 },
|
|
528
|
+
key,
|
|
529
|
+
plaintextBytes
|
|
530
|
+
));
|
|
531
|
+
return {
|
|
532
|
+
ciphertext: sealed.slice(0, sealed.length - TAG_LENGTH_BYTES),
|
|
533
|
+
authTag: sealed.slice(sealed.length - TAG_LENGTH_BYTES)
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** AES-256-GCM decrypt from detached-tag form; returns plaintext bytes. */
|
|
538
|
+
async function aesGcmOpen(hexKey, iv, ciphertext, authTag) {
|
|
539
|
+
const key = await importAesKey(hexKey, 'decrypt');
|
|
540
|
+
// node's decipher accepted GCM tags truncated down to 96 bits; mirror that
|
|
541
|
+
// by sizing tagLength from the provided tag. Anything outside the standard
|
|
542
|
+
// 96–128-bit range keeps 128 and fails authentication, as it did before.
|
|
543
|
+
const tagBits = authTag.length * 8;
|
|
544
|
+
const tagLength = tagBits >= 96 && tagBits <= 128 ? tagBits : 128;
|
|
545
|
+
const plain = await globalThis.crypto.subtle.decrypt(
|
|
546
|
+
{ name: 'AES-GCM', iv, tagLength },
|
|
547
|
+
key,
|
|
548
|
+
concatBytes(ciphertext, authTag)
|
|
549
|
+
);
|
|
550
|
+
return new Uint8Array(plain);
|
|
551
|
+
}
|
|
552
|
+
|
|
418
553
|
/**
|
|
419
554
|
* Derive a user-specific encryption key from the master key.
|
|
420
555
|
*
|
|
421
556
|
* @param {string} userId - Unique user identifier.
|
|
422
557
|
* @param {string} masterKey - 64-char hex master key.
|
|
423
|
-
* @returns {string} 64-char hex key.
|
|
558
|
+
* @returns {Promise<string>} 64-char hex key.
|
|
424
559
|
*/
|
|
425
|
-
function deriveUserEncryptionKey(userId, masterKey) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
.digest('hex')
|
|
429
|
-
.slice(0, 64);
|
|
560
|
+
async function deriveUserEncryptionKey(userId, masterKey) {
|
|
561
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', utf8(masterKey + userId));
|
|
562
|
+
return bytesToHex(new Uint8Array(digest)).slice(0, 64);
|
|
430
563
|
}
|
|
431
564
|
|
|
432
565
|
/**
|
|
@@ -434,25 +567,17 @@ function deriveUserEncryptionKey(userId, masterKey) {
|
|
|
434
567
|
*
|
|
435
568
|
* @param {{ iv: string, authTag: string, encryptedData: string }} encryptedPayload
|
|
436
569
|
* @param {string} encryptionKey - 64-char hex key.
|
|
437
|
-
* @returns {Object} Decrypted JSON object.
|
|
570
|
+
* @returns {Promise<Object>} Decrypted JSON object.
|
|
438
571
|
*/
|
|
439
|
-
function decryptPayload(encryptedPayload, encryptionKey) {
|
|
572
|
+
async function decryptPayload(encryptedPayload, encryptionKey) {
|
|
440
573
|
const { iv, authTag, encryptedData } = encryptedPayload;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
574
|
+
const plain = await aesGcmOpen(
|
|
575
|
+
encryptionKey,
|
|
576
|
+
base64ToBytes(iv),
|
|
577
|
+
base64ToBytes(encryptedData),
|
|
578
|
+
base64ToBytes(authTag)
|
|
446
579
|
);
|
|
447
|
-
|
|
448
|
-
decipher.setAuthTag(Buffer.from(authTag, 'base64'));
|
|
449
|
-
|
|
450
|
-
const decrypted = Buffer.concat([
|
|
451
|
-
decipher.update(Buffer.from(encryptedData, 'base64')),
|
|
452
|
-
decipher.final()
|
|
453
|
-
]);
|
|
454
|
-
|
|
455
|
-
return JSON.parse(decrypted.toString('utf8'));
|
|
580
|
+
return JSON.parse(utf8Decode(plain));
|
|
456
581
|
}
|
|
457
582
|
|
|
458
583
|
/**
|
|
@@ -460,19 +585,17 @@ function decryptPayload(encryptedPayload, encryptionKey) {
|
|
|
460
585
|
*
|
|
461
586
|
* @param {string|Object} payload
|
|
462
587
|
* @param {string} encryptionKey - 64-char hex key.
|
|
463
|
-
* @returns {{ iv: string, authTag: string, encryptedData: string }}
|
|
588
|
+
* @returns {Promise<{ iv: string, authTag: string, encryptedData: string }>}
|
|
464
589
|
*/
|
|
465
|
-
function encryptPayload(payload, encryptionKey) {
|
|
590
|
+
async function encryptPayload(payload, encryptionKey) {
|
|
466
591
|
const plaintext = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
467
|
-
const iv = randomBytes
|
|
468
|
-
const
|
|
469
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
470
|
-
const authTag = cipher.getAuthTag();
|
|
592
|
+
const iv = randomBytes(12);
|
|
593
|
+
const { ciphertext, authTag } = await aesGcmSeal(encryptionKey, iv, utf8(plaintext));
|
|
471
594
|
|
|
472
595
|
return {
|
|
473
|
-
iv: iv
|
|
474
|
-
authTag: authTag
|
|
475
|
-
encryptedData:
|
|
596
|
+
iv: bytesToBase64(iv),
|
|
597
|
+
authTag: bytesToBase64(authTag),
|
|
598
|
+
encryptedData: bytesToBase64(ciphertext)
|
|
476
599
|
};
|
|
477
600
|
}
|
|
478
601
|
|
|
@@ -481,14 +604,12 @@ function encryptPayload(payload, encryptionKey) {
|
|
|
481
604
|
*
|
|
482
605
|
* @param {string} text - Plaintext string.
|
|
483
606
|
* @param {string} encryptionKey - 64-char hex key.
|
|
484
|
-
* @returns {string} Format: iv:authTag:encryptedData
|
|
607
|
+
* @returns {Promise<string>} Format: iv:authTag:encryptedData
|
|
485
608
|
*/
|
|
486
|
-
function encryptForStorage(text, encryptionKey) {
|
|
487
|
-
const iv = randomBytes
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
const authTag = cipher.getAuthTag();
|
|
491
|
-
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
609
|
+
async function encryptForStorage(text, encryptionKey) {
|
|
610
|
+
const iv = randomBytes(16);
|
|
611
|
+
const { ciphertext, authTag } = await aesGcmSeal(encryptionKey, iv, utf8(text));
|
|
612
|
+
return `${bytesToHex(iv)}:${bytesToHex(authTag)}:${bytesToHex(ciphertext)}`;
|
|
492
613
|
}
|
|
493
614
|
|
|
494
615
|
/**
|
|
@@ -496,17 +617,17 @@ function encryptForStorage(text, encryptionKey) {
|
|
|
496
617
|
*
|
|
497
618
|
* @param {string} encryptedText - Format: iv:authTag:encryptedData
|
|
498
619
|
* @param {string} encryptionKey - 64-char hex key.
|
|
499
|
-
* @returns {string} Plaintext string.
|
|
620
|
+
* @returns {Promise<string>} Plaintext string.
|
|
500
621
|
*/
|
|
501
|
-
function decryptFromStorage(encryptedText, encryptionKey) {
|
|
622
|
+
async function decryptFromStorage(encryptedText, encryptionKey) {
|
|
502
623
|
const [ivHex, authTagHex, encryptedDataHex] = encryptedText.split(':');
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
624
|
+
const plain = await aesGcmOpen(
|
|
625
|
+
encryptionKey,
|
|
626
|
+
hexToBytes(ivHex),
|
|
627
|
+
hexToBytes(encryptedDataHex),
|
|
628
|
+
hexToBytes(authTagHex)
|
|
507
629
|
);
|
|
508
|
-
|
|
509
|
-
return decipher.update(encryptedDataHex, 'hex', 'utf8') + decipher.final('utf8');
|
|
630
|
+
return utf8Decode(plain);
|
|
510
631
|
}
|
|
511
632
|
|
|
512
633
|
/**
|
|
@@ -548,7 +669,7 @@ function createGetUserKeyHandler(ctx) {
|
|
|
548
669
|
body: {
|
|
549
670
|
success: true,
|
|
550
671
|
data: {
|
|
551
|
-
userKey: deriveUserEncryptionKey(userId, masterKey),
|
|
672
|
+
userKey: await deriveUserEncryptionKey(userId, masterKey),
|
|
552
673
|
version: 1
|
|
553
674
|
}
|
|
554
675
|
}
|
|
@@ -675,8 +796,8 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
675
796
|
return { success: false, messagesSent: 0, error: 'TENANT_MASTER_KEY_MISSING' };
|
|
676
797
|
}
|
|
677
798
|
|
|
678
|
-
const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
|
|
679
|
-
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
799
|
+
const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
|
|
800
|
+
const decryptedPayload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
|
|
680
801
|
|
|
681
802
|
let messageContent;
|
|
682
803
|
/** @type {unknown} */
|
|
@@ -1076,11 +1197,11 @@ function createScheduleMessageHandler(ctx) {
|
|
|
1076
1197
|
}
|
|
1077
1198
|
|
|
1078
1199
|
const encryptedBody = parsedBody.data;
|
|
1200
|
+
const userKey = await deriveUserEncryptionKey(userId, masterKey);
|
|
1079
1201
|
|
|
1080
1202
|
let payload;
|
|
1081
1203
|
try {
|
|
1082
|
-
|
|
1083
|
-
payload = decryptPayload(encryptedBody, userKey);
|
|
1204
|
+
payload = await decryptPayload(encryptedBody, userKey);
|
|
1084
1205
|
} catch (error) {
|
|
1085
1206
|
if (error instanceof SyntaxError) {
|
|
1086
1207
|
return { status: 400, body: { success: false, error: { code: 'INVALID_PAYLOAD_FORMAT', message: '解密后的数据不是有效 JSON' } } };
|
|
@@ -1105,7 +1226,6 @@ function createScheduleMessageHandler(ctx) {
|
|
|
1105
1226
|
}
|
|
1106
1227
|
|
|
1107
1228
|
const taskUuid = payload.uuid || randomUUID();
|
|
1108
|
-
const userKey = deriveUserEncryptionKey(userId, masterKey);
|
|
1109
1229
|
|
|
1110
1230
|
const fullTaskData = {
|
|
1111
1231
|
contactName: payload.contactName,
|
|
@@ -1133,7 +1253,7 @@ function createScheduleMessageHandler(ctx) {
|
|
|
1133
1253
|
metadata: payload.metadata || {}
|
|
1134
1254
|
};
|
|
1135
1255
|
|
|
1136
|
-
const encryptedPayload = encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
1256
|
+
const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
1137
1257
|
|
|
1138
1258
|
/**
|
|
1139
1259
|
* In-server instant path. Delivers an instant message through this
|
|
@@ -1347,8 +1467,8 @@ async function runScheduledTick(ctx) {
|
|
|
1347
1467
|
}
|
|
1348
1468
|
|
|
1349
1469
|
try {
|
|
1350
|
-
const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
|
|
1351
|
-
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
1470
|
+
const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
|
|
1471
|
+
const decryptedPayload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
|
|
1352
1472
|
|
|
1353
1473
|
if (decryptedPayload.recurrenceType === 'none') {
|
|
1354
1474
|
await db.deleteTaskById(task.id);
|
|
@@ -1458,11 +1578,11 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1458
1578
|
}
|
|
1459
1579
|
|
|
1460
1580
|
const encryptedBody = parsedBody.data;
|
|
1461
|
-
const userKey = deriveUserEncryptionKey(userId, masterKey);
|
|
1581
|
+
const userKey = await deriveUserEncryptionKey(userId, masterKey);
|
|
1462
1582
|
let updates;
|
|
1463
1583
|
|
|
1464
1584
|
try {
|
|
1465
|
-
updates = decryptPayload(encryptedBody, userKey);
|
|
1585
|
+
updates = await decryptPayload(encryptedBody, userKey);
|
|
1466
1586
|
} catch (_error) {
|
|
1467
1587
|
return { status: 400, body: { success: false, error: { code: 'DECRYPTION_FAILED', message: '请求体解密失败' } } };
|
|
1468
1588
|
}
|
|
@@ -1538,7 +1658,7 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1538
1658
|
return { status: 409, body: { success: false, error: { code: 'TASK_ALREADY_COMPLETED', message: '任务已完成或已失败,无法更新' } } };
|
|
1539
1659
|
}
|
|
1540
1660
|
|
|
1541
|
-
const existingData = JSON.parse(decryptFromStorage(existingTask.encrypted_payload, userKey));
|
|
1661
|
+
const existingData = JSON.parse(await decryptFromStorage(existingTask.encrypted_payload, userKey));
|
|
1542
1662
|
|
|
1543
1663
|
// When the caller switches prompt source (completePrompt ↔ messages),
|
|
1544
1664
|
// null out the other so storage stays one-of (matches schedule-message
|
|
@@ -1567,7 +1687,7 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1567
1687
|
...(Object.prototype.hasOwnProperty.call(updates, 'splitPattern') && { splitPattern: updates.splitPattern ?? null })
|
|
1568
1688
|
};
|
|
1569
1689
|
|
|
1570
|
-
const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
|
|
1690
|
+
const encryptedPayload = await encryptForStorage(JSON.stringify(updatedData), userKey);
|
|
1571
1691
|
const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : undefined;
|
|
1572
1692
|
|
|
1573
1693
|
const result = await db.updateTaskByUuid(taskUuid, userId, encryptedPayload, extraFields);
|
|
@@ -1694,10 +1814,10 @@ function createMessagesHandler(ctx) {
|
|
|
1694
1814
|
|
|
1695
1815
|
const { tasks, total } = await db.listTasks(userId, { status, limit, offset });
|
|
1696
1816
|
|
|
1697
|
-
const userKey = deriveUserEncryptionKey(userId, masterKey);
|
|
1817
|
+
const userKey = await deriveUserEncryptionKey(userId, masterKey);
|
|
1698
1818
|
|
|
1699
|
-
const decryptedTasks = tasks.map(task => {
|
|
1700
|
-
const decrypted = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
1819
|
+
const decryptedTasks = await Promise.all(tasks.map(async task => {
|
|
1820
|
+
const decrypted = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
|
|
1701
1821
|
return {
|
|
1702
1822
|
id: task.id,
|
|
1703
1823
|
uuid: task.uuid,
|
|
@@ -1711,13 +1831,13 @@ function createMessagesHandler(ctx) {
|
|
|
1711
1831
|
createdAt: task.created_at,
|
|
1712
1832
|
updatedAt: task.updated_at
|
|
1713
1833
|
};
|
|
1714
|
-
});
|
|
1834
|
+
}));
|
|
1715
1835
|
|
|
1716
1836
|
const responsePayload = {
|
|
1717
1837
|
tasks: decryptedTasks,
|
|
1718
1838
|
pagination: { total, limit, offset, hasMore: offset + limit < total }
|
|
1719
1839
|
};
|
|
1720
|
-
const encryptedResponse = encryptPayload(responsePayload, userKey);
|
|
1840
|
+
const encryptedResponse = await encryptPayload(responsePayload, userKey);
|
|
1721
1841
|
|
|
1722
1842
|
return {
|
|
1723
1843
|
status: 200,
|
|
@@ -2253,53 +2373,6 @@ function createSingleUserServer(config) {
|
|
|
2253
2373
|
};
|
|
2254
2374
|
}
|
|
2255
2375
|
|
|
2256
|
-
/**
|
|
2257
|
-
* Runtime-neutral crypto + encoding helpers.
|
|
2258
|
-
*
|
|
2259
|
-
* Everything in here is implemented on top of WHATWG-standard primitives
|
|
2260
|
-
* (`globalThis.crypto.subtle`, `TextEncoder`, `Uint8Array`) so the package
|
|
2261
|
-
* runs on Cloudflare Workers, Vercel Edge, Netlify Edge, Deno, Bun, and
|
|
2262
|
-
* Node ≥ 19 with zero polyfills. The Node adapter polyfills
|
|
2263
|
-
* `globalThis.crypto` for Node 18 deployments before any of these are
|
|
2264
|
-
* touched.
|
|
2265
|
-
*/
|
|
2266
|
-
|
|
2267
|
-
const TEXT_ENCODER = new TextEncoder();
|
|
2268
|
-
new TextDecoder('utf-8', { fatal: false });
|
|
2269
|
-
|
|
2270
|
-
/** UTF-8 encode a string into a Uint8Array. */
|
|
2271
|
-
function utf8(str) {
|
|
2272
|
-
return TEXT_ENCODER.encode(String(str));
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
/** Encode bytes as base64url (no padding). */
|
|
2277
|
-
function bytesToBase64Url(buf) {
|
|
2278
|
-
const bytes = toUint8(buf);
|
|
2279
|
-
let bin = '';
|
|
2280
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
2281
|
-
bin += String.fromCharCode(bytes[i]);
|
|
2282
|
-
}
|
|
2283
|
-
// btoa is available in all Web Crypto runtimes (browsers, Workers, Node 16+).
|
|
2284
|
-
const b64 = (typeof btoa === 'function')
|
|
2285
|
-
? btoa(bin)
|
|
2286
|
-
: Buffer.from(bin, 'binary').toString('base64');
|
|
2287
|
-
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
/** Encode a JSON-serializable value as base64url (UTF-8 JSON). */
|
|
2292
|
-
function jsonToBase64Url(value) {
|
|
2293
|
-
return bytesToBase64Url(utf8(JSON.stringify(value)));
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
/** Cryptographically random bytes. */
|
|
2297
|
-
function randomBytes(n) {
|
|
2298
|
-
const out = new Uint8Array(n);
|
|
2299
|
-
globalThis.crypto.getRandomValues(out);
|
|
2300
|
-
return out;
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
2376
|
/**
|
|
2304
2377
|
* Web Push — RFC 8030 (transport) + RFC 8291 (aes128gcm payload encryption)
|
|
2305
2378
|
* + RFC 8292 (VAPID).
|