@rei-standard/amsg-client 2.5.0-next.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -19,21 +19,23 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // src/index.js
20
20
  var src_exports = {};
21
21
  __export(src_exports, {
22
- MESSAGE_KIND: () => import_amsg_shared.MESSAGE_KIND,
23
- MESSAGE_TYPE: () => import_amsg_shared.MESSAGE_TYPE,
24
- PUSH_SOURCE: () => import_amsg_shared.PUSH_SOURCE,
22
+ MESSAGE_KIND: () => import_amsg_shared2.MESSAGE_KIND,
23
+ MESSAGE_TYPE: () => import_amsg_shared2.MESSAGE_TYPE,
24
+ PUSH_SOURCE: () => import_amsg_shared2.PUSH_SOURCE,
25
25
  ReiClient: () => ReiClient,
26
- buildContentPush: () => import_amsg_shared.buildContentPush,
27
- buildErrorPush: () => import_amsg_shared.buildErrorPush,
28
- buildReasoningPush: () => import_amsg_shared.buildReasoningPush,
29
- buildToolRequestPush: () => import_amsg_shared.buildToolRequestPush,
30
- isContentPush: () => import_amsg_shared.isContentPush,
31
- isErrorPush: () => import_amsg_shared.isErrorPush,
32
- isReasoningPush: () => import_amsg_shared.isReasoningPush,
33
- isToolRequestPush: () => import_amsg_shared.isToolRequestPush
26
+ buildContentPush: () => import_amsg_shared2.buildContentPush,
27
+ buildErrorPush: () => import_amsg_shared2.buildErrorPush,
28
+ buildReasoningPush: () => import_amsg_shared2.buildReasoningPush,
29
+ buildToolRequestPush: () => import_amsg_shared2.buildToolRequestPush,
30
+ isContentPush: () => import_amsg_shared2.isContentPush,
31
+ isErrorPush: () => import_amsg_shared2.isErrorPush,
32
+ isReasoningPush: () => import_amsg_shared2.isReasoningPush,
33
+ isToolRequestPush: () => import_amsg_shared2.isToolRequestPush
34
34
  });
35
35
  module.exports = __toCommonJS(src_exports);
36
36
  var import_amsg_shared = require("@rei-standard/amsg-shared");
37
+ var import_amsg_shared2 = require("@rei-standard/amsg-shared");
38
+ var TEXT_ENCODER = new TextEncoder();
37
39
  var AVATAR_URL_MAX_LENGTH = 2048;
