@tinycloud/sdk-services 3.1.0-beta.7 → 3.1.0-beta.9

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.
@@ -1,4 +1,4 @@
1
- import { P as PermissionHint } from '../types-qwMmB7qB.cjs';
1
+ import { P as PermissionHint } from '../types-CVwwxqXF.cjs';
2
2
 
3
3
  /** A reachable node returned an HTTP failure for a decrypt request. */
4
4
  declare class DecryptTransportResponseError extends Error {
@@ -1,4 +1,4 @@
1
- import { P as PermissionHint } from '../types-qwMmB7qB.js';
1
+ import { P as PermissionHint } from '../types-CVwwxqXF.js';
2
2
 
3
3
  /** A reachable node returned an HTTP failure for a decrypt request. */
4
4
  declare class DecryptTransportResponseError extends Error {
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,18 @@ 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
+ }
794
833
  function encodeKvBatchPartName(path) {
795
834
  return encodeURIComponent(path).replace(
796
835
  /[!'()*]/g,
@@ -1709,6 +1748,169 @@ var KVService = class extends BaseService {
1709
1748
  }
1710
1749
  });
1711
1750
  }
1751
+ /**
1752
+ * Read one page of the `tinycloud.kv/sync` change feed for a prefix.
1753
+ */
1754
+ async changes(options) {
1755
+ return this.withTelemetry("changes", options?.prefix, async () => {
1756
+ if (!this.requireAuth()) {
1757
+ return err(authRequiredError("kv"));
1758
+ }
1759
+ if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
1760
+ return err(serviceError(
1761
+ ErrorCodes.INVALID_INPUT,
1762
+ "KV changes prefix must be a non-empty string",
1763
+ "kv"
1764
+ ));
1765
+ }
1766
+ if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
1767
+ return err(serviceError(
1768
+ ErrorCodes.INVALID_INPUT,
1769
+ "KV changes limit must be an integer from 1 through 1000",
1770
+ "kv"
1771
+ ));
1772
+ }
1773
+ const headers = {};
1774
+ if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
1775
+ if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
1776
+ if (options.retentionGrant !== void 0) {
1777
+ headers["x-tinycloud-retention-grant"] = options.retentionGrant;
1778
+ }
1779
+ const request = this.createRequestSignal(options.signal, options.timeout);
1780
+ try {
1781
+ const response = await this.invokeOperation(
1782
+ options.prefix,
1783
+ KVAction.SYNC,
1784
+ void 0,
1785
+ request.signal,
1786
+ headers
1787
+ );
1788
+ if (!response.ok) {
1789
+ return await this.changesFailure(response, options.prefix, request.signal);
1790
+ }
1791
+ const text = await response.text();
1792
+ let body;
1793
+ try {
1794
+ body = JSON.parse(text);
1795
+ } catch {
1796
+ body = void 0;
1797
+ }
1798
+ const page = this.normalizeChangesResponse(body);
1799
+ if (!page) {
1800
+ return err(serviceError(
1801
+ ErrorCodes.NETWORK_ERROR,
1802
+ "KV changes response was not a tinycloud.kv/sync page",
1803
+ "kv",
1804
+ { meta: { status: response.status, statusText: response.statusText } }
1805
+ ));
1806
+ }
1807
+ return ok(page);
1808
+ } catch (error) {
1809
+ return err(wrapError("kv", error));
1810
+ } finally {
1811
+ request.dispose();
1812
+ }
1813
+ });
1814
+ }
1815
+ /**
1816
+ * Map a failed `kv/sync` response. 410 and the retention 403 carry
1817
+ * `{"error":{"code","reason"}}`; a revoked grant is a 401 whose text names
1818
+ * `delegation-revoked` or `delegation-ancestor-revoked`. A body read that
1819
+ * fails because the request was cancelled or timed out rethrows, so the
1820
+ * caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
1821
+ */
1822
+ async changesFailure(response, prefix, signal) {
1823
+ if (response.status === 404) {
1824
+ return this.classifyNotFound(response, prefix);
1825
+ }
1826
+ let errorText;
1827
+ try {
1828
+ errorText = await response.text();
1829
+ } catch (error) {
1830
+ if (signal.aborted) throw error;
1831
+ errorText = "";
1832
+ }
1833
+ const meta = { status: response.status, statusText: response.statusText };
1834
+ let reason;
1835
+ let bodyCode;
1836
+ try {
1837
+ const body = JSON.parse(errorText);
1838
+ const error = isJsonObject(body) ? body.error : void 0;
1839
+ if (isJsonObject(error)) {
1840
+ if (typeof error.code === "string") bodyCode = error.code;
1841
+ if (typeof error.reason === "string") reason = error.reason;
1842
+ }
1843
+ } catch {
1844
+ }
1845
+ const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
1846
+ if (response.status === 410) {
1847
+ return err(serviceError(
1848
+ ErrorCodes.KV_SYNC_RESET_REQUIRED,
1849
+ `${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
1850
+ "kv",
1851
+ { meta: { ...meta, reason } }
1852
+ ));
1853
+ }
1854
+ if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
1855
+ return err(serviceError(
1856
+ ErrorCodes.KV_RETENTION_GRANT_REFUSED,
1857
+ `${context}: the retention grant was refused (${reason ?? "reason not given"})`,
1858
+ "kv",
1859
+ { meta: { ...meta, reason } }
1860
+ ));
1861
+ }
1862
+ if (response.status === 401) {
1863
+ const revoked = /\bdelegation-ancestor-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED : /\bdelegation-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_REVOKED : void 0;
1864
+ if (revoked !== void 0) {
1865
+ return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
1866
+ }
1867
+ }
1868
+ if (response.status === 401 || response.status === 403) {
1869
+ return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
1870
+ }
1871
+ return err(serviceError(
1872
+ response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
1873
+ `${context}: ${response.status} - ${errorText}`,
1874
+ "kv",
1875
+ { meta }
1876
+ ));
1877
+ }
1878
+ normalizeChangesResponse(data) {
1879
+ if (!isJsonObject(data)) return void 0;
1880
+ const { changes, more, cursor, source, authority } = data;
1881
+ 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)) {
1882
+ return void 0;
1883
+ }
1884
+ const { notBefore, expiresAt, retainUntil } = authority;
1885
+ if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
1886
+ return void 0;
1887
+ }
1888
+ const parsed = [];
1889
+ for (const change of changes) {
1890
+ if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
1891
+ if (change.deleted === true) {
1892
+ parsed.push({ key: change.key, deleted: true });
1893
+ continue;
1894
+ }
1895
+ const { metadata } = change;
1896
+ if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
1897
+ return void 0;
1898
+ }
1899
+ parsed.push({
1900
+ key: change.key,
1901
+ deleted: false,
1902
+ etag: change.etag,
1903
+ metadata
1904
+ });
1905
+ }
1906
+ return {
1907
+ changes: parsed,
1908
+ more,
1909
+ cursor,
1910
+ source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
1911
+ authority: { notBefore, expiresAt, retainUntil }
1912
+ };
1913
+ }
1712
1914
  /**
1713
1915
  * Delete a key.
1714
1916
  */