@rei-standard/amsg-client 2.9.0-next.2 → 2.9.0-next.4

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
@@ -170,6 +170,41 @@ var ReiClient = class {
170
170
  if (!json.success) throw new Error(json.error?.message || "Failed to fetch VAPID public key");
171
171
  return json.publicKey;
172
172
  }
173
+ /**
174
+ * Fetch the worker's capability manifest (single-user amsg-server 2.7.0+,
175
+ * `GET /capabilities`).
176
+ *
177
+ * Feature detection for deploy drift: an outdated worker lacks newer
178
+ * endpoints/behaviors silently, so the frontend can call this once and
179
+ * show a "worker needs a redeploy" hint instead of leaving new features
180
+ * dead. Feature names are library-defined strings (e.g. `client-state`,
181
+ * `client-state-chunking`, `agentic-hooks`) that grow over time.
182
+ *
183
+ * Sends `X-Client-Token` when a `serverToken` is configured.
184
+ *
185
+ * @returns {Promise<{ serverVersion: string, features: string[] } | null>}
186
+ * `null` when the worker predates the endpoint (HTTP 404) or the
187
+ * response is not JSON (e.g. a proxy error page). Other failures
188
+ * (wrong token, 5xx with a JSON envelope) throw.
189
+ */
190
+ async getCapabilities() {
191
+ const res = await fetch(`${this._baseUrl}/capabilities`, {
192
+ method: "GET",
193
+ headers: this._withServerToken({})
194
+ });
195
+ if (res.status === 404) return null;
196
+ let json;
197
+ try {
198
+ json = await res.json();
199
+ } catch {
200
+ return null;
201
+ }
202
+ if (!json?.success) throw new Error(json?.error?.message || "Failed to fetch capabilities");
203
+ return {
204
+ serverVersion: typeof json.serverVersion === "string" ? json.serverVersion : "",
205
+ features: Array.isArray(json.features) ? json.features : []
206
+ };
207
+ }
173
208
  // ─── Public API ─────────────────────────────────────────────────
