@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 +137 -0
- package/dist/index.d.cts +148 -0
- package/dist/index.d.ts +148 -0
- package/dist/index.mjs +137 -0
- package/package.json +2 -2
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.
|
|
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.
|
|
36
|
+
"@rei-standard/amsg-shared": "^0.4.0-next.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"tsup": "^8.0.0",
|