38
40
  function makeLocalError(code, message, details) {
39
41
  const err = new Error(`[rei-standard-amsg-client] ${message}`);
@@ -187,11 +189,11 @@ var ReiClient = class {
187
189
  * @param {string} [endpointPath] - Path under the resolved base URL. Default '/instant'.
188
190
  * @param {{ authorization?: string, expectsBackupPush?: boolean }} [opts]
189
191
  * - `authorization`: optional auth header to forward.
190
- * - `expectsBackupPush`: opt-in flag for the dev warning. Set to `true`
191
- * to log a one-shot console.warn confirming you understand the
192
- * "200 ≠ delivered" pitfall and have your own out-of-band check
193
- * (or migrated to `deliver()`). Set to `false` to explicitly silence
194
- * the warning (you have read this contract).
192
+ * - `expectsBackupPush`: opt-in dev reminder. Set to `true` to log a
193
+ * one-shot console.warn that this is a low-level transport and
194
+ * "HTTP 200 ≠ delivery confirmation" once the worker has backup
195
+ * push enabled (amsg-instant 0.9.0+ default). Default (omitted) is
196
+ * silent.
195
197
  * @returns {Promise<Object>} `{ success, data?: { messagesSent, sentAt }, error? }`
196
198
  */
197
199
  async sendInstant(payload, endpointPath = "/instant", opts = {}) {
@@ -201,6 +203,7 @@ var ReiClient = class {
201
203
  endpointPath,
202
204
  { authorization: opts.authorization, methodName: "sendInstant" }
203
205
  );
206
+ headers["Accept"] = "application/json";
204
207
  const res = await fetch(url, { method: "POST", headers, body });
205
208
  return res.json();
206
209
  }
@@ -231,10 +234,10 @@ var ReiClient = class {
231
234
  * @param {(error: unknown) => void} [options.onError]
232
235
  * @param {() => void} [options.onDone]
233
236
  * @param {AbortSignal} [options.signal]
234
- * @param {boolean} [options.expectsBackupPush] - Opt-in flag for the dev warning.
235
- * Set to `true` to log a one-shot console.warn confirming you understand the
236
- * "rejection delivery failure" pitfall and have your own check (or migrated
237
- * to `deliver()`). Set to `false` to explicitly silence the warning.
237
+ * @param {boolean} [options.expectsBackupPush] - Opt-in dev reminder. Set
238
+ * to `true` to log a one-shot console.warn that "rejection delivery
239
+ * failure" once the worker has backup push enabled (amsg-instant 0.9.0+
240
+ * default). Default (omitted) is silent.
238
241
  * @returns {Promise<void>}
239
242
  */
240
243
  async consumeInstantStream(payload, endpointPath = "/instant", options = {}) {
@@ -551,7 +554,7 @@ var ReiClient = class {
551
554
  async subscribePush(vapidPublicKey, registration) {
552
555
  const subscription = await registration.pushManager.subscribe({
553
556
  userVisibleOnly: true,
554
- applicationServerKey: this._urlBase64ToUint8Array(vapidPublicKey)
557
+ applicationServerKey: (0, import_amsg_shared.base64UrlToBytes)(vapidPublicKey)
555
558
  });
556
559
  return subscription;
557
560
  }
@@ -599,7 +602,7 @@ var ReiClient = class {
599
602
  */
600
603
  _assertPayloadSize(bodyJson, methodName) {
601
604
  if (this._maxPayloadBytes == null) return;
602
- const bytes = new TextEncoder().encode(bodyJson).length;
605
+ const bytes = TEXT_ENCODER.encode(bodyJson).length;
603
606
  if (bytes > this._maxPayloadBytes) {
604
607
  throw makeLocalError(
605
608
  "PAYLOAD_TOO_LARGE_LOCAL",
@@ -871,10 +874,10 @@ ${piece}` : piece;
871
874
  return Math.min(defaultGrace, remainingMs);
872
875
  }
873
876
  /**
874
- * One-shot dev warning for low-level instant APIs. The warning is opt-in
875
- * per call via `opts.expectsBackupPush === true`, and can be explicitly
876
- * silenced via `opts.expectsBackupPush === false`. Fires at most once per
877
- * ReiClient instance per method name.
877
+ * One-shot dev reminder for low-level instant APIs. The warning is opt-in
878
+ * per call via `opts.expectsBackupPush === true` and fires at most once
879
+ * per ReiClient instance per method name. Default (omitted or `false`)
880
+ * is silent.
878
881
  *
879
882
  * @private
880
883
  * @param {string} methodName
@@ -886,7 +889,7 @@ ${piece}` : piece;
886
889
  this._lowLevelWarned.add(methodName);
887
890
  const verdict = methodName === "sendInstant" ? "HTTP 200 \u2260 delivery confirmation" : "rejection \u2260 delivery failure";
888
891
  console.warn(
889
- `[rei-standard-amsg-client] ${methodName} is a low-level transport \u2014 ${verdict} when the worker is configured with always-on backup Web Push (amsg-instant 0.9.0+ default). Prefer client.deliver() for a correct delivered / cancelled / timeout / send-failed verdict. Pass expectsBackupPush: false to silence this warning.`
892
+ `[rei-standard-amsg-client] ${methodName} is a low-level transport \u2014 ${verdict} when the worker is configured with always-on backup Web Push (amsg-instant 0.9.0+ default). Prefer client.deliver() for a correct delivered / cancelled / timeout / send-failed verdict.`
890
893
  );
891
894
  }
892
895
  // ─── Crypto helpers (Web Crypto API) ────────────────────────────
@@ -900,7 +903,7 @@ ${piece}` : piece;
900
903
  if (!this._userKey) throw new Error("[rei-standard-amsg-client] Not initialised. Call init() first.");
901
904
  const iv = crypto.getRandomValues(new Uint8Array(12));
902
905
  const key = await crypto.subtle.importKey("raw", this._userKey, { name: "AES-GCM" }, false, ["encrypt"]);
903
- const encoded = new TextEncoder().encode(plaintext);
906
+ const encoded = TEXT_ENCODER.encode(plaintext);
904
907
  const cipherBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);
905
908
  const cipherArr = new Uint8Array(cipherBuf);
906
909
  const encryptedData = cipherArr.slice(0, cipherArr.length - 16);
@@ -953,15 +956,6 @@ ${piece}` : piece;
953
956
  }
954
957
  return arr;
955
958
  }
956
- /** @private */
957
- _urlBase64ToUint8Array(base64String) {
958
- const padding = "=".repeat((4 - base64String.length % 4) % 4);
959
- const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
960
- const raw = atob(base64);
961
- const arr = new Uint8Array(raw.length);
962
- for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
963
- return arr;
964
- }
965
959
  };
966
960
  function normalizeMaxPayloadBytes(value) {
967
961
  if (value === void 0 || value === null) return null;
package/dist/index.d.cts CHANGED
@@ -1,3 +1,4 @@
1
+ import { base64UrlToBytes } from '@rei-standard/amsg-shared';
1
2
  export { MESSAGE_KIND, MESSAGE_TYPE, PUSH_SOURCE, buildContentPush, buildErrorPush, buildReasoningPush, buildToolRequestPush, isContentPush, isErrorPush, isReasoningPush, isToolRequestPush } from '@rei-standard/amsg-shared';
2
3
 
3
4
  /**
@@ -30,6 +31,11 @@ export { MESSAGE_KIND, MESSAGE_TYPE, PUSH_SOURCE, buildContentPush, buildErrorPu
30
31
  * await client.scheduleMessage({ ... });
31
32
  */
32
33
 
34
+
35
+ // `TextEncoder` is stateless — hoist once instead of allocating a fresh
36
+ // instance for every encrypt + payload-size check.
37
+ const TEXT_ENCODER = new TextEncoder();
38
+
33
39
  /** @typedef {import('@rei-standard/amsg-shared').MessageKind} MessageKind */
34
40
  /** @typedef {import('@rei-standard/amsg-shared').MessageType} MessageType */
35
41
  /** @typedef {import('@rei-standard/amsg-shared').PushSource} PushSource */
@@ -400,11 +406,11 @@ class ReiClient {
400
406
  * @param {string} [endpointPath] - Path under the resolved base URL. Default '/instant'.
401
407
  * @param {{ authorization?: string, expectsBackupPush?: boolean }} [opts]
402
408
  * - `authorization`: optional auth header to forward.
403
- * - `expectsBackupPush`: opt-in flag for the dev warning. Set to `true`
404
- * to log a one-shot console.warn confirming you understand the
405
- * "200 ≠ delivered" pitfall and have your own out-of-band check
406
- * (or migrated to `deliver()`). Set to `false` to explicitly silence
407
- * the warning (you have read this contract).
409
+ * - `expectsBackupPush`: opt-in dev reminder. Set to `true` to log a
410
+ * one-shot console.warn that this is a low-level transport and
411
+ * "HTTP 200 ≠ delivery confirmation" once the worker has backup
412
+ * push enabled (amsg-instant 0.9.0+ default). Default (omitted) is
413
+ * silent.
408
414
  * @returns {Promise<Object>} `{ success, data?: { messagesSent, sentAt }, error? }`
409
415
  */
410
416
  async sendInstant(payload, endpointPath = '/instant', opts = {}) {
@@ -415,6 +421,10 @@ class ReiClient {
415
421
  endpointPath,
416
422
  { authorization: opts.authorization, methodName: 'sendInstant' }
417
423
  );
424
+ // Pin the response shape: amsg-instant routes the JSON `{ success, data }`
425
+ // envelope only when the caller asked exclusively for it. Omitting Accept
426
+ // gets the SSE branch and `res.json()` then throws on the SSE bytes.
427
+ headers['Accept'] = 'application/json';
418
428
 
419
429
  const res = await fetch(url, { method: 'POST', headers, body });
420
430
  return res.json();
@@ -447,10 +457,10 @@ class ReiClient {
447
457
  * @param {(error: unknown) => void} [options.onError]
448
458
  * @param {() => void} [options.onDone]
449
459
  * @param {AbortSignal} [options.signal]
450
- * @param {boolean} [options.expectsBackupPush] - Opt-in flag for the dev warning.
451
- * Set to `true` to log a one-shot console.warn confirming you understand the
452
- * "rejection delivery failure" pitfall and have your own check (or migrated
453
- * to `deliver()`). Set to `false` to explicitly silence the warning.
460
+ * @param {boolean} [options.expectsBackupPush] - Opt-in dev reminder. Set
461
+ * to `true` to log a one-shot console.warn that "rejection delivery
462
+ * failure" once the worker has backup push enabled (amsg-instant 0.9.0+
463
+ * default). Default (omitted) is silent.
454
464
  * @returns {Promise<void>}
455
465
  */
456
466
  async consumeInstantStream(payload, endpointPath = '/instant', options = {}) {
@@ -860,7 +870,7 @@ class ReiClient {
860
870
  async subscribePush(vapidPublicKey, registration) {
861
871
  const subscription = await registration.pushManager.subscribe({
862
872
  userVisibleOnly: true,
863
- applicationServerKey: this._urlBase64ToUint8Array(vapidPublicKey)
873
+ applicationServerKey: base64UrlToBytes(vapidPublicKey)
864
874
  });
865
875
  return subscription;
866
876
  }
@@ -911,7 +921,7 @@ class ReiClient {
911
921
  */
912
922
  _assertPayloadSize(bodyJson, methodName) {
913
923
  if (this._maxPayloadBytes == null) return;
914
- const bytes = new TextEncoder().encode(bodyJson).length;
924
+ const bytes = TEXT_ENCODER.encode(bodyJson).length;
915
925
  if (bytes > this._maxPayloadBytes) {
916
926
  throw makeLocalError(
917
927
  'PAYLOAD_TOO_LARGE_LOCAL',
@@ -1204,10 +1214,10 @@ class ReiClient {
1204
1214
  }
1205
1215
 
1206
1216
  /**
1207
- * One-shot dev warning for low-level instant APIs. The warning is opt-in
1208
- * per call via `opts.expectsBackupPush === true`, and can be explicitly
1209
- * silenced via `opts.expectsBackupPush === false`. Fires at most once per
1210
- * ReiClient instance per method name.
1217
+ * One-shot dev reminder for low-level instant APIs. The warning is opt-in
1218
+ * per call via `opts.expectsBackupPush === true` and fires at most once
1219
+ * per ReiClient instance per method name. Default (omitted or `false`)
1220
+ * is silent.
1211
1221
  *
1212
1222
  * @private
1213
1223
  * @param {string} methodName
@@ -1221,7 +1231,7 @@ class ReiClient {
1221
1231
  ? 'HTTP 200 ≠ delivery confirmation'
1222
1232
  : 'rejection ≠ delivery failure';
1223
1233
  console.warn(
1224
- `[rei-standard-amsg-client] ${methodName} is a low-level transport — ${verdict} when the worker is configured with always-on backup Web Push (amsg-instant 0.9.0+ default). Prefer client.deliver() for a correct delivered / cancelled / timeout / send-failed verdict. Pass expectsBackupPush: false to silence this warning.`
1234
+ `[rei-standard-amsg-client] ${methodName} is a low-level transport — ${verdict} when the worker is configured with always-on backup Web Push (amsg-instant 0.9.0+ default). Prefer client.deliver() for a correct delivered / cancelled / timeout / send-failed verdict.`
1225
1235
  );
1226
1236
  }
1227
1237
 
@@ -1238,7 +1248,7 @@ class ReiClient {
1238
1248
 
1239
1249
  const iv = crypto.getRandomValues(new Uint8Array(12));
1240
1250
  const key = await crypto.subtle.importKey('raw', this._userKey, { name: 'AES-GCM' }, false, ['encrypt']);
1241
- const encoded = new TextEncoder().encode(plaintext);
1251
+ const encoded = TEXT_ENCODER.encode(plaintext);
1242
1252
  const cipherBuf = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
1243
1253
 
1244
1254
  // Web Crypto appends the 16-byte auth tag at the end of the ciphertext
@@ -1302,15 +1312,6 @@ class ReiClient {
1302
1312
  return arr;
1303
1313
  }
1304
1314
 
1305
- /** @private */
1306
- _urlBase64ToUint8Array(base64String) {
1307
- const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
1308
- const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
1309
- const raw = atob(base64);
1310
- const arr = new Uint8Array(raw.length);
1311
- for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
1312
- return arr;
1313
- }
1314
1315
  }
1315
1316
 
1316
1317
  function normalizeMaxPayloadBytes(value) {
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { base64UrlToBytes } from '@rei-standard/amsg-shared';
1
2
  export { MESSAGE_KIND, MESSAGE_TYPE, PUSH_SOURCE, buildContentPush, buildErrorPush, buildReasoningPush, buildToolRequestPush, isContentPush, isErrorPush, isReasoningPush, isToolRequestPush } from '@rei-standard/amsg-shared';
2
3
 
3
4
  /**
@@ -30,6 +31,11 @@ export { MESSAGE_KIND, MESSAGE_TYPE, PUSH_SOURCE, buildContentPush, buildErrorPu
30
31
  * await client.scheduleMessage({ ... });
31
32
  */
32
33
 
34
+
35
+ // `TextEncoder` is stateless — hoist once instead of allocating a fresh
36
+ // instance for every encrypt + payload-size check.
37
+ const TEXT_ENCODER = new TextEncoder();
38
+
33
39
  /** @typedef {import('@rei-standard/amsg-shared').MessageKind} MessageKind */
34
40
  /** @typedef {import('@rei-standard/amsg-shared').MessageType} MessageType */
35
41
  /** @typedef {import('@rei-standard/amsg-shared').PushSource} PushSource */
@@ -400,11 +406,11 @@ class ReiClient {
400
406
  * @param {string} [endpointPath] - Path under the resolved base URL. Default '/instant'.
401
407
  * @param {{ authorization?: string, expectsBackupPush?: boolean }} [opts]
402
408
  * - `authorization`: optional auth header to forward.
403
- * - `expectsBackupPush`: opt-in flag for the dev warning. Set to `true`
404
- * to log a one-shot console.warn confirming you understand the
405
- * "200 ≠ delivered" pitfall and have your own out-of-band check
406
- * (or migrated to `deliver()`). Set to `false` to explicitly silence
407
- * the warning (you have read this contract).
409
+ * - `expectsBackupPush`: opt-in dev reminder. Set to `true` to log a
410
+ * one-shot console.warn that this is a low-level transport and
411
+ * "HTTP 200 ≠ delivery confirmation" once the worker has backup
412
+ * push enabled (amsg-instant 0.9.0+ default). Default (omitted) is
413
+ * silent.
408
414
  * @returns {Promise<Object>} `{ success, data?: { messagesSent, sentAt }, error? }`
409
415
  */
410
416
  async sendInstant(payload, endpointPath = '/instant', opts = {}) {
@@ -415,6 +421,10 @@ class ReiClient {
415
421
  endpointPath,
416
422
  { authorization: opts.authorization, methodName: 'sendInstant' }
417
423
  );
424
+ // Pin the response shape: amsg-instant routes the JSON `{ success, data }`
425
+ // envelope only when the caller asked exclusively for it. Omitting Accept
426
+ // gets the SSE branch and `res.json()` then throws on the SSE bytes.
427
+ headers['Accept'] = 'application/json';
418
428
 
419
429
  const res = await fetch(url, { method: 'POST', headers, body });
420
430
  return res.json();
@@ -447,10 +457,10 @@ class ReiClient {
447
457
  * @param {(error: unknown) => void} [options.onError]
448
458
  * @param {() => void} [options.onDone]
449
459
  * @param {AbortSignal} [options.signal]
450
- * @param {boolean} [options.expectsBackupPush] - Opt-in flag for the dev warning.
451
- * Set to `true` to log a one-shot console.warn confirming you understand the
452
- * "rejection delivery failure" pitfall and have your own check (or migrated
453
- * to `deliver()`). Set to `false` to explicitly silence the warning.
460
+ * @param {boolean} [options.expectsBackupPush] - Opt-in dev reminder. Set
461
+ * to `true` to log a one-shot console.warn that "rejection delivery
462
+ * failure" once the worker has backup push enabled (amsg-instant 0.9.0+
463
+ * default). Default (omitted) is silent.
454
464
  * @returns {Promise<void>}
455
465
  */
456
466
  async consumeInstantStream(payload, endpointPath = '/instant', options = {}) {
@@ -860,7 +870,7 @@ class ReiClient {
860
870
  async subscribePush(vapidPublicKey, registration) {
861
871
  const subscription = await registration.pushManager.subscribe({
862
872
  userVisibleOnly: true,
863
- applicationServerKey: this._urlBase64ToUint8Array(vapidPublicKey)
873
+ applicationServerKey: base64UrlToBytes(vapidPublicKey)
864
874
  });
865
875
  return subscription;
866
876
  }
@@ -911,7 +921,7 @@ class ReiClient {
911
921
  */
912
922
  _assertPayloadSize(bodyJson, methodName) {
913
923
  if (this._maxPayloadBytes == null) return;
914
- const bytes = new TextEncoder().encode(bodyJson).length;
924
+ const bytes = TEXT_ENCODER.encode(bodyJson).length;
915
925
  if (bytes > this._maxPayloadBytes) {
916
926
  throw makeLocalError(
917
927
  'PAYLOAD_TOO_LARGE_LOCAL',
@@ -1204,10 +1214,10 @@ class ReiClient {
1204
1214
  }
1205
1215
 
1206
1216
  /**
1207
- * One-shot dev warning for low-level instant APIs. The warning is opt-in
1208
- * per call via `opts.expectsBackupPush === true`, and can be explicitly
1209
- * silenced via `opts.expectsBackupPush === false`. Fires at most once per
1210
- * ReiClient instance per method name.
1217
+ * One-shot dev reminder for low-level instant APIs. The warning is opt-in
1218
+ * per call via `opts.expectsBackupPush === true` and fires at most once
1219
+ * per ReiClient instance per method name. Default (omitted or `false`)
1220
+ * is silent.
1211
1221
  *
1212
1222
  * @private
1213
1223
  * @param {string} methodName
@@ -1221,7 +1231,7 @@ class ReiClient {
1221
1231
  ? 'HTTP 200 ≠ delivery confirmation'
1222
1232
  : 'rejection ≠ delivery failure';
1223
1233
  console.warn(
1224
- `[rei-standard-amsg-client] ${methodName} is a low-level transport — ${verdict} when the worker is configured with always-on backup Web Push (amsg-instant 0.9.0+ default). Prefer client.deliver() for a correct delivered / cancelled / timeout / send-failed verdict. Pass expectsBackupPush: false to silence this warning.`
1234
+ `[rei-standard-amsg-client] ${methodName} is a low-level transport — ${verdict} when the worker is configured with always-on backup Web Push (amsg-instant 0.9.0+ default). Prefer client.deliver() for a correct delivered / cancelled / timeout / send-failed verdict.`
1225
1235
  );
1226
1236
  }
1227
1237
 
@@ -1238,7 +1248,7 @@ class ReiClient {
1238
1248
 
1239
1249
  const iv = crypto.getRandomValues(new Uint8Array(12));
1240
1250
  const key = await crypto.subtle.importKey('raw', this._userKey, { name: 'AES-GCM' }, false, ['encrypt']);
1241
- const encoded = new TextEncoder().encode(plaintext);
1251
+ const encoded = TEXT_ENCODER.encode(plaintext);
1242
1252
  const cipherBuf = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
1243
1253
 
1244
1254
  // Web Crypto appends the 16-byte auth tag at the end of the ciphertext
@@ -1302,15 +1312,6 @@ class ReiClient {
1302
1312
  return arr;
1303
1313
  }
1304
1314
 
1305
- /** @private */
1306
- _urlBase64ToUint8Array(base64String) {
1307
- const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
1308
- const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
1309
- const raw = atob(base64);
1310
- const arr = new Uint8Array(raw.length);
1311
- for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
1312
- return arr;
1313
- }
1314
1315
  }
1315
1316
 
1316
1317
  function normalizeMaxPayloadBytes(value) {
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/index.js
2
+ import { base64UrlToBytes } from "@rei-standard/amsg-shared";
2
3
  import {
3
4
  MESSAGE_KIND,
4
5
  MESSAGE_TYPE,
@@ -12,6 +13,7 @@ import {
12
13
  isToolRequestPush,
13
14
  isErrorPush
14
15
  } from "@rei-standard/amsg-shared";
16
+ var TEXT_ENCODER = new TextEncoder();
15
17
  var AVATAR_URL_MAX_LENGTH = 2048;
16
18
  function makeLocalError(code, message, details) {
17
19
  const err = new Error(`[rei-standard-amsg-client] ${message}`);
@@ -165,11 +167,11 @@ var ReiClient = class {
165
167
  * @param {string} [endpointPath] - Path under the resolved base URL. Default '/instant'.
166
168
  * @param {{ authorization?: string, expectsBackupPush?: boolean }} [opts]
167
169
  * - `authorization`: optional auth header to forward.
168
- * - `expectsBackupPush`: opt-in flag for the dev warning. Set to `true`
169
- * to log a one-shot console.warn confirming you understand the
170
- * "200 ≠ delivered" pitfall and have your own out-of-band check
171
- * (or migrated to `deliver()`). Set to `false` to explicitly silence
172
- * the warning (you have read this contract).
170
+ * - `expectsBackupPush`: opt-in dev reminder. Set to `true` to log a
171
+ * one-shot console.warn that this is a low-level transport and
172
+ * "HTTP 200 ≠ delivery confirmation" once the worker has backup
173
+ * push enabled (amsg-instant 0.9.0+ default). Default (omitted) is
174
+ * silent.
173
175
  * @returns {Promise<Object>} `{ success, data?: { messagesSent, sentAt }, error? }`
174
176
  */
175
177
  async sendInstant(payload, endpointPath = "/instant", opts = {}) {
@@ -179,6 +181,7 @@ var ReiClient = class {
179
181
  endpointPath,
180
182
  { authorization: opts.authorization, methodName: "sendInstant" }
181
183
  );
184
+ headers["Accept"] = "application/json";
182
185
  const res = await fetch(url, { method: "POST", headers, body });
183
186
  return res.json();
184
187
  }
@@ -209,10 +212,10 @@ var ReiClient = class {
209
212
  * @param {(error: unknown) => void} [options.onError]
210
213
  * @param {() => void} [options.onDone]
211
214
  * @param {AbortSignal} [options.signal]
212
- * @param {boolean} [options.expectsBackupPush] - Opt-in flag for the dev warning.
213
- * Set to `true` to log a one-shot console.warn confirming you understand the
214
- * "rejection delivery failure" pitfall and have your own check (or migrated
215
- * to `deliver()`). Set to `false` to explicitly silence the warning.
215
+ * @param {boolean} [options.expectsBackupPush] - Opt-in dev reminder. Set
216
+ * to `true` to log a one-shot console.warn that "rejection delivery
217
+ * failure" once the worker has backup push enabled (amsg-instant 0.9.0+
218
+ * default). Default (omitted) is silent.
216
219
  * @returns {Promise<void>}
217
220
  */
218
221
  async consumeInstantStream(payload, endpointPath = "/instant", options = {}) {
@@ -529,7 +532,7 @@ var ReiClient = class {
529
532
  async subscribePush(vapidPublicKey, registration) {
530
533
  const subscription = await registration.pushManager.subscribe({
531
534
  userVisibleOnly: true,
532
- applicationServerKey: this._urlBase64ToUint8Array(vapidPublicKey)
535
+ applicationServerKey: base64UrlToBytes(vapidPublicKey)
533
536
  });
534
537
  return subscription;
535
538
  }
@@ -577,7 +580,7 @@ var ReiClient = class {
577
580
  */
578
581
  _assertPayloadSize(bodyJson, methodName) {
579
582
  if (this._maxPayloadBytes == null) return;
580
- const bytes = new TextEncoder().encode(bodyJson).length;
583
+ const bytes = TEXT_ENCODER.encode(bodyJson).length;
581
584
  if (bytes > this._maxPayloadBytes) {
582
585
  throw makeLocalError(
583
586
  "PAYLOAD_TOO_LARGE_LOCAL",
@@ -849,10 +852,10 @@ ${piece}` : piece;
849
852
  return Math.min(defaultGrace, remainingMs);
850
853
  }
851
854
  /**
852
- * One-shot dev warning for low-level instant APIs. The warning is opt-in
853
- * per call via `opts.expectsBackupPush === true`, and can be explicitly
854
- * silenced via `opts.expectsBackupPush === false`. Fires at most once per
855
- * ReiClient instance per method name.
855
+ * One-shot dev reminder for low-level instant APIs. The warning is opt-in
856
+ * per call via `opts.expectsBackupPush === true` and fires at most once
857
+ * per ReiClient instance per method name. Default (omitted or `false`)
858
+ * is silent.
856
859
  *
857
860
  * @private
858
861
  * @param {string} methodName
@@ -864,7 +867,7 @@ ${piece}` : piece;
864
867
  this._lowLevelWarned.add(methodName);
865
868
  const verdict = methodName === "sendInstant" ? "HTTP 200 \u2260 delivery confirmation" : "rejection \u2260 delivery failure";
866
869
  console.warn(
867
- `[rei-standard-amsg-client] ${methodName} is a low-level transport \u2014 ${verdict} when the worker is configured with always-on backup Web Push (amsg-instant 0.9.0+ default). Prefer client.deliver() for a correct delivered / cancelled / timeout / send-failed verdict. Pass expectsBackupPush: false to silence this warning.`
870
+ `[rei-standard-amsg-client] ${methodName} is a low-level transport \u2014 ${verdict} when the worker is configured with always-on backup Web Push (amsg-instant 0.9.0+ default). Prefer client.deliver() for a correct delivered / cancelled / timeout / send-failed verdict.`
868
871
  );
869
872
  }
870
873
  // ─── Crypto helpers (Web Crypto API) ────────────────────────────
@@ -878,7 +881,7 @@ ${piece}` : piece;
878
881
  if (!this._userKey) throw new Error("[rei-standard-amsg-client] Not initialised. Call init() first.");
879
882
  const iv = crypto.getRandomValues(new Uint8Array(12));
880
883
  const key = await crypto.subtle.importKey("raw", this._userKey, { name: "AES-GCM" }, false, ["encrypt"]);
881
- const encoded = new TextEncoder().encode(plaintext);
884
+ const encoded = TEXT_ENCODER.encode(plaintext);
882
885
  const cipherBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);
883
886
  const cipherArr = new Uint8Array(cipherBuf);
884
887
  const encryptedData = cipherArr.slice(0, cipherArr.length - 16);
@@ -931,15 +934,6 @@ ${piece}` : piece;
931
934
  }
932
935
  return arr;
933
936
  }
934
- /** @private */
935
- _urlBase64ToUint8Array(base64String) {
936
- const padding = "=".repeat((4 - base64String.length % 4) % 4);
937
- const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
938
- const raw = atob(base64);
939
- const arr = new Uint8Array(raw.length);
940
- for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
941
- return arr;
942
- }
943
937
  };
944
938
  function normalizeMaxPayloadBytes(value) {
945
939
  if (value === void 0 || value === null) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rei-standard/amsg-client",
3
- "version": "2.5.0-next.0",
3
+ "version": "2.5.0",
4
4
  "description": "ReiStandard Active Messaging browser client SDK — also re-exports shared push types, builders, and guards from @rei-standard/amsg-shared",
5
5
  "repository": {
6
6
  "type": "git",