174
209
  /**
175
210
  * Schedule a message.
@@ -597,6 +632,108 @@ var ReiClient = class {
597
632
  data: decrypted
598
633
  };
599
634
  }
635
+ // ─── Client state (single-user cloud mirror) ────────────────────
636
+ /**
637
+ * Batch-upsert client-state entries (single-user worker,
638
+ * amsg-server 2.6.0+ `/client-state`).
639
+ *
640
+ * The worker keeps one live copy per (namespace, key); this client is
641
+ * the only writer. Send everything that changed in ONE call — e.g.
642
+ * inside the few-seconds window before iOS backgrounds the page — the
643
+ * server upserts the whole batch in a single DB round trip. Upserts
644
+ * are last-write-wins on `updatedAt`: entries older than the stored
645
+ * row are skipped, so re-sending a stale batch is harmless.
646
+ *
647
+ * The payload is encrypted like every other amsg-server call
648
+ * (requires `init()`); the worker re-encrypts each value at rest
649
+ * under the per-user key.
650
+ *
651
+ * Large values: on amsg-server 2.7.0+ a value over 200KB is stored
652
+ * chunked across rows by the worker itself — no client-side splitting
653
+ * needed, and reads return the original value reassembled. The default
654
+ * per-value ceiling is 5MB (worker factory config `maxStateValueBytes`).
655
+ * Older workers reject the whole batch for oversized values; probe with
656
+ * `getCapabilities()` (feature `client-state-chunking`) when you need
657
+ * to know which behavior you'll get.
658
+ *
659
+ * Partial failure: an invalid/oversized entry only rejects itself.
660
+ * When at least one entry is rejected the response carries
661
+ * `data.rejected: [{ index, namespace, key, code, message }]`; when all
662
+ * entries are accepted the response shape is unchanged (no `rejected`).
663
+ *
664
+ * @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
665
+ * - `value`: pre-serialized string (the SDK does not stringify it for you).
666
+ * - `updatedAt`: epoch milliseconds.
667
+ * @returns {Promise<Object>} `{ success, data?: { upserted, skipped, rejected? }, error? }`
668
+ */
669
+ async putClientState(entries) {
670
+ if (!Array.isArray(entries) || entries.length === 0) {
671
+ throw new TypeError("[rei-standard-amsg-client] entries must be a non-empty array");
672
+ }
673
+ const json = JSON.stringify({ entries });
674
+ this._assertPayloadSize(json, "putClientState");
675
+ const encrypted = await this._encrypt(json);
676
+ const res = await fetch(`${this._baseUrl}/client-state`, {
677
+ method: "PUT",
678
+ headers: this._withServerToken({
679
+ "Content-Type": "application/json",
680
+ "X-User-Id": this._userId,
681
+ "X-Payload-Encrypted": "true",
682
+ "X-Encryption-Version": "1"
683
+ }),
684
+ body: JSON.stringify(encrypted)
685
+ });
686
+ return res.json();
687
+ }
688
+ /**
689
+ * Read every entry of one client-state namespace.
690
+ *
691
+ * The response rides the encrypted-response envelope (same as
692
+ * `listMessages`); this method decrypts it and returns plaintext
693
+ * values.
694
+ *
695
+ * @param {string} namespace
696
+ * @returns {Promise<Object>} `{ success, data?: { namespace, entries }, error? }`
697
+ * where `entries` is `Array<{ namespace, key, value, updatedAt }>`.
698
+ */
699
+ async getClientState(namespace) {
700
+ if (typeof namespace !== "string" || !namespace.trim()) {
701
+ throw new TypeError("[rei-standard-amsg-client] namespace must be a non-empty string");
702
+ }
703
+ const res = await fetch(
704
+ `${this._baseUrl}/client-state?namespace=${encodeURIComponent(namespace)}`,
705
+ {
706
+ method: "GET",
707
+ headers: this._withServerToken({
708
+ "X-User-Id": this._userId,
709
+ "X-Response-Encrypted": "true",
710
+ "X-Encryption-Version": "1"
711
+ })
712
+ }
713
+ );
714
+ const json = await res.json();
715
+ if (!json?.success || json?.encrypted !== true) return json;
716
+ const decrypted = await this._decrypt(json.data);
717
+ return {
718
+ success: true,
719
+ encrypted: true,
720
+ version: json.version || 1,
721
+ data: decrypted
722
+ };
723
+ }
724
+ /**
725
+ * Wipe every client-state entry of this user, across all namespaces —
726
+ * e.g. behind a "clear cloud state" settings action.
727
+ *
728
+ * @returns {Promise<Object>} `{ success, data?: { deleted }, error? }`
729
+ */
730
+ async clearClientState() {
731
+ const res = await fetch(`${this._baseUrl}/client-state`, {
732
+ method: "DELETE",
733
+ headers: this._withServerToken({ "X-User-Id": this._userId })
734
+ });
735
+ return res.json();
736
+ }
600
737
  // ─── Push Subscription ──────────────────────────────────────────
601
738
  /**
602
739
  * Subscribe to Web Push notifications.
package/dist/index.d.cts CHANGED
@@ -464,6 +464,43 @@ class ReiClient {
464
464
  return json.publicKey;
465
465
  }
466
466
 
467
+ /**
468
+ * Fetch the worker's capability manifest (single-user amsg-server 2.7.0+,
469
+ * `GET /capabilities`).
470
+ *
471
+ * Feature detection for deploy drift: an outdated worker lacks newer
472
+ * endpoints/behaviors silently, so the frontend can call this once and
473
+ * show a "worker needs a redeploy" hint instead of leaving new features
474
+ * dead. Feature names are library-defined strings (e.g. `client-state`,
475
+ * `client-state-chunking`, `agentic-hooks`) that grow over time.
476
+ *
477
+ * Sends `X-Client-Token` when a `serverToken` is configured.
478
+ *
479
+ * @returns {Promise<{ serverVersion: string, features: string[] } | null>}
480
+ * `null` when the worker predates the endpoint (HTTP 404) or the
481
+ * response is not JSON (e.g. a proxy error page). Other failures
482
+ * (wrong token, 5xx with a JSON envelope) throw.
483
+ */
484
+ async getCapabilities() {
485
+ const res = await fetch(`${this._baseUrl}/capabilities`, {
486
+ method: 'GET',
487
+ headers: this._withServerToken({})
488
+ });
489
+ if (res.status === 404) return null;
490
+
491
+ let json;
492
+ try {
493
+ json = await res.json();
494
+ } catch {
495
+ return null;
496
+ }
497
+ if (!json?.success) throw new Error(json?.error?.message || 'Failed to fetch capabilities');
498
+ return {
499
+ serverVersion: typeof json.serverVersion === 'string' ? json.serverVersion : '',
500
+ features: Array.isArray(json.features) ? json.features : []
501
+ };
502
+ }
503
+
467
504
  // ─── Public API ─────────────────────────────────────────────────
