@tinycloud/sdk-services 3.1.0-beta.7 → 3.1.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/{BaseService-DU-lE-Qx.d.ts → BaseService-B_UKhVht.d.ts} +1 -1
- package/dist/{BaseService-C8zRy0ww.d.cts → BaseService-DWbPbov7.d.cts} +1 -1
- package/dist/encryption/index.cjs +8 -0
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +2 -2
- package/dist/encryption/index.d.ts +2 -2
- package/dist/encryption/index.js +8 -0
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +760 -169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -17
- package/dist/index.d.ts +108 -17
- package/dist/index.js +760 -169
- package/dist/index.js.map +1 -1
- package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
- package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
- package/dist/kv/index.cjs +256 -9
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +182 -3
- package/dist/kv/index.d.ts +182 -3
- package/dist/kv/index.js +256 -9
- package/dist/kv/index.js.map +1 -1
- package/dist/sql/index.cjs +8 -0
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +2 -2
- package/dist/sql/index.d.ts +2 -2
- package/dist/sql/index.js +8 -0
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-qwMmB7qB.d.cts → types-CVwwxqXF.d.cts} +8 -0
- package/dist/{types-qwMmB7qB.d.ts → types-CVwwxqXF.d.ts} +8 -0
- package/package.json +3 -3
package/dist/kv/index.cjs
CHANGED
|
@@ -34,6 +34,10 @@ var ErrorCodes = {
|
|
|
34
34
|
AUTH_EXPIRED: "AUTH_EXPIRED",
|
|
35
35
|
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
36
36
|
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
|
|
37
|
+
/** HTTP 401: the delegation the invocation cites was revoked. */
|
|
38
|
+
AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
|
|
39
|
+
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
40
|
+
AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
|
|
37
41
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
38
42
|
TIMEOUT: "TIMEOUT",
|
|
39
43
|
ABORTED: "ABORTED",
|
|
@@ -45,6 +49,10 @@ var ErrorCodes = {
|
|
|
45
49
|
KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
|
|
46
50
|
KV_CONFLICT: "KV_CONFLICT",
|
|
47
51
|
KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
|
|
52
|
+
/** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
|
|
53
|
+
KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
|
|
54
|
+
/** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
|
|
55
|
+
KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
|
|
48
56
|
// SQL-specific errors
|
|
49
57
|
SQL_ERROR: "SQL_ERROR",
|
|
50
58
|
SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
|
|
@@ -734,6 +742,30 @@ var PrefixedKVService = class _PrefixedKVService {
|
|
|
734
742
|
removePrefix
|
|
735
743
|
});
|
|
736
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* Read the change feed for everything under this prefix. A view with an
|
|
747
|
+
* empty prefix (`""` or `"/"`) is refused: the node does not serve a
|
|
748
|
+
* whole-space feed.
|
|
749
|
+
*/
|
|
750
|
+
async changes(options) {
|
|
751
|
+
if (this._prefix.replace(/\/+$/, "") === "") {
|
|
752
|
+
return err(serviceError(
|
|
753
|
+
ErrorCodes.INVALID_INPUT,
|
|
754
|
+
"KV changes needs a non-empty prefix; this view has none",
|
|
755
|
+
"kv"
|
|
756
|
+
));
|
|
757
|
+
}
|
|
758
|
+
const syncPrefix = `${this._prefix}/`;
|
|
759
|
+
const response = await this._kv.changes({ ...options, prefix: syncPrefix });
|
|
760
|
+
if (!response.ok) return response;
|
|
761
|
+
return ok({
|
|
762
|
+
...response.data,
|
|
763
|
+
changes: response.data.changes.map((change) => ({
|
|
764
|
+
...change,
|
|
765
|
+
key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
|
|
766
|
+
}))
|
|
767
|
+
});
|
|
768
|
+
}
|
|
737
769
|
/**
|
|
738
770
|
* Delete a key.
|
|
739
771
|
*/
|
|
@@ -786,11 +818,36 @@ var KVAction = {
|
|
|
786
818
|
PUT: "tinycloud.kv/put",
|
|
787
819
|
LIST: "tinycloud.kv/list",
|
|
788
820
|
DELETE: "tinycloud.kv/del",
|
|
789
|
-
HEAD: "tinycloud.kv/metadata"
|
|
821
|
+
HEAD: "tinycloud.kv/metadata",
|
|
822
|
+
/** The change feed; never implied by `*` or `tinycloud.kv/*`. */
|
|
823
|
+
SYNC: "tinycloud.kv/sync",
|
|
824
|
+
/** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
|
|
825
|
+
RETAIN: "tinycloud.kv/retain"
|
|
790
826
|
};
|
|
791
827
|
|
|
792
828
|
// src/kv/KVService.ts
|
|
793
829
|
var MAX_KV_BATCH_READ_ITEMS = 100;
|
|
830
|
+
function isJsonObject(value) {
|
|
831
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
832
|
+
}
|
|
833
|
+
var pathByteEncoder = new TextEncoder();
|
|
834
|
+
function comparePathsByBytes(a, b) {
|
|
835
|
+
const left = pathByteEncoder.encode(a);
|
|
836
|
+
const right = pathByteEncoder.encode(b);
|
|
837
|
+
const length = Math.min(left.length, right.length);
|
|
838
|
+
for (let index = 0; index < length; index++) {
|
|
839
|
+
if (left[index] !== right[index]) {
|
|
840
|
+
return left[index] - right[index];
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return left.length - right.length;
|
|
844
|
+
}
|
|
845
|
+
var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
|
|
846
|
+
function revocationCodeOf(body) {
|
|
847
|
+
const match = REVOKED_BODY.exec(body.trim());
|
|
848
|
+
if (match === null) return void 0;
|
|
849
|
+
return match[1] !== void 0 ? ErrorCodes.AUTH_DELEGATION_REVOKED : ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED;
|
|
850
|
+
}
|
|
794
851
|
function encodeKvBatchPartName(path) {
|
|
795
852
|
return encodeURIComponent(path).replace(
|
|
796
853
|
/[!'()*]/g,
|
|
@@ -1018,15 +1075,25 @@ var KVService = class extends BaseService {
|
|
|
1018
1075
|
)?.[1];
|
|
1019
1076
|
return contentType?.includes("application/json") ? JSON.parse(text) : text;
|
|
1020
1077
|
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Validate a batch read response and return its items in the caller's
|
|
1080
|
+
* request order. The node flattens invocation abilities from a `BTreeMap`,
|
|
1081
|
+
* so results arrive sorted by UTF-8 byte order of the path, not in request
|
|
1082
|
+
* order. Items that carry their path in `key` are matched by path; if no
|
|
1083
|
+
* item carries a key, items are assumed to be in the node's byte-sorted
|
|
1084
|
+
* order and the request is aligned the same way. Any mix of keyed and
|
|
1085
|
+
* keyless items, a missing requested path, an unrequested path, or a
|
|
1086
|
+
* malformed item fails closed (returns undefined).
|
|
1087
|
+
*/
|
|
1021
1088
|
normalizeBatchReadResponse(data, paths, requireData) {
|
|
1022
1089
|
if (!data || typeof data !== "object") return void 0;
|
|
1023
1090
|
const response = data;
|
|
1024
|
-
|
|
1091
|
+
const items = response.results;
|
|
1092
|
+
if (!Array.isArray(items) || items.length !== paths.length) {
|
|
1025
1093
|
return void 0;
|
|
1026
1094
|
}
|
|
1027
|
-
for (
|
|
1028
|
-
|
|
1029
|
-
if (!item || typeof item !== "object" || item.key !== paths[index] || typeof item.ok !== "boolean") {
|
|
1095
|
+
for (const item of items) {
|
|
1096
|
+
if (!item || typeof item !== "object" || typeof item.ok !== "boolean") {
|
|
1030
1097
|
return void 0;
|
|
1031
1098
|
}
|
|
1032
1099
|
if (item.ok && (!item.headers || typeof item.headers !== "object" || Object.values(item.headers).some((value) => typeof value !== "string") || requireData && typeof item.dataBase64 !== "string")) {
|
|
@@ -1036,7 +1103,24 @@ var KVService = class extends BaseService {
|
|
|
1036
1103
|
return void 0;
|
|
1037
1104
|
}
|
|
1038
1105
|
}
|
|
1039
|
-
|
|
1106
|
+
if (items.every((item) => typeof item.key === "string")) {
|
|
1107
|
+
const byPath = new Map(items.map((item) => [item.key, item]));
|
|
1108
|
+
if (byPath.size !== items.length) return void 0;
|
|
1109
|
+
const aligned2 = [];
|
|
1110
|
+
for (const path of paths) {
|
|
1111
|
+
const item = byPath.get(path);
|
|
1112
|
+
if (!item) return void 0;
|
|
1113
|
+
aligned2.push(item);
|
|
1114
|
+
}
|
|
1115
|
+
return aligned2;
|
|
1116
|
+
}
|
|
1117
|
+
if (items.some((item) => item.key !== void 0)) return void 0;
|
|
1118
|
+
const requestOrder = paths.map((_, index) => index).sort((a, b) => comparePathsByBytes(paths[a], paths[b]));
|
|
1119
|
+
const aligned = new Array(items.length);
|
|
1120
|
+
for (let position = 0; position < requestOrder.length; position++) {
|
|
1121
|
+
aligned[requestOrder[position]] = items[position];
|
|
1122
|
+
}
|
|
1123
|
+
return aligned;
|
|
1040
1124
|
}
|
|
1041
1125
|
async batchRead(keys, action, options) {
|
|
1042
1126
|
if (!this.requireAuth()) return err(authRequiredError("kv"));
|
|
@@ -1129,19 +1213,19 @@ var KVService = class extends BaseService {
|
|
|
1129
1213
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1130
1214
|
));
|
|
1131
1215
|
}
|
|
1132
|
-
const
|
|
1216
|
+
const items = this.normalizeBatchReadResponse(
|
|
1133
1217
|
await response.json(),
|
|
1134
1218
|
paths,
|
|
1135
1219
|
action === KVAction.GET
|
|
1136
1220
|
);
|
|
1137
|
-
if (!
|
|
1221
|
+
if (!items) {
|
|
1138
1222
|
return err(serviceError(
|
|
1139
1223
|
ErrorCodes.NETWORK_ERROR,
|
|
1140
1224
|
"KV batch read response did not match the requested keys",
|
|
1141
1225
|
"kv"
|
|
1142
1226
|
));
|
|
1143
1227
|
}
|
|
1144
|
-
const results =
|
|
1228
|
+
const results = items.map((item, index) => {
|
|
1145
1229
|
if (!item.ok) {
|
|
1146
1230
|
return {
|
|
1147
1231
|
key: keys[index],
|
|
@@ -1709,6 +1793,169 @@ var KVService = class extends BaseService {
|
|
|
1709
1793
|
}
|
|
1710
1794
|
});
|
|
1711
1795
|
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
1798
|
+
*/
|
|
1799
|
+
async changes(options) {
|
|
1800
|
+
return this.withTelemetry("changes", options?.prefix, async () => {
|
|
1801
|
+
if (!this.requireAuth()) {
|
|
1802
|
+
return err(authRequiredError("kv"));
|
|
1803
|
+
}
|
|
1804
|
+
if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
|
|
1805
|
+
return err(serviceError(
|
|
1806
|
+
ErrorCodes.INVALID_INPUT,
|
|
1807
|
+
"KV changes prefix must be a non-empty string",
|
|
1808
|
+
"kv"
|
|
1809
|
+
));
|
|
1810
|
+
}
|
|
1811
|
+
if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
|
|
1812
|
+
return err(serviceError(
|
|
1813
|
+
ErrorCodes.INVALID_INPUT,
|
|
1814
|
+
"KV changes limit must be an integer from 1 through 1000",
|
|
1815
|
+
"kv"
|
|
1816
|
+
));
|
|
1817
|
+
}
|
|
1818
|
+
const headers = {};
|
|
1819
|
+
if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
|
|
1820
|
+
if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
|
|
1821
|
+
if (options.retentionGrant !== void 0) {
|
|
1822
|
+
headers["x-tinycloud-retention-grant"] = options.retentionGrant;
|
|
1823
|
+
}
|
|
1824
|
+
const request = this.createRequestSignal(options.signal, options.timeout);
|
|
1825
|
+
try {
|
|
1826
|
+
const response = await this.invokeOperation(
|
|
1827
|
+
options.prefix,
|
|
1828
|
+
KVAction.SYNC,
|
|
1829
|
+
void 0,
|
|
1830
|
+
request.signal,
|
|
1831
|
+
headers
|
|
1832
|
+
);
|
|
1833
|
+
if (!response.ok) {
|
|
1834
|
+
return await this.changesFailure(response, options.prefix, request.signal);
|
|
1835
|
+
}
|
|
1836
|
+
const text = await response.text();
|
|
1837
|
+
let body;
|
|
1838
|
+
try {
|
|
1839
|
+
body = JSON.parse(text);
|
|
1840
|
+
} catch {
|
|
1841
|
+
body = void 0;
|
|
1842
|
+
}
|
|
1843
|
+
const page = this.normalizeChangesResponse(body);
|
|
1844
|
+
if (!page) {
|
|
1845
|
+
return err(serviceError(
|
|
1846
|
+
ErrorCodes.NETWORK_ERROR,
|
|
1847
|
+
"KV changes response was not a tinycloud.kv/sync page",
|
|
1848
|
+
"kv",
|
|
1849
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1850
|
+
));
|
|
1851
|
+
}
|
|
1852
|
+
return ok(page);
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
return err(wrapError("kv", error));
|
|
1855
|
+
} finally {
|
|
1856
|
+
request.dispose();
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Map a failed `kv/sync` response. 410 and the retention 403 carry
|
|
1862
|
+
* `{"error":{"code","reason"}}`; a revoked grant is a 401 whose whole body
|
|
1863
|
+
* is the node's revocation error (see REVOKED_BODY). A body read that
|
|
1864
|
+
* fails because the request was cancelled or timed out rethrows, so the
|
|
1865
|
+
* caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
|
|
1866
|
+
*/
|
|
1867
|
+
async changesFailure(response, prefix, signal) {
|
|
1868
|
+
if (response.status === 404) {
|
|
1869
|
+
return this.classifyNotFound(response, prefix);
|
|
1870
|
+
}
|
|
1871
|
+
let errorText;
|
|
1872
|
+
try {
|
|
1873
|
+
errorText = await response.text();
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
if (signal.aborted) throw error;
|
|
1876
|
+
errorText = "";
|
|
1877
|
+
}
|
|
1878
|
+
const meta = { status: response.status, statusText: response.statusText };
|
|
1879
|
+
let reason;
|
|
1880
|
+
let bodyCode;
|
|
1881
|
+
try {
|
|
1882
|
+
const body = JSON.parse(errorText);
|
|
1883
|
+
const error = isJsonObject(body) ? body.error : void 0;
|
|
1884
|
+
if (isJsonObject(error)) {
|
|
1885
|
+
if (typeof error.code === "string") bodyCode = error.code;
|
|
1886
|
+
if (typeof error.reason === "string") reason = error.reason;
|
|
1887
|
+
}
|
|
1888
|
+
} catch {
|
|
1889
|
+
}
|
|
1890
|
+
const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
|
|
1891
|
+
if (response.status === 410) {
|
|
1892
|
+
return err(serviceError(
|
|
1893
|
+
ErrorCodes.KV_SYNC_RESET_REQUIRED,
|
|
1894
|
+
`${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
|
|
1895
|
+
"kv",
|
|
1896
|
+
{ meta: { ...meta, reason } }
|
|
1897
|
+
));
|
|
1898
|
+
}
|
|
1899
|
+
if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
|
|
1900
|
+
return err(serviceError(
|
|
1901
|
+
ErrorCodes.KV_RETENTION_GRANT_REFUSED,
|
|
1902
|
+
`${context}: the retention grant was refused (${reason ?? "reason not given"})`,
|
|
1903
|
+
"kv",
|
|
1904
|
+
{ meta: { ...meta, reason } }
|
|
1905
|
+
));
|
|
1906
|
+
}
|
|
1907
|
+
if (response.status === 401) {
|
|
1908
|
+
const revoked = revocationCodeOf(errorText);
|
|
1909
|
+
if (revoked !== void 0) {
|
|
1910
|
+
return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
if (response.status === 401 || response.status === 403) {
|
|
1914
|
+
return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
|
|
1915
|
+
}
|
|
1916
|
+
return err(serviceError(
|
|
1917
|
+
response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
|
|
1918
|
+
`${context}: ${response.status} - ${errorText}`,
|
|
1919
|
+
"kv",
|
|
1920
|
+
{ meta }
|
|
1921
|
+
));
|
|
1922
|
+
}
|
|
1923
|
+
normalizeChangesResponse(data) {
|
|
1924
|
+
if (!isJsonObject(data)) return void 0;
|
|
1925
|
+
const { changes, more, cursor, source, authority } = data;
|
|
1926
|
+
if (!Array.isArray(changes) || typeof more !== "boolean" || typeof cursor !== "string" || !isJsonObject(source) || typeof source.nodeDid !== "string" || typeof source.space !== "string" || typeof source.prefix !== "string" || !isJsonObject(authority)) {
|
|
1927
|
+
return void 0;
|
|
1928
|
+
}
|
|
1929
|
+
const { notBefore, expiresAt, retainUntil } = authority;
|
|
1930
|
+
if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
|
|
1931
|
+
return void 0;
|
|
1932
|
+
}
|
|
1933
|
+
const parsed = [];
|
|
1934
|
+
for (const change of changes) {
|
|
1935
|
+
if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
|
|
1936
|
+
if (change.deleted === true) {
|
|
1937
|
+
parsed.push({ key: change.key, deleted: true });
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
const { metadata } = change;
|
|
1941
|
+
if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
|
|
1942
|
+
return void 0;
|
|
1943
|
+
}
|
|
1944
|
+
parsed.push({
|
|
1945
|
+
key: change.key,
|
|
1946
|
+
deleted: false,
|
|
1947
|
+
etag: change.etag,
|
|
1948
|
+
metadata
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
return {
|
|
1952
|
+
changes: parsed,
|
|
1953
|
+
more,
|
|
1954
|
+
cursor,
|
|
1955
|
+
source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
|
|
1956
|
+
authority: { notBefore, expiresAt, retainUntil }
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1712
1959
|
/**
|
|
1713
1960
|
* Delete a key.
|
|
1714
1961
|
*/
|