468
505
 
469
506
  /**
@@ -992,6 +1029,117 @@ class ReiClient {
992
1029
  };
993
1030
  }
994
1031
 
1032
+ // ─── Client state (single-user cloud mirror) ────────────────────
1033
+
1034
+ /**
1035
+ * Batch-upsert client-state entries (single-user worker,
1036
+ * amsg-server 2.6.0+ `/client-state`).
1037
+ *
1038
+ * The worker keeps one live copy per (namespace, key); this client is
1039
+ * the only writer. Send everything that changed in ONE call — e.g.
1040
+ * inside the few-seconds window before iOS backgrounds the page — the
1041
+ * server upserts the whole batch in a single DB round trip. Upserts
1042
+ * are last-write-wins on `updatedAt`: entries older than the stored
1043
+ * row are skipped, so re-sending a stale batch is harmless.
1044
+ *
1045
+ * The payload is encrypted like every other amsg-server call
1046
+ * (requires `init()`); the worker re-encrypts each value at rest
1047
+ * under the per-user key.
1048
+ *
1049
+ * Large values: on amsg-server 2.7.0+ a value over 200KB is stored
1050
+ * chunked across rows by the worker itself — no client-side splitting
1051
+ * needed, and reads return the original value reassembled. The default
1052
+ * per-value ceiling is 5MB (worker factory config `maxStateValueBytes`).
1053
+ * Older workers reject the whole batch for oversized values; probe with
1054
+ * `getCapabilities()` (feature `client-state-chunking`) when you need
1055
+ * to know which behavior you'll get.
1056
+ *
1057
+ * Partial failure: an invalid/oversized entry only rejects itself.
1058
+ * When at least one entry is rejected the response carries
1059
+ * `data.rejected: [{ index, namespace, key, code, message }]`; when all
1060
+ * entries are accepted the response shape is unchanged (no `rejected`).
1061
+ *
1062
+ * @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
1063
+ * - `value`: pre-serialized string (the SDK does not stringify it for you).
1064
+ * - `updatedAt`: epoch milliseconds.
1065
+ * @returns {Promise<Object>} `{ success, data?: { upserted, skipped, rejected? }, error? }`
1066
+ */
1067
+ async putClientState(entries) {
1068
+ if (!Array.isArray(entries) || entries.length === 0) {
1069
+ throw new TypeError('[rei-standard-amsg-client] entries must be a non-empty array');
1070
+ }
1071
+ const json = JSON.stringify({ entries });
1072
+ this._assertPayloadSize(json, 'putClientState');
1073
+ const encrypted = await this._encrypt(json);
1074
+
1075
+ const res = await fetch(`${this._baseUrl}/client-state`, {
1076
+ method: 'PUT',
1077
+ headers: this._withServerToken({
1078
+ 'Content-Type': 'application/json',
1079
+ 'X-User-Id': this._userId,
1080
+ 'X-Payload-Encrypted': 'true',
1081
+ 'X-Encryption-Version': '1'
1082
+ }),
1083
+ body: JSON.stringify(encrypted)
1084
+ });
1085
+
1086
+ return res.json();
1087
+ }
1088
+
1089
+ /**
1090
+ * Read every entry of one client-state namespace.
1091
+ *
1092
+ * The response rides the encrypted-response envelope (same as
1093
+ * `listMessages`); this method decrypts it and returns plaintext
1094
+ * values.
1095
+ *
1096
+ * @param {string} namespace
1097
+ * @returns {Promise<Object>} `{ success, data?: { namespace, entries }, error? }`
1098
+ * where `entries` is `Array<{ namespace, key, value, updatedAt }>`.
1099
+ */
1100
+ async getClientState(namespace) {
1101
+ if (typeof namespace !== 'string' || !namespace.trim()) {
1102
+ throw new TypeError('[rei-standard-amsg-client] namespace must be a non-empty string');
1103
+ }
1104
+ const res = await fetch(
1105
+ `${this._baseUrl}/client-state?namespace=${encodeURIComponent(namespace)}`,
1106
+ {
1107
+ method: 'GET',
1108
+ headers: this._withServerToken({
1109
+ 'X-User-Id': this._userId,
1110
+ 'X-Response-Encrypted': 'true',
1111
+ 'X-Encryption-Version': '1'
1112
+ })
1113
+ }
1114
+ );
1115
+
1116
+ const json = await res.json();
1117
+ if (!json?.success || json?.encrypted !== true) return json;
1118
+
1119
+ const decrypted = await this._decrypt(json.data);
1120
+ return {
1121
+ success: true,
1122
+ encrypted: true,
1123
+ version: json.version || 1,
1124
+ data: decrypted
1125
+ };
1126
+ }
1127
+
1128
+ /**
1129
+ * Wipe every client-state entry of this user, across all namespaces —
1130
+ * e.g. behind a "clear cloud state" settings action.
1131
+ *
1132
+ * @returns {Promise<Object>} `{ success, data?: { deleted }, error? }`
1133
+ */
1134
+ async clearClientState() {
1135
+ const res = await fetch(`${this._baseUrl}/client-state`, {
1136
+ method: 'DELETE',
1137
+ headers: this._withServerToken({ 'X-User-Id': this._userId })
1138
+ });
1139
+
1140
+ return res.json();
1141
+ }
1142
+
995
1143
  // ─── Push Subscription ──────────────────────────────────────────
996
1144
 
997
1145
  /**
package/dist/index.d.ts CHANGED
@@ -464,6 +464,43 @@ class ReiClient {
464
464
  return json.publicKey;
465
465
  }
466
466
 
467
+ /**
468
+ * Fetch the worker's capability manifest (single-user amsg-server 2.7.0+,
469
+ * `GET /capabilities`).
470
+ *
471
+ * Feature detection for deploy drift: an outdated worker lacks newer
472
+ * endpoints/behaviors silently, so the frontend can call this once and
473
+ * show a "worker needs a redeploy" hint instead of leaving new features
474
+ * dead. Feature names are library-defined strings (e.g. `client-state`,
475
+ * `client-state-chunking`, `agentic-hooks`) that grow over time.
476
+ *
477
+ * Sends `X-Client-Token` when a `serverToken` is configured.
478
+ *
479
+ * @returns {Promise<{ serverVersion: string, features: string[] } | null>}
480
+ * `null` when the worker predates the endpoint (HTTP 404) or the
481
+ * response is not JSON (e.g. a proxy error page). Other failures
482
+ * (wrong token, 5xx with a JSON envelope) throw.
483
+ */
484
+ async getCapabilities() {
485
+ const res = await fetch(`${this._baseUrl}/capabilities`, {
486
+ method: 'GET',
487
+ headers: this._withServerToken({})
488
+ });
489
+ if (res.status === 404) return null;
490
+
491
+ let json;
492
+ try {
493
+ json = await res.json();
494
+ } catch {
495
+ return null;
496
+ }
497
+ if (!json?.success) throw new Error(json?.error?.message || 'Failed to fetch capabilities');
498
+ return {
499
+ serverVersion: typeof json.serverVersion === 'string' ? json.serverVersion : '',
500
+ features: Array.isArray(json.features) ? json.features : []
501
+ };
502
+ }
503
+
467
504
  // ─── Public API ─────────────────────────────────────────────────
468
505
 
469
506
  /**
@@ -992,6 +1029,117 @@ class ReiClient {
992
1029
  };
993
1030
  }
994
1031
 
1032
+ // ─── Client state (single-user cloud mirror) ────────────────────
1033
+
1034
+ /**
1035
+ * Batch-upsert client-state entries (single-user worker,
1036
+ * amsg-server 2.6.0+ `/client-state`).
1037
+ *
1038
+ * The worker keeps one live copy per (namespace, key); this client is
1039
+ * the only writer. Send everything that changed in ONE call — e.g.
1040
+ * inside the few-seconds window before iOS backgrounds the page — the
1041
+ * server upserts the whole batch in a single DB round trip. Upserts
1042
+ * are last-write-wins on `updatedAt`: entries older than the stored
1043
+ * row are skipped, so re-sending a stale batch is harmless.
1044
+ *
1045
+ * The payload is encrypted like every other amsg-server call
1046
+ * (requires `init()`); the worker re-encrypts each value at rest
1047
+ * under the per-user key.
1048
+ *
1049
+ * Large values: on amsg-server 2.7.0+ a value over 200KB is stored
1050
+ * chunked across rows by the worker itself — no client-side splitting
1051
+ * needed, and reads return the original value reassembled. The default
1052
+ * per-value ceiling is 5MB (worker factory config `maxStateValueBytes`).
1053
+ * Older workers reject the whole batch for oversized values; probe with
1054
+ * `getCapabilities()` (feature `client-state-chunking`) when you need
1055
+ * to know which behavior you'll get.
1056
+ *
1057
+ * Partial failure: an invalid/oversized entry only rejects itself.
1058
+ * When at least one entry is rejected the response carries
1059
+ * `data.rejected: [{ index, namespace, key, code, message }]`; when all
1060
+ * entries are accepted the response shape is unchanged (no `rejected`).
1061
+ *
1062
+ * @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
1063
+ * - `value`: pre-serialized string (the SDK does not stringify it for you).
1064
+ * - `updatedAt`: epoch milliseconds.
1065
+ * @returns {Promise<Object>} `{ success, data?: { upserted, skipped, rejected? }, error? }`
1066
+ */
1067
+ async putClientState(entries) {
1068
+ if (!Array.isArray(entries) || entries.length === 0) {
1069
+ throw new TypeError('[rei-standard-amsg-client] entries must be a non-empty array');
1070
+ }
1071
+ const json = JSON.stringify({ entries });
1072
+ this._assertPayloadSize(json, 'putClientState');
1073
+ const encrypted = await this._encrypt(json);
1074
+
1075
+ const res = await fetch(`${this._baseUrl}/client-state`, {
1076
+ method: 'PUT',
1077
+ headers: this._withServerToken({
1078
+ 'Content-Type': 'application/json',
1079
+ 'X-User-Id': this._userId,
1080
+ 'X-Payload-Encrypted': 'true',
1081
+ 'X-Encryption-Version': '1'
1082
+ }),
1083
+ body: JSON.stringify(encrypted)
1084
+ });
1085
+
1086
+ return res.json();
1087
+ }
1088
+
1089
+ /**
1090
+ * Read every entry of one client-state namespace.
1091
+ *
1092
+ * The response rides the encrypted-response envelope (same as
1093
+ * `listMessages`); this method decrypts it and returns plaintext
1094
+ * values.
1095
+ *
1096
+ * @param {string} namespace
1097
+ * @returns {Promise<Object>} `{ success, data?: { namespace, entries }, error? }`
1098
+ * where `entries` is `Array<{ namespace, key, value, updatedAt }>`.
1099
+ */
1100
+ async getClientState(namespace) {
1101
+ if (typeof namespace !== 'string' || !namespace.trim()) {
1102
+ throw new TypeError('[rei-standard-amsg-client] namespace must be a non-empty string');
1103
+ }
1104
+ const res = await fetch(
1105
+ `${this._baseUrl}/client-state?namespace=${encodeURIComponent(namespace)}`,
1106
+ {
1107
+ method: 'GET',
1108
+ headers: this._withServerToken({
1109
+ 'X-User-Id': this._userId,
1110
+ 'X-Response-Encrypted': 'true',
1111
+ 'X-Encryption-Version': '1'
1112
+ })
1113
+ }
1114
+ );
1115
+
1116
+ const json = await res.json();
1117
+ if (!json?.success || json?.encrypted !== true) return json;
1118
+
1119
+ const decrypted = await this._decrypt(json.data);
1120
+ return {
1121
+ success: true,
1122
+ encrypted: true,
1123
+ version: json.version || 1,
1124
+ data: decrypted
1125
+ };
1126
+ }
1127
+
1128
+ /**
1129
+ * Wipe every client-state entry of this user, across all namespaces —
1130
+ * e.g. behind a "clear cloud state" settings action.
1131
+ *
1132
+ * @returns {Promise<Object>} `{ success, data?: { deleted }, error? }`
1133
+ */
1134
+ async clearClientState() {
1135
+ const res = await fetch(`${this._baseUrl}/client-state`, {
1136
+ method: 'DELETE',
1137
+ headers: this._withServerToken({ 'X-User-Id': this._userId })
1138
+ });
1139
+
1140
+ return res.json();
1141
+ }
1142
+
995
1143
  // ─── Push Subscription ──────────────────────────────────────────
996
1144
 
997
1145
  /**
package/dist/index.mjs CHANGED
@@ -148,6 +148,41 @@ var ReiClient = class {
148
148
  if (!json.success) throw new Error(json.error?.message || "Failed to fetch VAPID public key");
149
149
  return json.publicKey;
150
150
  }
151
+ /**
152
+ * Fetch the worker's capability manifest (single-user amsg-server 2.7.0+,
153
+ * `GET /capabilities`).
154
+ *
155
+ * Feature detection for deploy drift: an outdated worker lacks newer
156
+ * endpoints/behaviors silently, so the frontend can call this once and
157
+ * show a "worker needs a redeploy" hint instead of leaving new features
158
+ * dead. Feature names are library-defined strings (e.g. `client-state`,
159
+ * `client-state-chunking`, `agentic-hooks`) that grow over time.
160
+ *
161
+ * Sends `X-Client-Token` when a `serverToken` is configured.
162
+ *
163
+ * @returns {Promise<{ serverVersion: string, features: string[] } | null>}
164
+ * `null` when the worker predates the endpoint (HTTP 404) or the
165
+ * response is not JSON (e.g. a proxy error page). Other failures
166
+ * (wrong token, 5xx with a JSON envelope) throw.
167
+ */
168
+ async getCapabilities() {
169
+ const res = await fetch(`${this._baseUrl}/capabilities`, {
170
+ method: "GET",
171
+ headers: this._withServerToken({})
172
+ });
173
+ if (res.status === 404) return null;
174
+ let json;
175
+ try {
176
+ json = await res.json();
177
+ } catch {
178
+ return null;
179
+ }
180
+ if (!json?.success) throw new Error(json?.error?.message || "Failed to fetch capabilities");
181
+ return {
182
+ serverVersion: typeof json.serverVersion === "string" ? json.serverVersion : "",
183
+ features: Array.isArray(json.features) ? json.features : []
184
+ };
185
+ }
151
186
  // ─── Public API ─────────────────────────────────────────────────
152
187
  /**
153
188
  * Schedule a message.
@@ -575,6 +610,108 @@ var ReiClient = class {
575
610
  data: decrypted
576
611
  };
577
612
  }
613
+ // ─── Client state (single-user cloud mirror) ────────────────────
614
+ /**
615
+ * Batch-upsert client-state entries (single-user worker,
616
+ * amsg-server 2.6.0+ `/client-state`).
617
+ *
618
+ * The worker keeps one live copy per (namespace, key); this client is
619
+ * the only writer. Send everything that changed in ONE call — e.g.
620
+ * inside the few-seconds window before iOS backgrounds the page — the
621
+ * server upserts the whole batch in a single DB round trip. Upserts
622
+ * are last-write-wins on `updatedAt`: entries older than the stored
623
+ * row are skipped, so re-sending a stale batch is harmless.
624
+ *
625
+ * The payload is encrypted like every other amsg-server call
626
+ * (requires `init()`); the worker re-encrypts each value at rest
627
+ * under the per-user key.
628
+ *
629
+ * Large values: on amsg-server 2.7.0+ a value over 200KB is stored
630
+ * chunked across rows by the worker itself — no client-side splitting
631
+ * needed, and reads return the original value reassembled. The default
632
+ * per-value ceiling is 5MB (worker factory config `maxStateValueBytes`).
633
+ * Older workers reject the whole batch for oversized values; probe with
634
+ * `getCapabilities()` (feature `client-state-chunking`) when you need
635
+ * to know which behavior you'll get.
636
+ *
637
+ * Partial failure: an invalid/oversized entry only rejects itself.
638
+ * When at least one entry is rejected the response carries
639
+ * `data.rejected: [{ index, namespace, key, code, message }]`; when all
640
+ * entries are accepted the response shape is unchanged (no `rejected`).
641
+ *
642
+ * @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
643
+ * - `value`: pre-serialized string (the SDK does not stringify it for you).
644
+ * - `updatedAt`: epoch milliseconds.
645
+ * @returns {Promise<Object>} `{ success, data?: { upserted, skipped, rejected? }, error? }`
646
+ */
647
+ async putClientState(entries) {
648
+ if (!Array.isArray(entries) || entries.length === 0) {
649
+ throw new TypeError("[rei-standard-amsg-client] entries must be a non-empty array");
650
+ }
651
+ const json = JSON.stringify({ entries });
652
+ this._assertPayloadSize(json, "putClientState");
653
+ const encrypted = await this._encrypt(json);
654
+ const res = await fetch(`${this._baseUrl}/client-state`, {
655
+ method: "PUT",
656
+ headers: this._withServerToken({
657
+ "Content-Type": "application/json",
658
+ "X-User-Id": this._userId,
659
+ "X-Payload-Encrypted": "true",
660
+ "X-Encryption-Version": "1"
661
+ }),
662
+ body: JSON.stringify(encrypted)
663
+ });
664
+ return res.json();
665
+ }
666
+ /**
667
+ * Read every entry of one client-state namespace.
668
+ *
669
+ * The response rides the encrypted-response envelope (same as
670
+ * `listMessages`); this method decrypts it and returns plaintext
671
+ * values.
672
+ *
673
+ * @param {string} namespace
674
+ * @returns {Promise<Object>} `{ success, data?: { namespace, entries }, error? }`
675
+ * where `entries` is `Array<{ namespace, key, value, updatedAt }>`.
676
+ */
677
+ async getClientState(namespace) {
678
+ if (typeof namespace !== "string" || !namespace.trim()) {
679
+ throw new TypeError("[rei-standard-amsg-client] namespace must be a non-empty string");
680
+ }
681
+ const res = await fetch(
682
+ `${this._baseUrl}/client-state?namespace=${encodeURIComponent(namespace)}`,
683
+ {
684
+ method: "GET",
685
+ headers: this._withServerToken({
686
+ "X-User-Id": this._userId,
687
+ "X-Response-Encrypted": "true",
688
+ "X-Encryption-Version": "1"
689
+ })
690
+ }
691
+ );
692
+ const json = await res.json();
693
+ if (!json?.success || json?.encrypted !== true) return json;
694
+ const decrypted = await this._decrypt(json.data);
695
+ return {
696
+ success: true,
697
+ encrypted: true,
698
+ version: json.version || 1,
699
+ data: decrypted
700
+ };
701
+ }
702
+ /**
703
+ * Wipe every client-state entry of this user, across all namespaces —
704
+ * e.g. behind a "clear cloud state" settings action.
705
+ *
706
+ * @returns {Promise<Object>} `{ success, data?: { deleted }, error? }`
707
+ */
708
+ async clearClientState() {
709
+ const res = await fetch(`${this._baseUrl}/client-state`, {
710
+ method: "DELETE",
711
+ headers: this._withServerToken({ "X-User-Id": this._userId })
712
+ });
713
+ return res.json();
714
+ }
578
715
  // ─── Push Subscription ──────────────────────────────────────────
579
716
  /**
580
717
  * Subscribe to Web Push notifications.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rei-standard/amsg-client",
3
- "version": "2.9.0-next.2",
3
+ "version": "2.9.0-next.4",
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",
@@ -33,7 +33,7 @@
33
33
  "node": ">=20"
34
34
  },
35
35
  "dependencies": {
36
- "@rei-standard/amsg-shared": "^0.4.0-next.0"
36
+ "@rei-standard/amsg-shared": "^0.4.0-next.1"
37
37
  },
38
38
  "devDependencies": {
39
39
  "tsup": "^8.0.0",