@tinycloud/sdk-services 3.2.6-beta.4 → 3.3.0-beta.25

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.
Files changed (45) hide show
  1. package/dist/{BaseService-1Ug6y7DC.d.cts → BaseService-BhfKzKke.d.cts} +5 -1
  2. package/dist/{BaseService-Bn07Vudy.d.ts → BaseService-x4AFZiEa.d.ts} +5 -1
  3. package/dist/encryption/index.cjs +25 -4
  4. package/dist/encryption/index.cjs.map +1 -1
  5. package/dist/encryption/index.d.cts +2 -2
  6. package/dist/encryption/index.d.ts +2 -2
  7. package/dist/encryption/index.js +25 -4
  8. package/dist/encryption/index.js.map +1 -1
  9. package/dist/index.cjs +959 -808
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +114 -7
  12. package/dist/index.d.ts +114 -7
  13. package/dist/index.js +958 -808
  14. package/dist/index.js.map +1 -1
  15. package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
  16. package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
  17. package/dist/kv/index.cjs +136 -99
  18. package/dist/kv/index.cjs.map +1 -1
  19. package/dist/kv/index.d.cts +4 -5
  20. package/dist/kv/index.d.ts +4 -5
  21. package/dist/kv/index.js +136 -99
  22. package/dist/kv/index.js.map +1 -1
  23. package/dist/kv/replication/grantPolicy.cjs +82 -0
  24. package/dist/kv/replication/grantPolicy.cjs.map +1 -0
  25. package/dist/kv/replication/grantPolicy.d.cts +19 -0
  26. package/dist/kv/replication/grantPolicy.d.ts +19 -0
  27. package/dist/kv/replication/grantPolicy.js +50 -0
  28. package/dist/kv/replication/grantPolicy.js.map +1 -0
  29. package/dist/kv/replication/index.cjs +576 -132
  30. package/dist/kv/replication/index.cjs.map +1 -1
  31. package/dist/kv/replication/index.d.cts +3 -3
  32. package/dist/kv/replication/index.d.ts +3 -3
  33. package/dist/kv/replication/index.js +576 -132
  34. package/dist/kv/replication/index.js.map +1 -1
  35. package/dist/{scope-BnyVPZ5E.d.ts → scope-9U4gVNFN.d.ts} +312 -7
  36. package/dist/{scope-CQ5kmHlt.d.cts → scope-BzdcrOZG.d.cts} +312 -7
  37. package/dist/sql/index.cjs +98 -50
  38. package/dist/sql/index.cjs.map +1 -1
  39. package/dist/sql/index.d.cts +2 -2
  40. package/dist/sql/index.d.ts +2 -2
  41. package/dist/sql/index.js +98 -50
  42. package/dist/sql/index.js.map +1 -1
  43. package/dist/{types-gmdUiorz.d.cts → types-eVS1Whuf.d.cts} +29 -3
  44. package/dist/{types-gmdUiorz.d.ts → types-eVS1Whuf.d.ts} +29 -3
  45. package/package.json +16 -2
package/dist/index.cjs CHANGED
@@ -84,8 +84,8 @@ __export(src_exports, {
84
84
  authRequiredError: () => authRequiredError,
85
85
  authUnauthorizedError: () => authUnauthorizedError,
86
86
  authorizationVerdictOf: () => authorizationVerdictOf,
87
- base64Decode: () => base64Decode2,
88
- base64Encode: () => base64Encode2,
87
+ base64Decode: () => base64Decode,
88
+ base64Encode: () => base64Encode,
89
89
  buildCanonicalDecryptRequest: () => buildCanonicalDecryptRequest,
90
90
  buildDecryptAttenuation: () => buildDecryptAttenuation,
91
91
  buildDecryptFacts: () => buildDecryptFacts,
@@ -115,13 +115,14 @@ __export(src_exports, {
115
115
  generateRandomReceiverKey: () => generateRandomReceiverKey,
116
116
  getTinyCloudDebugLogs: () => getTinyCloudDebugLogs,
117
117
  hexDecode: () => hexDecode,
118
- hexEncode: () => hexEncode2,
118
+ hexEncode: () => hexEncode,
119
119
  installTinyCloudDebugGlobals: () => installTinyCloudDebugGlobals,
120
120
  isNetworkId: () => isNetworkId,
121
121
  isStorageFullError: () => isStorageFullError,
122
122
  kvPrefixCovers: () => kvPrefixCovers,
123
123
  networkDiscoveryKey: () => networkDiscoveryKey,
124
124
  networkError: () => networkError,
125
+ nodeRetryError: () => nodeRetryError,
125
126
  notFoundError: () => notFoundError,
126
127
  ok: () => ok,
127
128
  openWrappedKey: () => openWrappedKey,
@@ -179,6 +180,14 @@ var ErrorCodes = {
179
180
  ABORTED: "ABORTED",
180
181
  INVALID_INPUT: "INVALID_INPUT",
181
182
  PERMISSION_DENIED: "PERMISSION_DENIED",
183
+ // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
184
+ // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
185
+ /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
186
+ NODE_BUSY: "NODE_BUSY",
187
+ /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
188
+ REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
189
+ /** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
190
+ OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
182
191
  // KV-specific errors
183
192
  KV_NOT_FOUND: "KV_NOT_FOUND",
184
193
  KV_WRITE_FAILED: "KV_WRITE_FAILED",
@@ -214,11 +223,12 @@ var ErrorCodes = {
214
223
  DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
215
224
  };
216
225
  var defaultRetryPolicy = {
217
- maxAttempts: 3,
226
+ maxAttempts: 4,
218
227
  backoff: "exponential",
219
- baseDelayMs: 1e3,
220
- maxDelayMs: 1e4,
221
- retryableErrors: [ErrorCodes.NETWORK_ERROR, ErrorCodes.TIMEOUT]
228
+ baseDelayMs: 250,
229
+ maxDelayMs: 4e3,
230
+ retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
231
+ maxTotalMs: 12e3
222
232
  };
223
233
  var TelemetryEvents = {
224
234
  SPAN: "telemetry.span",
@@ -645,7 +655,9 @@ var RetryPolicySchema = import_zod.z.object({
645
655
  /** Maximum delay in milliseconds between retries */
646
656
  maxDelayMs: import_zod.z.number().nonnegative(),
647
657
  /** Error codes that should trigger a retry */
648
- retryableErrors: import_zod.z.array(import_zod.z.string())
658
+ retryableErrors: import_zod.z.array(import_zod.z.string()),
659
+ /** Total time budget for all attempts and waits, in milliseconds */
660
+ maxTotalMs: import_zod.z.number().positive().optional()
649
661
  });
650
662
  var ServiceSessionSchema = import_zod.z.object({
651
663
  /** The delegation header containing the UCAN */
@@ -782,412 +794,107 @@ function validateTelemetrySpanEvent(data) {
782
794
  return { ok: true, data: result.data };
783
795
  }
784
796
 
785
- // src/context.ts
786
- var ServiceContext = class {
787
- constructor(config) {
788
- this._session = null;
789
- this._services = /* @__PURE__ */ new Map();
790
- this._eventHandlers = /* @__PURE__ */ new Map();
791
- this._abortController = new AbortController();
792
- this._retired = false;
793
- this._invoke = this.wrapInvoke(config.invoke);
794
- this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
795
- this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
796
- this._hosts = config.hosts;
797
- this._session = config.session ?? null;
798
- this._retryPolicy = {
799
- ...defaultRetryPolicy,
800
- ...config.retryPolicy
801
- };
802
- this._telemetryEnabled = typeof config.telemetry === "boolean" ? config.telemetry : config.telemetry?.enabled === true;
803
- this._telemetryHandler = typeof config.telemetry === "object" ? config.telemetry.onEvent : void 0;
804
- }
805
- // ============================================================
806
- // Session Management
807
- // ============================================================
808
- /**
809
- * Get the current session.
810
- */
811
- get session() {
812
- return this._retired ? null : this._session;
797
+ // src/responseErrors.ts
798
+ function parseServiceErrorBody(errorText) {
799
+ try {
800
+ return JSON.parse(errorText);
801
+ } catch {
802
+ return {};
813
803
  }
814
- /**
815
- * Check if the context has an authenticated session.
816
- */
817
- get isAuthenticated() {
818
- return !this._retired && this._session !== null;
804
+ }
805
+ function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
806
+ if (parsed.message) {
807
+ return compactMessage(parsed.message);
819
808
  }
820
- /**
821
- * Update the session and notify all registered services.
822
- *
823
- * @param session - New session or null to clear
824
- */
825
- setSession(session) {
826
- this.assertActive();
827
- this._session = session;
828
- this.emit("session.changed", { authenticated: session !== null });
829
- for (const service of this._services.values()) {
830
- service.onSessionChange(session);
809
+ if (looksLikeHtml(errorText)) {
810
+ if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
811
+ return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
831
812
  }
813
+ return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
832
814
  }
833
- // ============================================================
834
- // Platform Dependencies
835
- // ============================================================
836
- /**
837
- * Get the invoke function for WASM operations.
838
- */
839
- get invoke() {
840
- this.assertActive();
841
- return this._invoke;
842
- }
843
- /**
844
- * Get the multi-resource invoke function when available.
845
- */
846
- get invokeAny() {
847
- this.assertActive();
848
- return this._invokeAny;
815
+ return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
816
+ }
817
+ function responseErrorMeta(status, statusText, errorText) {
818
+ const meta = { status, statusText };
819
+ const snippet = compactMessage(errorText);
820
+ if (snippet) {
821
+ meta.responseSnippet = snippet.slice(0, 300);
849
822
  }
850
- /**
851
- * Get the fetch function for HTTP requests.
852
- */
853
- get fetch() {
854
- this.assertActive();
855
- return this._fetch;
823
+ return meta;
824
+ }
825
+ function looksLikeHtml(text) {
826
+ return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
827
+ }
828
+ function compactMessage(message) {
829
+ const compact = message.replace(/\s+/g, " ").trim();
830
+ return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
831
+ }
832
+
833
+ // src/errors.ts
834
+ var import_bootstrap = require("@tinycloud/bootstrap");
835
+
836
+ // src/encryption/networkId.ts
837
+ var URN_PREFIX = "urn:tinycloud:encryption:";
838
+ var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
839
+ var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
840
+ var NetworkIdError = class extends Error {
841
+ constructor(message) {
842
+ super(message);
843
+ this.name = "NetworkIdError";
856
844
  }
857
- /**
858
- * Get the list of TinyCloud host URLs.
859
- */
860
- get hosts() {
861
- this.assertActive();
862
- return this._hosts;
845
+ };
846
+ function parseNetworkId(networkId) {
847
+ if (typeof networkId !== "string" || networkId.length === 0) {
848
+ throw new NetworkIdError("networkId must be a non-empty string");
863
849
  }
864
- // ============================================================
865
- // Service Registry
866
- // ============================================================
867
- /**
868
- * Register a service with the context.
869
- *
870
- * @param name - Service name (e.g., 'kv')
871
- * @param service - Service instance
872
- */
873
- registerService(name, service) {
874
- this._services.set(name, service);
850
+ if (!networkId.startsWith(URN_PREFIX)) {
851
+ throw new NetworkIdError(
852
+ `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
853
+ );
875
854
  }
876
- /**
877
- * Unregister a service from the context.
878
- *
879
- * @param name - Service name to remove
880
- */
881
- unregisterService(name) {
882
- this._services.delete(name);
855
+ const body = networkId.slice(URN_PREFIX.length);
856
+ const lastColon = body.lastIndexOf(":");
857
+ if (lastColon <= 0 || lastColon === body.length - 1) {
858
+ throw new NetworkIdError(
859
+ `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
860
+ );
883
861
  }
884
- /**
885
- * Get a registered service by name.
886
- *
887
- * @param name - Service name
888
- * @returns The service instance or undefined if not registered
889
- */
890
- getService(name) {
891
- return this._services.get(name);
862
+ const ownerDid = body.slice(0, lastColon);
863
+ const name = body.slice(lastColon + 1);
864
+ if (!ownerDid.startsWith("did:")) {
865
+ throw new NetworkIdError(
866
+ `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
867
+ );
892
868
  }
893
- // ============================================================
894
- // Event System (Telemetry)
895
- // ============================================================
896
- /**
897
- * Emit a telemetry event.
898
- *
899
- * @param event - Event name
900
- * @param data - Event data
901
- */
902
- emit(event, data) {
903
- tinyCloudDebugLogger.log(event, data);
904
- if (this._telemetryEnabled && this._telemetryHandler) {
905
- try {
906
- this._telemetryHandler(event, projectDiagnosticData(data));
907
- } catch (error) {
908
- console.error(`Error in telemetry handler for "${event}":`, error);
909
- }
910
- }
911
- const handlers = this._eventHandlers.get(event);
912
- if (handlers) {
913
- for (const handler of handlers) {
914
- try {
915
- handler(data);
916
- } catch (error) {
917
- console.error(`Error in event handler for "${event}":`, error);
918
- }
919
- }
920
- }
869
+ const didParts = ownerDid.split(":");
870
+ if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
871
+ throw new NetworkIdError(
872
+ `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
873
+ );
921
874
  }
922
- /**
923
- * Subscribe to telemetry events.
924
- *
925
- * @param event - Event name to subscribe to
926
- * @param handler - Handler function
927
- * @returns Unsubscribe function
928
- */
929
- on(event, handler) {
930
- if (!this._eventHandlers.has(event)) {
931
- this._eventHandlers.set(event, /* @__PURE__ */ new Set());
932
- }
933
- this._eventHandlers.get(event).add(handler);
934
- return () => {
935
- const handlers = this._eventHandlers.get(event);
936
- if (handlers) {
937
- handlers.delete(handler);
938
- if (handlers.size === 0) {
939
- this._eventHandlers.delete(event);
940
- }
941
- }
942
- };
875
+ if (!NETWORK_NAME_RE.test(name)) {
876
+ throw new NetworkIdError(
877
+ `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
878
+ );
943
879
  }
944
- /**
945
- * Remove all event handlers for an event.
946
- *
947
- * @param event - Event name (if omitted, clears all events)
948
- */
949
- clearEventHandlers(event) {
950
- if (event) {
951
- this._eventHandlers.delete(event);
952
- } else {
953
- this._eventHandlers.clear();
954
- }
880
+ return { networkId, ownerDid, name };
881
+ }
882
+ function buildNetworkId(ownerDid, name) {
883
+ if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
884
+ throw new NetworkIdError("ownerDid must be a DID");
955
885
  }
956
- // ============================================================
957
- // Lifecycle
958
- // ============================================================
959
- /**
960
- * Get the abort signal for cancelling operations.
961
- */
962
- get abortSignal() {
963
- return this._abortController.signal;
886
+ if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
887
+ throw new NetworkIdError(
888
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
889
+ );
964
890
  }
965
- get operationAbortSignal() {
966
- return this._operationAbortSignal;
967
- }
968
- setOperationAbortSignal(signal) {
969
- this._operationAbortSignal = signal;
970
- }
971
- /**
972
- * Permanently retire this graph after its owner installs a replacement.
973
- * Unlike `abort()`, retirement never creates a fresh controller, so captured
974
- * services cannot resume calls under a superseded session.
975
- */
976
- retire() {
977
- if (this._retired) return;
978
- this._retired = true;
979
- this._session = null;
980
- this._abortController.abort();
981
- for (const service of this._services.values()) {
982
- try {
983
- service.onSignOut();
984
- } catch (error) {
985
- console.error("Error retiring service:", error);
986
- }
987
- }
988
- }
989
- /**
990
- * Abort all pending operations and notify services.
991
- * Creates a new AbortController for future operations.
992
- */
993
- abort() {
994
- if (this._retired) return;
995
- this._abortController.abort();
996
- this._abortController = new AbortController();
997
- for (const service of this._services.values()) {
998
- try {
999
- service.onSignOut();
1000
- } catch (error) {
1001
- console.error("Error signing out service:", error);
1002
- }
1003
- }
1004
- }
1005
- /**
1006
- * Sign out - abort operations and clear session.
1007
- */
1008
- signOut() {
1009
- if (this._retired) return;
1010
- this.abort();
1011
- this.setSession(null);
1012
- this.emit("session.expired", {});
1013
- }
1014
- // ============================================================
1015
- // Retry Policy
1016
- // ============================================================
1017
- /**
1018
- * Get the retry policy configuration.
1019
- */
1020
- get retryPolicy() {
1021
- return this._retryPolicy;
1022
- }
1023
- wrapInvoke(invoke) {
1024
- return (session, service, path, action, facts) => {
1025
- this.assertActive();
1026
- if (!tinyCloudDebugLogger.isEnabled()) {
1027
- return invoke(session, service, path, action, facts);
1028
- }
1029
- const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
1030
- service,
1031
- path,
1032
- action,
1033
- facts
1034
- });
1035
- try {
1036
- const result = invoke(session, service, path, action, facts);
1037
- timer.stop({ ok: true, service, path, action });
1038
- return result;
1039
- } catch (error) {
1040
- timer.stop({ ok: false, service, path, action, error });
1041
- throw error;
1042
- }
1043
- };
1044
- }
1045
- assertActive() {
1046
- if (this._retired) {
1047
- throw new Error("Service graph has been retired: its session was replaced or signed out.");
1048
- }
1049
- }
1050
- wrapInvokeAny(invokeAny) {
1051
- return (session, entries, facts) => {
1052
- this.assertActive();
1053
- if (!tinyCloudDebugLogger.isEnabled()) {
1054
- return invokeAny(session, entries, facts);
1055
- }
1056
- const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
1057
- entries,
1058
- facts
1059
- });
1060
- try {
1061
- const result = invokeAny(session, entries, facts);
1062
- timer.stop({ ok: true, entries });
1063
- return result;
1064
- } catch (error) {
1065
- timer.stop({ ok: false, entries, error });
1066
- throw error;
1067
- }
1068
- };
1069
- }
1070
- wrapFetch(fetchFn) {
1071
- return async (url, init) => {
1072
- this.assertActive();
1073
- const request = {
1074
- ...init,
1075
- signal: this.combineSignals(init?.signal)
1076
- };
1077
- if (!tinyCloudDebugLogger.isEnabled()) {
1078
- return fetchFn(url, request);
1079
- }
1080
- const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", {
1081
- url,
1082
- method: request.method ?? "GET",
1083
- init: request
1084
- });
1085
- try {
1086
- const response = await fetchFn(url, request);
1087
- timer.stop({
1088
- ok: response.ok,
1089
- url,
1090
- method: request.method ?? "GET",
1091
- status: response.status,
1092
- statusText: response.statusText
1093
- });
1094
- return response;
1095
- } catch (error) {
1096
- timer.stop({
1097
- ok: false,
1098
- url,
1099
- method: request.method ?? "GET",
1100
- error
1101
- });
1102
- throw error;
1103
- }
1104
- };
1105
- }
1106
- combineSignals(signal) {
1107
- if (!signal) return this._abortController.signal;
1108
- const combined = new AbortController();
1109
- const abort = (source) => combined.abort(source.reason);
1110
- if (signal.aborted) {
1111
- abort(signal);
1112
- } else if (this._abortController.signal.aborted) {
1113
- abort(this._abortController.signal);
1114
- } else {
1115
- signal.addEventListener("abort", () => abort(signal), { once: true });
1116
- this._abortController.signal.addEventListener(
1117
- "abort",
1118
- () => abort(this._abortController.signal),
1119
- { once: true }
1120
- );
1121
- }
1122
- return combined.signal;
1123
- }
1124
- };
1125
-
1126
- // src/errors.ts
1127
- var import_bootstrap = require("@tinycloud/bootstrap");
1128
-
1129
- // src/encryption/networkId.ts
1130
- var URN_PREFIX = "urn:tinycloud:encryption:";
1131
- var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
1132
- var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
1133
- var NetworkIdError = class extends Error {
1134
- constructor(message) {
1135
- super(message);
1136
- this.name = "NetworkIdError";
1137
- }
1138
- };
1139
- function parseNetworkId(networkId) {
1140
- if (typeof networkId !== "string" || networkId.length === 0) {
1141
- throw new NetworkIdError("networkId must be a non-empty string");
1142
- }
1143
- if (!networkId.startsWith(URN_PREFIX)) {
1144
- throw new NetworkIdError(
1145
- `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
1146
- );
1147
- }
1148
- const body = networkId.slice(URN_PREFIX.length);
1149
- const lastColon = body.lastIndexOf(":");
1150
- if (lastColon <= 0 || lastColon === body.length - 1) {
1151
- throw new NetworkIdError(
1152
- `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
1153
- );
1154
- }
1155
- const ownerDid = body.slice(0, lastColon);
1156
- const name = body.slice(lastColon + 1);
1157
- if (!ownerDid.startsWith("did:")) {
1158
- throw new NetworkIdError(
1159
- `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
1160
- );
1161
- }
1162
- const didParts = ownerDid.split(":");
1163
- if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
1164
- throw new NetworkIdError(
1165
- `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
1166
- );
1167
- }
1168
- if (!NETWORK_NAME_RE.test(name)) {
1169
- throw new NetworkIdError(
1170
- `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1171
- );
1172
- }
1173
- return { networkId, ownerDid, name };
1174
- }
1175
- function buildNetworkId(ownerDid, name) {
1176
- if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
1177
- throw new NetworkIdError("ownerDid must be a DID");
1178
- }
1179
- if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
1180
- throw new NetworkIdError(
1181
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1182
- );
1183
- }
1184
- const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
1185
- parseNetworkId(networkId);
1186
- return networkId;
1187
- }
1188
- function isNetworkId(networkId) {
1189
- if (typeof networkId !== "string") {
1190
- return false;
891
+ const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
892
+ parseNetworkId(networkId);
893
+ return networkId;
894
+ }
895
+ function isNetworkId(networkId) {
896
+ if (typeof networkId !== "string") {
897
+ return false;
1191
898
  }
1192
899
  try {
1193
900
  parseNetworkId(networkId);
@@ -1282,6 +989,30 @@ function networkError(service, message, cause) {
1282
989
  cause
1283
990
  };
1284
991
  }
992
+ function nodeRetryError(service, message, response, errorText) {
993
+ const body = errorText.trim();
994
+ const [, token, stage] = /^([a-z_]+)(?::\s*([a-z_]+):)?/.exec(body) ?? [];
995
+ let code;
996
+ if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable" || token === "read_audit_busy")) {
997
+ code = ErrorCodes.NODE_BUSY;
998
+ } else if (response.status === 503 && token === "request_timeout") {
999
+ code = ErrorCodes.REQUEST_TIMEOUT;
1000
+ } else if (response.status === 500 && token === "outcome_unknown") {
1001
+ code = ErrorCodes.OUTCOME_UNKNOWN;
1002
+ } else {
1003
+ return void 0;
1004
+ }
1005
+ const retryable = code !== ErrorCodes.OUTCOME_UNKNOWN;
1006
+ const retryAfter = response.headers.get("retry-after")?.trim();
1007
+ return serviceError(code, message, service, {
1008
+ meta: {
1009
+ ...responseErrorMeta(response.status, response.statusText, errorText),
1010
+ retryable,
1011
+ ...token === "sql_busy" && stage ? { stage } : {},
1012
+ ...retryable && retryAfter && /^\d{1,9}$/.test(retryAfter) ? { retryAfterMs: Number(retryAfter) * 1e3 } : {}
1013
+ }
1014
+ });
1015
+ }
1285
1016
  var RequestTimeoutError = class extends Error {
1286
1017
  constructor(timeoutMs) {
1287
1018
  super(`Request timed out after ${timeoutMs}ms.`);
@@ -1521,15 +1252,401 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1521
1252
  cause: error
1522
1253
  };
1523
1254
  }
1524
- return {
1525
- code: defaultCode,
1526
- message: String(error),
1527
- service
1528
- };
1529
- }
1530
- function errorResult(error) {
1531
- return err(error);
1532
- }
1255
+ return {
1256
+ code: defaultCode,
1257
+ message: String(error),
1258
+ service
1259
+ };
1260
+ }
1261
+ function errorResult(error) {
1262
+ return err(error);
1263
+ }
1264
+
1265
+ // src/sleep.ts
1266
+ var abortableSleep = (ms, signal, unref = false) => new Promise((resolve) => {
1267
+ const done = () => {
1268
+ clearTimeout(timer);
1269
+ signal.removeEventListener("abort", done);
1270
+ resolve();
1271
+ };
1272
+ const timer = setTimeout(done, ms);
1273
+ if (unref) timer.unref?.();
1274
+ signal.aborted ? done() : signal.addEventListener("abort", done);
1275
+ });
1276
+
1277
+ // src/context.ts
1278
+ var ServiceContext = class {
1279
+ constructor(config) {
1280
+ this._session = null;
1281
+ this._services = /* @__PURE__ */ new Map();
1282
+ this._eventHandlers = /* @__PURE__ */ new Map();
1283
+ this._abortController = new AbortController();
1284
+ this._retired = false;
1285
+ this._invoke = this.wrapInvoke(config.invoke);
1286
+ this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
1287
+ this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
1288
+ this._hosts = config.hosts;
1289
+ this._session = config.session ?? null;
1290
+ this._retryPolicy = {
1291
+ ...defaultRetryPolicy,
1292
+ ...config.retryPolicy
1293
+ };
1294
+ this._telemetryEnabled = typeof config.telemetry === "boolean" ? config.telemetry : config.telemetry?.enabled === true;
1295
+ this._telemetryHandler = typeof config.telemetry === "object" ? config.telemetry.onEvent : void 0;
1296
+ }
1297
+ // ============================================================
1298
+ // Session Management
1299
+ // ============================================================
1300
+ /**
1301
+ * Get the current session.
1302
+ */
1303
+ get session() {
1304
+ return this._retired ? null : this._session;
1305
+ }
1306
+ /**
1307
+ * Check if the context has an authenticated session.
1308
+ */
1309
+ get isAuthenticated() {
1310
+ return !this._retired && this._session !== null;
1311
+ }
1312
+ /**
1313
+ * Update the session and notify all registered services.
1314
+ *
1315
+ * @param session - New session or null to clear
1316
+ */
1317
+ setSession(session) {
1318
+ this.assertActive();
1319
+ this._session = session;
1320
+ this.emit("session.changed", { authenticated: session !== null });
1321
+ for (const service of this._services.values()) {
1322
+ service.onSessionChange(session);
1323
+ }
1324
+ }
1325
+ // ============================================================
1326
+ // Platform Dependencies
1327
+ // ============================================================
1328
+ /**
1329
+ * Get the invoke function for WASM operations.
1330
+ */
1331
+ get invoke() {
1332
+ this.assertActive();
1333
+ return this._invoke;
1334
+ }
1335
+ /**
1336
+ * Get the multi-resource invoke function when available.
1337
+ */
1338
+ get invokeAny() {
1339
+ this.assertActive();
1340
+ return this._invokeAny;
1341
+ }
1342
+ /**
1343
+ * Get the fetch function for HTTP requests.
1344
+ */
1345
+ get fetch() {
1346
+ this.assertActive();
1347
+ return this._fetch;
1348
+ }
1349
+ /**
1350
+ * Get the list of TinyCloud host URLs.
1351
+ */
1352
+ get hosts() {
1353
+ this.assertActive();
1354
+ return this._hosts;
1355
+ }
1356
+ // ============================================================
1357
+ // Service Registry
1358
+ // ============================================================
1359
+ /**
1360
+ * Register a service with the context.
1361
+ *
1362
+ * @param name - Service name (e.g., 'kv')
1363
+ * @param service - Service instance
1364
+ */
1365
+ registerService(name, service) {
1366
+ this._services.set(name, service);
1367
+ }
1368
+ /**
1369
+ * Unregister a service from the context.
1370
+ *
1371
+ * @param name - Service name to remove
1372
+ */
1373
+ unregisterService(name) {
1374
+ this._services.delete(name);
1375
+ }
1376
+ /**
1377
+ * Get a registered service by name.
1378
+ *
1379
+ * @param name - Service name
1380
+ * @returns The service instance or undefined if not registered
1381
+ */
1382
+ getService(name) {
1383
+ return this._services.get(name);
1384
+ }
1385
+ // ============================================================
1386
+ // Event System (Telemetry)
1387
+ // ============================================================
1388
+ /**
1389
+ * Emit a telemetry event.
1390
+ *
1391
+ * @param event - Event name
1392
+ * @param data - Event data
1393
+ */
1394
+ emit(event, data) {
1395
+ tinyCloudDebugLogger.log(event, data);
1396
+ if (this._telemetryEnabled && this._telemetryHandler) {
1397
+ try {
1398
+ this._telemetryHandler(event, projectDiagnosticData(data));
1399
+ } catch (error) {
1400
+ console.error(`Error in telemetry handler for "${event}":`, error);
1401
+ }
1402
+ }
1403
+ const handlers = this._eventHandlers.get(event);
1404
+ if (handlers) {
1405
+ for (const handler of handlers) {
1406
+ try {
1407
+ handler(data);
1408
+ } catch (error) {
1409
+ console.error(`Error in event handler for "${event}":`, error);
1410
+ }
1411
+ }
1412
+ }
1413
+ }
1414
+ /**
1415
+ * Subscribe to telemetry events.
1416
+ *
1417
+ * @param event - Event name to subscribe to
1418
+ * @param handler - Handler function
1419
+ * @returns Unsubscribe function
1420
+ */
1421
+ on(event, handler) {
1422
+ if (!this._eventHandlers.has(event)) {
1423
+ this._eventHandlers.set(event, /* @__PURE__ */ new Set());
1424
+ }
1425
+ this._eventHandlers.get(event).add(handler);
1426
+ return () => {
1427
+ const handlers = this._eventHandlers.get(event);
1428
+ if (handlers) {
1429
+ handlers.delete(handler);
1430
+ if (handlers.size === 0) {
1431
+ this._eventHandlers.delete(event);
1432
+ }
1433
+ }
1434
+ };
1435
+ }
1436
+ /**
1437
+ * Remove all event handlers for an event.
1438
+ *
1439
+ * @param event - Event name (if omitted, clears all events)
1440
+ */
1441
+ clearEventHandlers(event) {
1442
+ if (event) {
1443
+ this._eventHandlers.delete(event);
1444
+ } else {
1445
+ this._eventHandlers.clear();
1446
+ }
1447
+ }
1448
+ // ============================================================
1449
+ // Lifecycle
1450
+ // ============================================================
1451
+ /**
1452
+ * Get the abort signal for cancelling operations.
1453
+ */
1454
+ get abortSignal() {
1455
+ return this._abortController.signal;
1456
+ }
1457
+ get operationAbortSignal() {
1458
+ return this._operationAbortSignal;
1459
+ }
1460
+ setOperationAbortSignal(signal) {
1461
+ this._operationAbortSignal = signal;
1462
+ }
1463
+ /**
1464
+ * Permanently retire this graph after its owner installs a replacement.
1465
+ * Unlike `abort()`, retirement never creates a fresh controller, so captured
1466
+ * services cannot resume calls under a superseded session.
1467
+ */
1468
+ retire() {
1469
+ if (this._retired) return;
1470
+ this._retired = true;
1471
+ this._session = null;
1472
+ this._abortController.abort();
1473
+ for (const service of this._services.values()) {
1474
+ try {
1475
+ service.onSignOut();
1476
+ } catch (error) {
1477
+ console.error("Error retiring service:", error);
1478
+ }
1479
+ }
1480
+ }
1481
+ /**
1482
+ * Abort all pending operations and notify services.
1483
+ * Creates a new AbortController for future operations.
1484
+ */
1485
+ abort() {
1486
+ if (this._retired) return;
1487
+ this._abortController.abort();
1488
+ this._abortController = new AbortController();
1489
+ for (const service of this._services.values()) {
1490
+ try {
1491
+ service.onSignOut();
1492
+ } catch (error) {
1493
+ console.error("Error signing out service:", error);
1494
+ }
1495
+ }
1496
+ }
1497
+ /**
1498
+ * Sign out - abort operations and clear session.
1499
+ */
1500
+ signOut() {
1501
+ if (this._retired) return;
1502
+ this.abort();
1503
+ this.setSession(null);
1504
+ this.emit("session.expired", {});
1505
+ }
1506
+ // ============================================================
1507
+ // Retry Policy
1508
+ // ============================================================
1509
+ /**
1510
+ * Get the retry policy configuration.
1511
+ */
1512
+ get retryPolicy() {
1513
+ return this._retryPolicy;
1514
+ }
1515
+ wrapInvoke(invoke) {
1516
+ return (session, service, path, action, facts) => {
1517
+ this.assertActive();
1518
+ if (!tinyCloudDebugLogger.isEnabled()) {
1519
+ return invoke(session, service, path, action, facts);
1520
+ }
1521
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
1522
+ service,
1523
+ path,
1524
+ action,
1525
+ facts
1526
+ });
1527
+ try {
1528
+ const result = invoke(session, service, path, action, facts);
1529
+ timer.stop({ ok: true, service, path, action });
1530
+ return result;
1531
+ } catch (error) {
1532
+ timer.stop({ ok: false, service, path, action, error });
1533
+ throw error;
1534
+ }
1535
+ };
1536
+ }
1537
+ assertActive() {
1538
+ if (this._retired) {
1539
+ throw new Error("Service graph has been retired: its session was replaced or signed out.");
1540
+ }
1541
+ }
1542
+ wrapInvokeAny(invokeAny) {
1543
+ return (session, entries, facts) => {
1544
+ this.assertActive();
1545
+ if (!tinyCloudDebugLogger.isEnabled()) {
1546
+ return invokeAny(session, entries, facts);
1547
+ }
1548
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
1549
+ entries,
1550
+ facts
1551
+ });
1552
+ try {
1553
+ const result = invokeAny(session, entries, facts);
1554
+ timer.stop({ ok: true, entries });
1555
+ return result;
1556
+ } catch (error) {
1557
+ timer.stop({ ok: false, entries, error });
1558
+ throw error;
1559
+ }
1560
+ };
1561
+ }
1562
+ /** Retry `NODE_BUSY` / `REQUEST_TIMEOUT`; `build` must re-sign per attempt (the node 409s a repeated invocation). */
1563
+ async invokeWithRetry(url, build) {
1564
+ this.assertActive();
1565
+ const policy = this._retryPolicy;
1566
+ const startedAt = Date.now();
1567
+ let backoffMs = 0;
1568
+ let signal;
1569
+ for (let attempt = 1; ; attempt++) {
1570
+ const init = await build();
1571
+ signal ?? (signal = this.combineSignals(init.signal));
1572
+ const response = await this._fetch(url, init);
1573
+ const done = () => {
1574
+ try {
1575
+ response.retry = { attempts: attempt, backoffMs };
1576
+ } catch {
1577
+ }
1578
+ return response;
1579
+ };
1580
+ if (attempt >= policy.maxAttempts || response.status !== 503 || !response.clone) return done();
1581
+ const text = await response.clone().text().catch(() => "");
1582
+ const error = nodeRetryError("", "", response, text);
1583
+ if (!error || error.meta?.retryable !== true || !policy.retryableErrors.includes(error.code)) {
1584
+ return done();
1585
+ }
1586
+ const remaining = (policy.maxTotalMs ?? Infinity) - (Date.now() - startedAt);
1587
+ if (remaining <= 0) return done();
1588
+ const retryAfterMs = error.meta.retryAfterMs;
1589
+ const step = Math.min(
1590
+ policy.maxDelayMs,
1591
+ policy.backoff === "exponential" ? policy.baseDelayMs * 2 ** (attempt - 1) : policy.backoff === "linear" ? policy.baseDelayMs * attempt : 0
1592
+ );
1593
+ const delay = Math.min(
1594
+ typeof retryAfterMs === "number" ? retryAfterMs : Math.random() * step,
1595
+ remaining
1596
+ );
1597
+ response.body?.cancel?.()?.catch?.(() => {
1598
+ });
1599
+ await abortableSleep(delay, signal);
1600
+ if (signal.aborted) throw signal.reason;
1601
+ backoffMs += delay;
1602
+ }
1603
+ }
1604
+ wrapFetch(fetchFn) {
1605
+ return async (url, init) => {
1606
+ this.assertActive();
1607
+ const request = {
1608
+ ...init,
1609
+ signal: this.combineSignals(init?.signal)
1610
+ };
1611
+ if (!tinyCloudDebugLogger.isEnabled()) {
1612
+ return fetchFn(url, request);
1613
+ }
1614
+ const meta = { url, method: request.method ?? "GET" };
1615
+ const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", { ...meta, init: request });
1616
+ try {
1617
+ const response = await fetchFn(url, request);
1618
+ timer.stop({
1619
+ ...meta,
1620
+ ok: response.ok,
1621
+ status: response.status,
1622
+ statusText: response.statusText
1623
+ });
1624
+ return response;
1625
+ } catch (error) {
1626
+ timer.stop({ ...meta, ok: false, error });
1627
+ throw error;
1628
+ }
1629
+ };
1630
+ }
1631
+ combineSignals(signal) {
1632
+ if (!signal) return this._abortController.signal;
1633
+ const combined = new AbortController();
1634
+ const abort = (source) => combined.abort(source.reason);
1635
+ if (signal.aborted) {
1636
+ abort(signal);
1637
+ } else if (this._abortController.signal.aborted) {
1638
+ abort(this._abortController.signal);
1639
+ } else {
1640
+ signal.addEventListener("abort", () => abort(signal), { once: true });
1641
+ this._abortController.signal.addEventListener(
1642
+ "abort",
1643
+ () => abort(this._abortController.signal),
1644
+ { once: true }
1645
+ );
1646
+ }
1647
+ return combined.signal;
1648
+ }
1649
+ };
1533
1650
 
1534
1651
  // src/base/BaseService.ts
1535
1652
  var MAX_TIMER_DELAY_MS = 2147483647;
@@ -1713,6 +1830,18 @@ var BaseService = class {
1713
1830
  }
1714
1831
  return controller.signal;
1715
1832
  }
1833
+ /** POST a signed invocation with retry; `mint` re-signs per attempt, `init.headers` add to the signed ones. */
1834
+ invokeWithRetry(url, mint, { headers: extra, ...init } = {}) {
1835
+ const build = () => {
1836
+ const signed = mint();
1837
+ return {
1838
+ method: "POST",
1839
+ ...init,
1840
+ headers: Array.isArray(signed) ? [...signed, ...Object.entries(extra ?? {})] : { ...signed, ...extra }
1841
+ };
1842
+ };
1843
+ return this.context.invokeWithRetry ? this.context.invokeWithRetry(url, build) : this.context.fetch(url, build());
1844
+ }
1716
1845
  /**
1717
1846
  * Create the abort signal for one request, bounded by the request timeout.
1718
1847
  *
@@ -2080,15 +2209,6 @@ var KVService = class extends BaseService {
2080
2209
  get host() {
2081
2210
  return this.context.hosts[0];
2082
2211
  }
2083
- withJsonContentType(headers) {
2084
- if (Array.isArray(headers)) {
2085
- return [...headers, ["content-type", "application/json"]];
2086
- }
2087
- return {
2088
- ...headers,
2089
- "content-type": "application/json"
2090
- };
2091
- }
2092
2212
  /**
2093
2213
  * Execute an invoke operation.
2094
2214
  *
@@ -2099,20 +2219,11 @@ var KVService = class extends BaseService {
2099
2219
  * @returns Fetch response
2100
2220
  */
2101
2221
  async invokeOperation(path, action, body, signal, extraHeaders) {
2102
- const session = this.context.session;
2103
- const headers = this.context.invoke(
2104
- session,
2105
- "kv",
2106
- path,
2107
- action
2222
+ return this.invokeWithRetry(
2223
+ `${this.host}/invoke`,
2224
+ () => this.context.invoke(this.context.session, "kv", path, action),
2225
+ { headers: extraHeaders, body, signal }
2108
2226
  );
2109
- const requestHeaders = Array.isArray(headers) ? [...headers, ...Object.entries(extraHeaders ?? {})] : { ...headers, ...extraHeaders };
2110
- return this.context.fetch(`${this.host}/invoke`, {
2111
- method: "POST",
2112
- headers: requestHeaders,
2113
- body,
2114
- signal
2115
- });
2116
2227
  }
2117
2228
  /**
2118
2229
  * Serialize a single put value into a fetch body.
@@ -2318,28 +2429,28 @@ var KVService = class extends BaseService {
2318
2429
  }
2319
2430
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2320
2431
  try {
2321
- const session = this.context.session;
2322
- const invocationHeaders = this.context.invokeAny(
2323
- session,
2324
- paths.map((path) => ({
2325
- spaceId: session.spaceId,
2326
- service: "kv",
2327
- path,
2328
- action
2329
- }))
2330
- );
2331
2432
  const limitHeaders = {};
2332
2433
  if (getOptions?.maxResponseBytes !== void 0) {
2333
2434
  limitHeaders["x-tinycloud-max-response-bytes"] = String(
2334
2435
  getOptions.maxResponseBytes
2335
2436
  );
2336
2437
  }
2337
- const headers = Array.isArray(invocationHeaders) ? [...invocationHeaders, ...Object.entries(limitHeaders)] : { ...invocationHeaders, ...limitHeaders };
2338
- const response = await this.context.fetch(`${this.host}/invoke`, {
2339
- method: "POST",
2340
- headers,
2341
- signal: request.signal
2342
- });
2438
+ const response = await this.invokeWithRetry(
2439
+ `${this.host}/invoke`,
2440
+ () => {
2441
+ const session = this.context.session;
2442
+ return this.context.invokeAny(
2443
+ session,
2444
+ paths.map((path) => ({
2445
+ spaceId: session.spaceId,
2446
+ service: "kv",
2447
+ path,
2448
+ action
2449
+ }))
2450
+ );
2451
+ },
2452
+ { headers: limitHeaders, signal: request.signal }
2453
+ );
2343
2454
  if (!response.ok) {
2344
2455
  const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
2345
2456
  if (response.status === 401 || response.status === 403) {
@@ -2385,10 +2496,10 @@ var KVService = class extends BaseService {
2385
2496
  result: err(serviceError(item.error.code, item.error.message, "kv"))
2386
2497
  };
2387
2498
  }
2388
- const headers2 = item.headers;
2499
+ const headers = item.headers;
2389
2500
  const data = action === KVAction.HEAD ? void 0 : this.parseBatchValue(
2390
2501
  item.dataBase64,
2391
- headers2,
2502
+ headers,
2392
2503
  getOptions?.raw,
2393
2504
  getOptions?.binary
2394
2505
  );
@@ -2396,7 +2507,7 @@ var KVService = class extends BaseService {
2396
2507
  key: keys[index],
2397
2508
  result: ok({
2398
2509
  data,
2399
- headers: this.createBatchResponseHeaders(headers2)
2510
+ headers: this.createBatchResponseHeaders(headers)
2400
2511
  })
2401
2512
  };
2402
2513
  });
@@ -2620,15 +2731,18 @@ var KVService = class extends BaseService {
2620
2731
  { meta: { status: response.status, statusText: response.statusText } }
2621
2732
  ));
2622
2733
  }
2734
+ const message = `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
2735
+ const retryError = nodeRetryError("kv", message, response, errorText);
2736
+ if (retryError) return err(retryError);
2623
2737
  return err(serviceError(
2624
2738
  ErrorCodes.NETWORK_ERROR,
2625
- `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2739
+ message,
2626
2740
  "kv",
2627
2741
  { meta: { status: response.status, statusText: response.statusText } }
2628
2742
  ));
2629
2743
  }
2630
2744
  const data = await this.parseResponse(response, options?.raw, options?.binary);
2631
- return ok({ data, headers: this.createResponseHeaders(response.headers) });
2745
+ return ok({ data, headers: this.createResponseHeaders(response.headers), ...response.retry && { retry: response.retry } });
2632
2746
  }
2633
2747
  async batchGet(keys, options) {
2634
2748
  return this.withTelemetry(
@@ -2704,26 +2818,22 @@ var KVService = class extends BaseService {
2704
2818
  { meta: { status: response.status, statusText: response.statusText } }
2705
2819
  ));
2706
2820
  }
2707
- if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
2708
- return err(serviceError(
2709
- ErrorCodes.KV_CONFLICT,
2710
- `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
2711
- "kv",
2712
- { meta: { status: response.status, statusText: response.statusText } }
2713
- ));
2714
- }
2821
+ const message = `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
2822
+ const retryError = nodeRetryError("kv", message, response, errorText);
2823
+ if (retryError) return err(retryError);
2715
2824
  const quotaError = this.handleQuotaErrorResponse(response, errorText, key);
2716
2825
  if (quotaError) return quotaError;
2717
2826
  return err(serviceError(
2718
2827
  ErrorCodes.KV_WRITE_FAILED,
2719
- `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2828
+ message,
2720
2829
  "kv",
2721
2830
  { meta: { status: response.status, statusText: response.statusText } }
2722
2831
  ));
2723
2832
  }
2724
2833
  return ok({
2725
2834
  data: void 0,
2726
- headers: this.createResponseHeaders(response.headers)
2835
+ headers: this.createResponseHeaders(response.headers),
2836
+ ...response.retry && { retry: response.retry }
2727
2837
  });
2728
2838
  }
2729
2839
  /**
@@ -2796,25 +2906,23 @@ var KVService = class extends BaseService {
2796
2906
  entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
2797
2907
  );
2798
2908
  }
2799
- const headers = this.context.invokeAny(
2800
- session,
2801
- paths.map((path) => ({
2802
- spaceId: session.spaceId,
2803
- service: "kv",
2804
- path,
2805
- action: KVAction.PUT
2806
- }))
2909
+ const response = await this.invokeWithRetry(
2910
+ `${this.host}/invoke`,
2911
+ () => {
2912
+ const headers = this.context.invokeAny(
2913
+ session,
2914
+ paths.map((path) => ({
2915
+ spaceId: session.spaceId,
2916
+ service: "kv",
2917
+ path,
2918
+ action: KVAction.PUT
2919
+ }))
2920
+ );
2921
+ requestMayHaveDispatched = true;
2922
+ return headers;
2923
+ },
2924
+ { body, signal }
2807
2925
  );
2808
- const url = `${this.host}/invoke`;
2809
- const init = {
2810
- method: "POST",
2811
- headers,
2812
- body,
2813
- signal
2814
- };
2815
- const fetchFn = this.context.fetch;
2816
- requestMayHaveDispatched = true;
2817
- const response = await fetchFn(url, init);
2818
2926
  if (!response.ok) {
2819
2927
  let errorText;
2820
2928
  try {
@@ -3016,9 +3124,12 @@ var KVService = class extends BaseService {
3016
3124
  );
3017
3125
  }
3018
3126
  const errorText = await response.text();
3127
+ const message = `Failed to list keys: ${response.status} - ${errorText}`;
3128
+ const retryError = nodeRetryError("kv", message, response, errorText);
3129
+ if (retryError) return err(retryError);
3019
3130
  return err(serviceError(
3020
3131
  ErrorCodes.NETWORK_ERROR,
3021
- `Failed to list keys: ${response.status} - ${errorText}`,
3132
+ message,
3022
3133
  "kv",
3023
3134
  { meta: { status: response.status, statusText: response.statusText } }
3024
3135
  ));
@@ -3269,24 +3380,20 @@ var KVService = class extends BaseService {
3269
3380
  { meta: { status: response.status, statusText: response.statusText } }
3270
3381
  ));
3271
3382
  }
3272
- if (response.status === 503 && options?.ifMatch !== void 0) {
3273
- return err(serviceError(
3274
- ErrorCodes.KV_CONFLICT,
3275
- `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
3276
- "kv",
3277
- { meta: { status: response.status, statusText: response.statusText } }
3278
- ));
3279
- }
3383
+ const message = `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
3384
+ const retryError = nodeRetryError("kv", message, response, errorText);
3385
+ if (retryError) return err(retryError);
3280
3386
  return err(serviceError(
3281
3387
  ErrorCodes.NETWORK_ERROR,
3282
- `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
3388
+ message,
3283
3389
  "kv",
3284
3390
  { meta: { status: response.status, statusText: response.statusText } }
3285
3391
  ));
3286
3392
  }
3287
3393
  return ok({
3288
3394
  data: void 0,
3289
- headers: this.createResponseHeaders(response.headers)
3395
+ headers: this.createResponseHeaders(response.headers),
3396
+ ...response.retry && { retry: response.retry }
3290
3397
  });
3291
3398
  }
3292
3399
  /**
@@ -3320,10 +3427,13 @@ var KVService = class extends BaseService {
3320
3427
  return await this.classifyNotFound(response, key);
3321
3428
  }
3322
3429
  const errorText = await response.text();
3430
+ const message = `Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
3431
+ const retryError = nodeRetryError("kv", message, response, errorText);
3432
+ if (retryError) return err(retryError);
3323
3433
  return err(
3324
3434
  serviceError(
3325
3435
  ErrorCodes.NETWORK_ERROR,
3326
- `Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
3436
+ message,
3327
3437
  "kv",
3328
3438
  { meta: { status: response.status, statusText: response.statusText } }
3329
3439
  )
@@ -3357,12 +3467,6 @@ var KVService = class extends BaseService {
3357
3467
  }
3358
3468
  const path = this.getFullPath(key, options?.prefix);
3359
3469
  const session = this.context.session;
3360
- const headers = this.context.invoke(
3361
- session,
3362
- "kv",
3363
- path,
3364
- KVAction.GET
3365
- );
3366
3470
  const body = {
3367
3471
  space: session.spaceId,
3368
3472
  path,
@@ -3376,12 +3480,15 @@ var KVService = class extends BaseService {
3376
3480
  }
3377
3481
  const request = this.createRequestSignal(options?.signal, options?.timeout);
3378
3482
  try {
3379
- const response = await this.context.fetch(`${this.host}/signed/kv`, {
3380
- method: "POST",
3381
- headers: this.withJsonContentType(headers),
3382
- body: JSON.stringify(body),
3383
- signal: request.signal
3384
- });
3483
+ const response = await this.invokeWithRetry(
3484
+ `${this.host}/signed/kv`,
3485
+ () => this.context.invoke(session, "kv", path, KVAction.GET),
3486
+ {
3487
+ headers: { "content-type": "application/json" },
3488
+ body: JSON.stringify(body),
3489
+ signal: request.signal
3490
+ }
3491
+ );
3385
3492
  if (!response.ok) {
3386
3493
  return await this.createSignedReadUrlError(response, key, path);
3387
3494
  }
@@ -3515,42 +3622,6 @@ function requiresSecretsOptIn(space, prefix) {
3515
3622
  return prefix.split("/", 1)[0] === "vault";
3516
3623
  }
3517
3624
 
3518
- // src/responseErrors.ts
3519
- function parseServiceErrorBody(errorText) {
3520
- try {
3521
- return JSON.parse(errorText);
3522
- } catch {
3523
- return {};
3524
- }
3525
- }
3526
- function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
3527
- if (parsed.message) {
3528
- return compactMessage(parsed.message);
3529
- }
3530
- if (looksLikeHtml(errorText)) {
3531
- if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
3532
- return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
3533
- }
3534
- return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
3535
- }
3536
- return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
3537
- }
3538
- function responseErrorMeta(status, statusText, errorText) {
3539
- const meta = { status, statusText };
3540
- const snippet = compactMessage(errorText);
3541
- if (snippet) {
3542
- meta.responseSnippet = snippet.slice(0, 300);
3543
- }
3544
- return meta;
3545
- }
3546
- function looksLikeHtml(text) {
3547
- return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
3548
- }
3549
- function compactMessage(message) {
3550
- const compact = message.replace(/\s+/g, " ").trim();
3551
- return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
3552
- }
3553
-
3554
3625
  // src/sql/SQLMigrations.ts
3555
3626
  var SQLMigrations = class {
3556
3627
  constructor(service, dbName) {
@@ -3914,18 +3985,19 @@ var SQLService = class extends BaseService {
3914
3985
  }
3915
3986
  // === Private helpers ===
3916
3987
  async invokeSQL(dbName, actions, body, signal) {
3917
- const session = this.context.session;
3918
3988
  const actionList = Array.isArray(actions) ? actions : [actions];
3919
- const headers = actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
3920
- return this.context.fetch(`${this.host}/invoke`, {
3921
- method: "POST",
3922
- headers: {
3923
- ...headers,
3924
- "Content-Type": "application/json"
3989
+ return this.invokeWithRetry(
3990
+ `${this.host}/invoke`,
3991
+ () => {
3992
+ const session = this.context.session;
3993
+ return actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
3925
3994
  },
3926
- body: JSON.stringify(body),
3927
- signal
3928
- });
3995
+ {
3996
+ headers: { "Content-Type": "application/json" },
3997
+ body: JSON.stringify(body),
3998
+ signal
3999
+ }
4000
+ );
3929
4001
  }
3930
4002
  actionForSql(sql, fallback) {
3931
4003
  const token = firstSqlToken(sql);
@@ -3966,6 +4038,8 @@ var SQLService = class extends BaseService {
3966
4038
  if (response.status === 413 && parseStorageRejection(errorText)) {
3967
4039
  return err(storageRejectionError("sql", 413, meta, errorText));
3968
4040
  }
4041
+ const retryError = nodeRetryError("sql", `SQL ${operation} failed: ${response.status} - ${errorText}`, response, errorText);
4042
+ if (retryError) return err(retryError);
3969
4043
  const errorBody = parseServiceErrorBody(errorText);
3970
4044
  const errorCode = this.mapHttpStatusToErrorCode(
3971
4045
  response.status,
@@ -4384,22 +4458,20 @@ var DuckDbService = class extends BaseService {
4384
4458
  }
4385
4459
  const request = this.createRequestSignal(options?.signal);
4386
4460
  try {
4387
- const session = this.context.session;
4388
- const headers = this.context.invoke(
4389
- session,
4390
- "duckdb",
4391
- dbName,
4392
- DuckDbAction.IMPORT
4461
+ const response = await this.invokeWithRetry(
4462
+ `${this.host}/invoke`,
4463
+ () => this.context.invoke(
4464
+ this.context.session,
4465
+ "duckdb",
4466
+ dbName,
4467
+ DuckDbAction.IMPORT
4468
+ ),
4469
+ {
4470
+ headers: { "Content-Type": "application/x-duckdb" },
4471
+ body: new Blob([data]),
4472
+ signal: request.signal
4473
+ }
4393
4474
  );
4394
- const response = await this.context.fetch(`${this.host}/invoke`, {
4395
- method: "POST",
4396
- headers: {
4397
- ...headers,
4398
- "Content-Type": "application/x-duckdb"
4399
- },
4400
- body: new Blob([data]),
4401
- signal: request.signal
4402
- });
4403
4475
  if (!response.ok) {
4404
4476
  return await this.handleErrorResponse(response, "import");
4405
4477
  }
@@ -4413,18 +4485,15 @@ var DuckDbService = class extends BaseService {
4413
4485
  }
4414
4486
  // === Private helpers ===
4415
4487
  async invokeDuckDb(dbName, action, body, signal, extraHeaders) {
4416
- const session = this.context.session;
4417
- const headers = this.context.invoke(session, "duckdb", dbName, action);
4418
- return this.context.fetch(`${this.host}/invoke`, {
4419
- method: "POST",
4420
- headers: {
4421
- ...headers,
4422
- "Content-Type": "application/json",
4423
- ...extraHeaders
4424
- },
4425
- body: JSON.stringify(body),
4426
- signal
4427
- });
4488
+ return this.invokeWithRetry(
4489
+ `${this.host}/invoke`,
4490
+ () => this.context.invoke(this.context.session, "duckdb", dbName, action),
4491
+ {
4492
+ headers: { "Content-Type": "application/json", ...extraHeaders },
4493
+ body: JSON.stringify(body),
4494
+ signal
4495
+ }
4496
+ );
4428
4497
  }
4429
4498
  async handleErrorResponse(response, operation) {
4430
4499
  const errorText = await response.text();
@@ -4435,6 +4504,8 @@ var DuckDbService = class extends BaseService {
4435
4504
  if (response.status === 413 && parseStorageRejection(errorText)) {
4436
4505
  return err(storageRejectionError("duckdb", 413, meta, errorText));
4437
4506
  }
4507
+ const retryError = nodeRetryError("duckdb", `DuckDB ${operation} failed: ${response.status} - ${errorText}`, response, errorText);
4508
+ if (retryError) return err(retryError);
4438
4509
  const errorBody = parseServiceErrorBody(errorText);
4439
4510
  const errorCode = this.mapHttpStatusToErrorCode(
4440
4511
  response.status,
@@ -4611,26 +4682,24 @@ var HooksService = class extends BaseService {
4611
4682
  );
4612
4683
  }
4613
4684
  try {
4614
- const response = await this.context.fetch(`${this.host}/hooks/webhooks`, {
4615
- method: "POST",
4616
- headers: {
4617
- ...serviceHeadersToRecord(
4618
- this.createHookHeaders(
4619
- "tinycloud.hooks/register",
4620
- buildScopePath(webhook.service, webhook.pathPrefix)
4621
- )
4622
- ),
4623
- "content-type": "application/json"
4624
- },
4625
- body: JSON.stringify({
4626
- space: webhook.space,
4627
- service: webhook.service,
4628
- pathPrefix: normalizePathPrefix(webhook.pathPrefix),
4629
- abilities: webhook.abilities ?? [],
4630
- callbackUrl: webhook.callbackUrl,
4631
- secret: webhook.secret
4632
- })
4633
- });
4685
+ const response = await this.invokeWithRetry(
4686
+ `${this.host}/hooks/webhooks`,
4687
+ () => this.createHookHeaders(
4688
+ "tinycloud.hooks/register",
4689
+ buildScopePath(webhook.service, webhook.pathPrefix)
4690
+ ),
4691
+ {
4692
+ headers: { "content-type": "application/json" },
4693
+ body: JSON.stringify({
4694
+ space: webhook.space,
4695
+ service: webhook.service,
4696
+ pathPrefix: normalizePathPrefix(webhook.pathPrefix),
4697
+ abilities: webhook.abilities ?? [],
4698
+ callbackUrl: webhook.callbackUrl,
4699
+ secret: webhook.secret
4700
+ })
4701
+ }
4702
+ );
4634
4703
  if (!response.ok) {
4635
4704
  return err(
4636
4705
  await responseError("hooks", "failed to register webhook", response)
@@ -4668,17 +4737,13 @@ var HooksService = class extends BaseService {
4668
4737
  query.set("prefix", normalizedPrefix);
4669
4738
  }
4670
4739
  }
4671
- const response = await this.context.fetch(
4740
+ const response = await this.invokeWithRetry(
4672
4741
  `${this.host}/hooks/webhooks${query.size > 0 ? `?${query.toString()}` : ""}`,
4673
- {
4674
- method: "GET",
4675
- headers: serviceHeadersToRecord(
4676
- this.createHookHeaders(
4677
- "tinycloud.hooks/list",
4678
- options.service ? buildScopePath(options.service, options.pathPrefix) : "webhooks"
4679
- )
4680
- )
4681
- }
4742
+ () => this.createHookHeaders(
4743
+ "tinycloud.hooks/list",
4744
+ options.service ? buildScopePath(options.service, options.pathPrefix) : "webhooks"
4745
+ ),
4746
+ { method: "GET" }
4682
4747
  );
4683
4748
  if (!response.ok) {
4684
4749
  return err(
@@ -4705,20 +4770,16 @@ var HooksService = class extends BaseService {
4705
4770
  return err(authRequiredError("hooks"));
4706
4771
  }
4707
4772
  try {
4708
- const response = await this.context.fetch(
4773
+ const response = await this.invokeWithRetry(
4709
4774
  `${this.host}/hooks/webhooks/${encodeURIComponent(id)}`,
4710
- {
4711
- method: "DELETE",
4712
- headers: serviceHeadersToRecord(
4713
- this.createHookHeaders(
4714
- "tinycloud.hooks/unregister",
4715
- options.target ? buildScopePath(
4716
- options.target.service,
4717
- options.target.pathPrefix
4718
- ) : `webhooks/${id}`
4719
- )
4720
- )
4721
- }
4775
+ () => this.createHookHeaders(
4776
+ "tinycloud.hooks/unregister",
4777
+ options.target ? buildScopePath(
4778
+ options.target.service,
4779
+ options.target.pathPrefix
4780
+ ) : `webhooks/${id}`
4781
+ ),
4782
+ { method: "DELETE" }
4722
4783
  );
4723
4784
  if (!response.ok) {
4724
4785
  return err(
@@ -5174,23 +5235,7 @@ function readProperty(value, key) {
5174
5235
  return void 0;
5175
5236
  }
5176
5237
  }
5177
- function sleep(delayMs, _attempt, signal) {
5178
- return new Promise((resolve) => {
5179
- const finish = () => {
5180
- clearTimeout(timer);
5181
- signal.removeEventListener("abort", stop);
5182
- resolve();
5183
- };
5184
- const timer = setTimeout(finish, delayMs);
5185
- timer.unref?.();
5186
- const stop = () => finish();
5187
- if (signal.aborted) {
5188
- finish();
5189
- } else {
5190
- signal.addEventListener("abort", stop, { once: true });
5191
- }
5192
- });
5193
- }
5238
+ var sleep = (delayMs, _attempt, signal) => abortableSleep(delayMs, signal, true);
5194
5239
  function abortPromise(signal) {
5195
5240
  let resolve;
5196
5241
  const promise = new Promise((done) => {
@@ -5607,40 +5652,150 @@ function parseStorageStatus(body) {
5607
5652
  if (status.account.plan) status.plan = status.account.plan;
5608
5653
  status.state = storageUsageState(account.usedBytes, account.limitBytes);
5609
5654
  }
5610
- return status;
5655
+ return status;
5656
+ }
5657
+
5658
+ // src/quota/TinyCloudQuota.ts
5659
+ var TinyCloudQuota = class {
5660
+ constructor(config = {}) {
5661
+ this.quotaUrl = null;
5662
+ this.config = config;
5663
+ }
5664
+ /** Set the quota URL discovered from the /info endpoint */
5665
+ setQuotaUrl(url) {
5666
+ this.quotaUrl = url;
5667
+ }
5668
+ /** Whether a quota service is available */
5669
+ get available() {
5670
+ return this.quotaUrl !== null;
5671
+ }
5672
+ /** Query quota status for a space from the quota URL */
5673
+ async getQuota(spaceId) {
5674
+ if (!this.quotaUrl) return null;
5675
+ const resp = await fetch(
5676
+ `${this.quotaUrl}/api/quota/${encodeURIComponent(spaceId)}`
5677
+ );
5678
+ if (!resp.ok) return null;
5679
+ const data = await resp.json();
5680
+ return {
5681
+ limitBytes: data.storage_limit_bytes ?? 0
5682
+ };
5683
+ }
5684
+ /** Trigger the upgrade callback when a quota error is encountered */
5685
+ handleQuotaError(info) {
5686
+ this.config.onUpgradeRequired?.(info);
5687
+ }
5688
+ };
5689
+
5690
+ // src/encryption/canonical.ts
5691
+ function canonicalize(value) {
5692
+ if (value === void 0) {
5693
+ return "";
5694
+ }
5695
+ return stringify(value);
5696
+ }
5697
+ function stringify(value) {
5698
+ if (value === null) return "null";
5699
+ switch (typeof value) {
5700
+ case "boolean":
5701
+ case "number":
5702
+ return JSON.stringify(value);
5703
+ case "string":
5704
+ return JSON.stringify(value);
5705
+ case "object": {
5706
+ if (Array.isArray(value)) {
5707
+ return `[${value.map(stringify).join(",")}]`;
5708
+ }
5709
+ const keys = Object.keys(value).sort();
5710
+ const parts = [];
5711
+ for (const k of keys) {
5712
+ const v = value[k];
5713
+ if (v === void 0) continue;
5714
+ parts.push(`${JSON.stringify(k)}:${stringify(v)}`);
5715
+ }
5716
+ return `{${parts.join(",")}}`;
5717
+ }
5718
+ default:
5719
+ throw new TypeError(
5720
+ `canonicalize: unsupported value type ${typeof value}`
5721
+ );
5722
+ }
5723
+ }
5724
+ var HEX = "0123456789abcdef";
5725
+ function hexEncode(bytes) {
5726
+ let out = "";
5727
+ for (let i = 0; i < bytes.length; i++) {
5728
+ const b = bytes[i];
5729
+ out += HEX[b >> 4 & 15] + HEX[b & 15];
5730
+ }
5731
+ return out;
5611
5732
  }
5612
-
5613
- // src/quota/TinyCloudQuota.ts
5614
- var TinyCloudQuota = class {
5615
- constructor(config = {}) {
5616
- this.quotaUrl = null;
5617
- this.config = config;
5733
+ function hexDecode(hex) {
5734
+ if (hex.length % 2 !== 0) {
5735
+ throw new Error("hex string must have even length");
5618
5736
  }
5619
- /** Set the quota URL discovered from the /info endpoint */
5620
- setQuotaUrl(url) {
5621
- this.quotaUrl = url;
5737
+ const out = new Uint8Array(hex.length / 2);
5738
+ for (let i = 0; i < out.length; i++) {
5739
+ const hi = parseInt(hex[i * 2], 16);
5740
+ const lo = parseInt(hex[i * 2 + 1], 16);
5741
+ if (Number.isNaN(hi) || Number.isNaN(lo)) {
5742
+ throw new Error("invalid hex character");
5743
+ }
5744
+ out[i] = hi << 4 | lo;
5622
5745
  }
5623
- /** Whether a quota service is available */
5624
- get available() {
5625
- return this.quotaUrl !== null;
5746
+ return out;
5747
+ }
5748
+ function base64Encode(bytes) {
5749
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
5750
+ let out = "";
5751
+ for (let i = 0; i < bytes.length; i += 3) {
5752
+ const b0 = bytes[i];
5753
+ const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
5754
+ const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
5755
+ out += chars[b0 >> 2 & 63];
5756
+ out += chars[(b0 << 4 | b1 >> 4) & 63];
5757
+ out += i + 1 < bytes.length ? chars[(b1 << 2 | b2 >> 6) & 63] : "=";
5758
+ out += i + 2 < bytes.length ? chars[b2 & 63] : "=";
5626
5759
  }
5627
- /** Query quota status for a space from the quota URL */
5628
- async getQuota(spaceId) {
5629
- if (!this.quotaUrl) return null;
5630
- const resp = await fetch(
5631
- `${this.quotaUrl}/api/quota/${encodeURIComponent(spaceId)}`
5632
- );
5633
- if (!resp.ok) return null;
5634
- const data = await resp.json();
5635
- return {
5636
- limitBytes: data.storage_limit_bytes ?? 0
5637
- };
5760
+ return out;
5761
+ }
5762
+ function base64Decode(s) {
5763
+ const clean = s.replace(/[^A-Za-z0-9+/=]/g, "");
5764
+ const len = clean.length;
5765
+ if (len % 4 !== 0) {
5766
+ throw new Error("invalid base64 input");
5638
5767
  }
5639
- /** Trigger the upgrade callback when a quota error is encountered */
5640
- handleQuotaError(info) {
5641
- this.config.onUpgradeRequired?.(info);
5768
+ const padding = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
5769
+ const outLen = len / 4 * 3 - padding;
5770
+ const out = new Uint8Array(outLen);
5771
+ const lookup = {};
5772
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
5773
+ for (let i = 0; i < chars.length; i++) lookup[chars[i]] = i;
5774
+ let outIdx = 0;
5775
+ for (let i = 0; i < len; i += 4) {
5776
+ const v0 = lookup[clean[i]] ?? 0;
5777
+ const v1 = lookup[clean[i + 1]] ?? 0;
5778
+ const v2 = clean[i + 2] === "=" ? 0 : lookup[clean[i + 2]] ?? 0;
5779
+ const v3 = clean[i + 3] === "=" ? 0 : lookup[clean[i + 3]] ?? 0;
5780
+ const b0 = v0 << 2 | v1 >> 4;
5781
+ const b1 = (v1 & 15) << 4 | v2 >> 2;
5782
+ const b2 = (v2 & 3) << 6 | v3;
5783
+ if (outIdx < outLen) out[outIdx++] = b0;
5784
+ if (outIdx < outLen) out[outIdx++] = b1;
5785
+ if (outIdx < outLen) out[outIdx++] = b2;
5642
5786
  }
5643
- };
5787
+ return out;
5788
+ }
5789
+ function utf8Encode(s) {
5790
+ return new TextEncoder().encode(s);
5791
+ }
5792
+ function utf8Decode(b) {
5793
+ return new TextDecoder().decode(b);
5794
+ }
5795
+ function canonicalHashHex(sha256, value) {
5796
+ const canonical = canonicalize(value);
5797
+ return hexEncode(sha256(utf8Encode(canonical)));
5798
+ }
5644
5799
 
5645
5800
  // src/vault/types.ts
5646
5801
  var VaultPublicSpaceKVActions = [
@@ -5811,7 +5966,7 @@ function toBytes(str) {
5811
5966
  function fromBytes(bytes) {
5812
5967
  return new TextDecoder().decode(bytes);
5813
5968
  }
5814
- function hexEncode(bytes) {
5969
+ function hexEncode2(bytes) {
5815
5970
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
5816
5971
  }
5817
5972
  function concatBytes(...arrays) {
@@ -5824,14 +5979,14 @@ function concatBytes(...arrays) {
5824
5979
  }
5825
5980
  return result;
5826
5981
  }
5827
- function base64Encode(bytes) {
5982
+ function base64Encode2(bytes) {
5828
5983
  let binary = "";
5829
5984
  for (let i = 0; i < bytes.length; i++) {
5830
5985
  binary += String.fromCharCode(bytes[i]);
5831
5986
  }
5832
5987
  return btoa(binary);
5833
5988
  }
5834
- function base64Decode(str) {
5989
+ function base64Decode2(str) {
5835
5990
  const binary = atob(str);
5836
5991
  const bytes = new Uint8Array(binary.length);
5837
5992
  for (let i = 0; i < binary.length; i++) {
@@ -5873,6 +6028,11 @@ function defaultVaultMessage(input) {
5873
6028
  return input.message;
5874
6029
  case "PERMISSION_DENIED":
5875
6030
  return input.message;
6031
+ case "AAD_MISMATCH":
6032
+ case "ETAG_MISMATCH":
6033
+ case "INVALID_ETAG":
6034
+ case "OUTCOME_UNKNOWN":
6035
+ case "NODE_BUSY":
5876
6036
  case "STORAGE_QUOTA_EXCEEDED":
5877
6037
  case "STORAGE_LIMIT_REACHED":
5878
6038
  return input.message;
@@ -5895,6 +6055,37 @@ function typedKvCause(error) {
5895
6055
  ...error.cause !== void 0 ? { cause: error.cause } : {}
5896
6056
  });
5897
6057
  }
6058
+ function canonicalAadSpaceId(spaceId) {
6059
+ return spaceId.replace(/0x[0-9a-fA-F]{40}/g, (address) => address.toLowerCase());
6060
+ }
6061
+ function checkEnvelopeAad(value, spaceId, key) {
6062
+ const e = value;
6063
+ if (typeof e !== "object" || e === null || Array.isArray(e) || !["networkId", "alg", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext"].every(
6064
+ (field) => typeof e[field] === "string" && e[field] !== ""
6065
+ ) || !Number.isInteger(e.keyVersion) || e.aad !== void 0 && typeof e.aad !== "string") {
6066
+ return "corrupt";
6067
+ }
6068
+ let aad = "";
6069
+ try {
6070
+ if (typeof e.aad === "string") aad = fromBytes(base64Decode(e.aad));
6071
+ } catch {
6072
+ }
6073
+ const suffix = `:${key}`;
6074
+ return aad.endsWith(suffix) && canonicalAadSpaceId(aad.slice(0, -suffix.length)) === canonicalAadSpaceId(`tinycloud.vault:${spaceId}`) ? "ok" : "mismatch";
6075
+ }
6076
+ var AMBIGUOUS_WRITE_STATUSES = [500, 502, 503, 504, 520, 522, 524];
6077
+ function ifMatchWriteError(key, error) {
6078
+ const status = error.meta?.status;
6079
+ const code = error.code === ErrorCodes.KV_PRECONDITION_FAILED ? "ETAG_MISMATCH" : error.code === ErrorCodes.KV_WRITE_FAILED && status === 400 ? "INVALID_ETAG" : error.code === ErrorCodes.OUTCOME_UNKNOWN || error.code === ErrorCodes.KV_WRITE_FAILED && typeof status === "number" && AMBIGUOUS_WRITE_STATUSES.includes(status) || (error.code === ErrorCodes.NETWORK_ERROR || error.code === ErrorCodes.TIMEOUT) && status === void 0 && error.meta?.requestMayHaveDispatched !== false ? "OUTCOME_UNKNOWN" : error.code === ErrorCodes.NODE_BUSY || error.code === ErrorCodes.REQUEST_TIMEOUT ? "NODE_BUSY" : void 0;
6080
+ if (code === void 0) return void 0;
6081
+ return vaultError({
6082
+ code,
6083
+ key,
6084
+ message: error.message,
6085
+ ...error.meta !== void 0 ? { meta: error.meta } : {},
6086
+ cause: typedKvCause(error)
6087
+ });
6088
+ }
5898
6089
  function kvWriteError(context, error) {
5899
6090
  if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
5900
6091
  return vaultError({ code: error.code, message: error.message, meta: error.meta });
@@ -6081,7 +6272,7 @@ var DataVaultService = class extends BaseService {
6081
6272
  }
6082
6273
  if (existingPubKey) {
6083
6274
  this.encryptionIdentity = {
6084
- publicKey: base64Decode(existingPubKey),
6275
+ publicKey: base64Decode2(existingPubKey),
6085
6276
  privateKey: new Uint8Array(0)
6086
6277
  // private key not available without signing
6087
6278
  };
@@ -6106,7 +6297,7 @@ var DataVaultService = class extends BaseService {
6106
6297
  );
6107
6298
  this.encryptionIdentity = this.crypto.x25519FromSeed(seed);
6108
6299
  try {
6109
- const pubKeyB64 = base64Encode(this.encryptionIdentity.publicKey);
6300
+ const pubKeyB64 = base64Encode2(this.encryptionIdentity.publicKey);
6110
6301
  await this.tc.ensurePublicSpace();
6111
6302
  await this.tc.publicKV.put(".well-known/vault-pubkey", pubKeyB64);
6112
6303
  await this.tc.publicKV.put(".well-known/vault-version", CURRENT_VAULT_VERSION);
@@ -6172,6 +6363,21 @@ var DataVaultService = class extends BaseService {
6172
6363
  message: "Authentication required"
6173
6364
  });
6174
6365
  }
6366
+ if (options?.ifMatch !== void 0) {
6367
+ if (options.ifNoneMatch !== void 0) {
6368
+ return vaultError({
6369
+ code: "STORAGE_ERROR",
6370
+ cause: new Error("ifMatch cannot be combined with ifNoneMatch")
6371
+ });
6372
+ }
6373
+ if (!/^"[^"\s]+"$/.test(options.ifMatch)) {
6374
+ return vaultError({
6375
+ code: "INVALID_ETAG",
6376
+ key,
6377
+ message: options.ifMatch.startsWith("W/") ? 'ifMatch is a weak etag (W/"..."): the node only issues strong etags, so the etag was weakened in transit (a proxy or CDN rewrote it). Read it from a connection that preserves strong ETags' : "ifMatch must be the strong, quoted ETag returned by a network read (VaultEntry.etag), passed exactly as returned"
6378
+ });
6379
+ }
6380
+ }
6175
6381
  try {
6176
6382
  const { plaintext, contentType } = this.serializeValue(value, options);
6177
6383
  const metadata = {
@@ -6194,7 +6400,8 @@ var DataVaultService = class extends BaseService {
6194
6400
  }
6195
6401
  const valuePath = `vault/${key}`;
6196
6402
  const valueBody = JSON.stringify(envelopeResult.data);
6197
- const valuePutResult = options?.ifNoneMatch !== void 0 ? await this.tc.kv.put(valuePath, valueBody, { ifNoneMatch: options.ifNoneMatch }) : await this.tc.kv.put(valuePath, valueBody);
6403
+ const putOptions = options?.ifMatch !== void 0 ? { ifMatch: options.ifMatch } : options?.ifNoneMatch !== void 0 ? { ifNoneMatch: options.ifNoneMatch } : void 0;
6404
+ const valuePutResult = putOptions !== void 0 ? await this.tc.kv.put(valuePath, valueBody, putOptions) : await this.tc.kv.put(valuePath, valueBody);
6198
6405
  if (!valuePutResult.ok) {
6199
6406
  if (options?.ifNoneMatch !== void 0 && valuePutResult.error.code === ErrorCodes.KV_PRECONDITION_FAILED) {
6200
6407
  return vaultError({
@@ -6204,6 +6411,10 @@ var DataVaultService = class extends BaseService {
6204
6411
  cause: typedKvCause(valuePutResult.error)
6205
6412
  });
6206
6413
  }
6414
+ if (options?.ifMatch !== void 0) {
6415
+ const classified = ifMatchWriteError(key, valuePutResult.error);
6416
+ if (classified !== void 0) return classified;
6417
+ }
6207
6418
  return kvWriteError("Failed to store encrypted value", valuePutResult.error);
6208
6419
  }
6209
6420
  return { ok: true, data: void 0 };
@@ -6238,6 +6449,19 @@ var DataVaultService = class extends BaseService {
6238
6449
  }
6239
6450
  const rawEnvelope = unwrapKVData(valueResult.data);
6240
6451
  const envelope = typeof rawEnvelope === "string" ? JSON.parse(rawEnvelope) : rawEnvelope;
6452
+ if (options?.expectAad === true) {
6453
+ const verdict = checkEnvelopeAad(envelope, this.vaultConfig.spaceId, key);
6454
+ if (verdict === "corrupt") {
6455
+ return vaultError({ code: "DECRYPTION_FAILED", message: "envelope is malformed" });
6456
+ }
6457
+ if (verdict === "mismatch") {
6458
+ return vaultError({
6459
+ code: "AAD_MISMATCH",
6460
+ key,
6461
+ message: `Envelope at ${key} is not bound to this space and key`
6462
+ });
6463
+ }
6464
+ }
6241
6465
  const proof = await this.decryptCapabilityProof();
6242
6466
  const plaintextResult = await config.service.decryptEnvelope(envelope, proof);
6243
6467
  if (!plaintextResult.ok) {
@@ -6254,7 +6478,11 @@ var DataVaultService = class extends BaseService {
6254
6478
  contentType,
6255
6479
  options
6256
6480
  );
6257
- return { ok: true, data: { value, metadata, keyId } };
6481
+ const etag = options?.source === "network" ? valueResult.data.headers?.etag : void 0;
6482
+ return {
6483
+ ok: true,
6484
+ data: { value, metadata, keyId, ...etag === void 0 ? {} : { etag } }
6485
+ };
6258
6486
  } catch (error) {
6259
6487
  return vaultError({
6260
6488
  code: "STORAGE_ERROR",
@@ -6300,6 +6528,12 @@ var DataVaultService = class extends BaseService {
6300
6528
  } catch {
6301
6529
  return { status: "corrupt_envelope" };
6302
6530
  }
6531
+ if (options?.expectAad === true) {
6532
+ const verdict = checkEnvelopeAad(envelope, this.vaultConfig.spaceId, key);
6533
+ if (verdict !== "ok") {
6534
+ return { status: verdict === "corrupt" ? "corrupt_envelope" : "aad_mismatch" };
6535
+ }
6536
+ }
6303
6537
  let plaintextResult;
6304
6538
  try {
6305
6539
  const proof = await this.decryptCapabilityProof();
@@ -6331,7 +6565,11 @@ var DataVaultService = class extends BaseService {
6331
6565
  contentType,
6332
6566
  options
6333
6567
  );
6334
- return { status: "ok", entry: { value, metadata, keyId } };
6568
+ const etag = valueResult.data.headers?.etag;
6569
+ return {
6570
+ status: "ok",
6571
+ entry: { value, metadata, keyId, ...etag === void 0 ? {} : { etag } }
6572
+ };
6335
6573
  } catch {
6336
6574
  return { status: "invalid_payload" };
6337
6575
  }
@@ -6381,6 +6619,14 @@ var DataVaultService = class extends BaseService {
6381
6619
  )
6382
6620
  });
6383
6621
  }
6622
+ if (options?.ifMatch !== void 0) {
6623
+ return vaultError({
6624
+ code: "STORAGE_ERROR",
6625
+ cause: new Error(
6626
+ "ifMatch is only supported by network-encrypted vaults"
6627
+ )
6628
+ });
6629
+ }
6384
6630
  if (!this._isUnlocked || !this.masterKey) {
6385
6631
  return vaultError({
6386
6632
  code: "VAULT_LOCKED",
@@ -6406,7 +6652,7 @@ var DataVaultService = class extends BaseService {
6406
6652
  }
6407
6653
  const contentType = options?.contentType ?? (value instanceof Uint8Array ? "application/octet-stream" : "application/json");
6408
6654
  const entryKey = this.crypto.randomBytes(32);
6409
- const keyId = hexEncode(this.crypto.sha256(entryKey)).slice(0, 16);
6655
+ const keyId = hexEncode2(this.crypto.sha256(entryKey)).slice(0, 16);
6410
6656
  const encrypted = this.crypto.encrypt(entryKey, plaintext);
6411
6657
  const keyBlob = this.crypto.encrypt(this.masterKey, entryKey);
6412
6658
  const metadata = {
@@ -6424,7 +6670,7 @@ var DataVaultService = class extends BaseService {
6424
6670
  ...metadata
6425
6671
  });
6426
6672
  const keyPayload = JSON.stringify({
6427
- key: base64Encode(keyBlob),
6673
+ key: base64Encode2(keyBlob),
6428
6674
  metadata: keyMetadata
6429
6675
  });
6430
6676
  const keyPutResult = await this.tc.kv.put(
@@ -6435,7 +6681,7 @@ var DataVaultService = class extends BaseService {
6435
6681
  return kvWriteError("Failed to store key blob", keyPutResult.error);
6436
6682
  }
6437
6683
  const valuePayload = JSON.stringify({
6438
- data: base64Encode(encrypted),
6684
+ data: base64Encode2(encrypted),
6439
6685
  metadata
6440
6686
  });
6441
6687
  const valuePutResult = await this.tc.kv.put(
@@ -6466,6 +6712,12 @@ var DataVaultService = class extends BaseService {
6466
6712
  if (this.usesNetworkEncryption) {
6467
6713
  return this.getNetworkEncrypted(key, options);
6468
6714
  }
6715
+ if (options?.expectAad === true) {
6716
+ return vaultError({
6717
+ code: "STORAGE_ERROR",
6718
+ cause: new Error("expectAad is only supported by network-encrypted vaults")
6719
+ });
6720
+ }
6469
6721
  if (!this._isUnlocked || !this.masterKey) {
6470
6722
  return vaultError({
6471
6723
  code: "VAULT_LOCKED",
@@ -6487,7 +6739,7 @@ var DataVaultService = class extends BaseService {
6487
6739
  return vaultError({ code: "KEY_NOT_FOUND", key });
6488
6740
  }
6489
6741
  const keyEnvelope = JSON.parse(keyResult.data.data);
6490
- const keyBlobBytes = base64Decode(keyEnvelope.key);
6742
+ const keyBlobBytes = base64Decode2(keyEnvelope.key);
6491
6743
  const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
6492
6744
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
6493
6745
  raw: true,
@@ -6497,7 +6749,7 @@ var DataVaultService = class extends BaseService {
6497
6749
  return vaultError({ code: "KEY_NOT_FOUND", key });
6498
6750
  }
6499
6751
  const valueEnvelope = JSON.parse(valueResult.data.data);
6500
- const encryptedBytes = base64Decode(valueEnvelope.data);
6752
+ const encryptedBytes = base64Decode2(valueEnvelope.data);
6501
6753
  const plaintext = this.crypto.decrypt(entryKey, encryptedBytes);
6502
6754
  const metadata = valueEnvelope.metadata ?? {};
6503
6755
  const contentType = metadata[VaultHeaders.CONTENT_TYPE] ?? "application/json";
@@ -6745,7 +6997,7 @@ var DataVaultService = class extends BaseService {
6745
6997
  return vaultError({ code: "KEY_NOT_FOUND", key });
6746
6998
  }
6747
6999
  const keyEnvelope = JSON.parse(keyResult.data.data);
6748
- const keyBlobBytes = base64Decode(keyEnvelope.key);
7000
+ const keyBlobBytes = base64Decode2(keyEnvelope.key);
6749
7001
  const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
6750
7002
  const ephemeralSeed = this.crypto.randomBytes(32);
6751
7003
  const ephemeralKeyPair = this.crypto.x25519FromSeed(ephemeralSeed);
@@ -6764,7 +7016,7 @@ var DataVaultService = class extends BaseService {
6764
7016
  encryptedGrant
6765
7017
  );
6766
7018
  const grantPayload = JSON.stringify({
6767
- grant: base64Encode(grantBlob),
7019
+ grant: base64Encode2(grantBlob),
6768
7020
  spaceId: this.vaultConfig.spaceId,
6769
7021
  metadata: {
6770
7022
  [VaultHeaders.GRANT_VERSION]: "1",
@@ -6804,6 +7056,14 @@ var DataVaultService = class extends BaseService {
6804
7056
  */
6805
7057
  async getShared(grantorDID, key, options) {
6806
7058
  return this.withTelemetry("getShared", key, async () => {
7059
+ if (options?.expectAad === true) {
7060
+ return vaultError({
7061
+ code: "STORAGE_ERROR",
7062
+ cause: new Error(
7063
+ "expectAad is not supported by getShared: the AAD binds the grantor's space, which this vault does not know"
7064
+ )
7065
+ });
7066
+ }
6807
7067
  if (this.usesNetworkEncryption) {
6808
7068
  const grantorKV = options?.kv;
6809
7069
  if (!grantorKV) {
@@ -6898,7 +7158,7 @@ var DataVaultService = class extends BaseService {
6898
7158
  });
6899
7159
  }
6900
7160
  const grantEnvelope = typeof grantResult.data?.data === "string" ? JSON.parse(grantResult.data.data) : grantResult.data?.data;
6901
- const grantBlobBytes = base64Decode(grantEnvelope.grant);
7161
+ const grantBlobBytes = base64Decode2(grantEnvelope.grant);
6902
7162
  const ephemeralPubKey = grantBlobBytes.slice(0, 32);
6903
7163
  const encryptedGrant = grantBlobBytes.slice(32);
6904
7164
  const sharedSecret = this.crypto.x25519Dh(
@@ -6921,7 +7181,7 @@ var DataVaultService = class extends BaseService {
6921
7181
  });
6922
7182
  }
6923
7183
  const valueEnvelope = typeof valueResult.data?.data === "string" ? JSON.parse(valueResult.data.data) : valueResult.data?.data;
6924
- const encryptedBytes = base64Decode(valueEnvelope.data);
7184
+ const encryptedBytes = base64Decode2(valueEnvelope.data);
6925
7185
  const plaintext = this.crypto.decrypt(entryKey, encryptedBytes);
6926
7186
  const metadata = valueEnvelope.metadata ?? {};
6927
7187
  const contentType = metadata[VaultHeaders.CONTENT_TYPE] ?? "application/json";
@@ -6975,7 +7235,7 @@ var DataVaultService = class extends BaseService {
6975
7235
  if (!result.ok) {
6976
7236
  return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
6977
7237
  }
6978
- const pubKeyBytes = base64Decode(result.data);
7238
+ const pubKeyBytes = base64Decode2(result.data);
6979
7239
  return { ok: true, data: pubKeyBytes };
6980
7240
  } catch (error) {
6981
7241
  return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
@@ -7094,7 +7354,7 @@ var DataVaultService = class extends BaseService {
7094
7354
  }
7095
7355
  const currentEntry = getResult.data;
7096
7356
  const newEntryKey = this.crypto.randomBytes(32);
7097
- const newKeyId = hexEncode(this.crypto.sha256(newEntryKey)).slice(
7357
+ const newKeyId = hexEncode2(this.crypto.sha256(newEntryKey)).slice(
7098
7358
  0,
7099
7359
  16
7100
7360
  );
@@ -7111,7 +7371,7 @@ var DataVaultService = class extends BaseService {
7111
7371
  [VaultHeaders.KEY_ID]: newKeyId
7112
7372
  };
7113
7373
  const keyPayload = JSON.stringify({
7114
- key: base64Encode(newKeyBlob),
7374
+ key: base64Encode2(newKeyBlob),
7115
7375
  metadata: JSON.stringify({
7116
7376
  keyId: newKeyId,
7117
7377
  ...metadata
@@ -7125,7 +7385,7 @@ var DataVaultService = class extends BaseService {
7125
7385
  return kvWriteError("Failed to store rotated key blob", keyPutResult.error);
7126
7386
  }
7127
7387
  const valuePayload = JSON.stringify({
7128
- data: base64Encode(encrypted),
7388
+ data: base64Encode2(encrypted),
7129
7389
  metadata
7130
7390
  });
7131
7391
  const valuePutResult = await this.tc.kv.put(
@@ -7417,116 +7677,6 @@ var SecretsService = class {
7417
7677
  }
7418
7678
  };
7419
7679
 
7420
- // src/encryption/canonical.ts
7421
- function canonicalize(value) {
7422
- if (value === void 0) {
7423
- return "";
7424
- }
7425
- return stringify(value);
7426
- }
7427
- function stringify(value) {
7428
- if (value === null) return "null";
7429
- switch (typeof value) {
7430
- case "boolean":
7431
- case "number":
7432
- return JSON.stringify(value);
7433
- case "string":
7434
- return JSON.stringify(value);
7435
- case "object": {
7436
- if (Array.isArray(value)) {
7437
- return `[${value.map(stringify).join(",")}]`;
7438
- }
7439
- const keys = Object.keys(value).sort();
7440
- const parts = [];
7441
- for (const k of keys) {
7442
- const v = value[k];
7443
- if (v === void 0) continue;
7444
- parts.push(`${JSON.stringify(k)}:${stringify(v)}`);
7445
- }
7446
- return `{${parts.join(",")}}`;
7447
- }
7448
- default:
7449
- throw new TypeError(
7450
- `canonicalize: unsupported value type ${typeof value}`
7451
- );
7452
- }
7453
- }
7454
- var HEX = "0123456789abcdef";
7455
- function hexEncode2(bytes) {
7456
- let out = "";
7457
- for (let i = 0; i < bytes.length; i++) {
7458
- const b = bytes[i];
7459
- out += HEX[b >> 4 & 15] + HEX[b & 15];
7460
- }
7461
- return out;
7462
- }
7463
- function hexDecode(hex) {
7464
- if (hex.length % 2 !== 0) {
7465
- throw new Error("hex string must have even length");
7466
- }
7467
- const out = new Uint8Array(hex.length / 2);
7468
- for (let i = 0; i < out.length; i++) {
7469
- const hi = parseInt(hex[i * 2], 16);
7470
- const lo = parseInt(hex[i * 2 + 1], 16);
7471
- if (Number.isNaN(hi) || Number.isNaN(lo)) {
7472
- throw new Error("invalid hex character");
7473
- }
7474
- out[i] = hi << 4 | lo;
7475
- }
7476
- return out;
7477
- }
7478
- function base64Encode2(bytes) {
7479
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7480
- let out = "";
7481
- for (let i = 0; i < bytes.length; i += 3) {
7482
- const b0 = bytes[i];
7483
- const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
7484
- const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
7485
- out += chars[b0 >> 2 & 63];
7486
- out += chars[(b0 << 4 | b1 >> 4) & 63];
7487
- out += i + 1 < bytes.length ? chars[(b1 << 2 | b2 >> 6) & 63] : "=";
7488
- out += i + 2 < bytes.length ? chars[b2 & 63] : "=";
7489
- }
7490
- return out;
7491
- }
7492
- function base64Decode2(s) {
7493
- const clean = s.replace(/[^A-Za-z0-9+/=]/g, "");
7494
- const len = clean.length;
7495
- if (len % 4 !== 0) {
7496
- throw new Error("invalid base64 input");
7497
- }
7498
- const padding = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
7499
- const outLen = len / 4 * 3 - padding;
7500
- const out = new Uint8Array(outLen);
7501
- const lookup = {};
7502
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7503
- for (let i = 0; i < chars.length; i++) lookup[chars[i]] = i;
7504
- let outIdx = 0;
7505
- for (let i = 0; i < len; i += 4) {
7506
- const v0 = lookup[clean[i]] ?? 0;
7507
- const v1 = lookup[clean[i + 1]] ?? 0;
7508
- const v2 = clean[i + 2] === "=" ? 0 : lookup[clean[i + 2]] ?? 0;
7509
- const v3 = clean[i + 3] === "=" ? 0 : lookup[clean[i + 3]] ?? 0;
7510
- const b0 = v0 << 2 | v1 >> 4;
7511
- const b1 = (v1 & 15) << 4 | v2 >> 2;
7512
- const b2 = (v2 & 3) << 6 | v3;
7513
- if (outIdx < outLen) out[outIdx++] = b0;
7514
- if (outIdx < outLen) out[outIdx++] = b1;
7515
- if (outIdx < outLen) out[outIdx++] = b2;
7516
- }
7517
- return out;
7518
- }
7519
- function utf8Encode(s) {
7520
- return new TextEncoder().encode(s);
7521
- }
7522
- function utf8Decode(b) {
7523
- return new TextDecoder().decode(b);
7524
- }
7525
- function canonicalHashHex(sha256, value) {
7526
- const canonical = canonicalize(value);
7527
- return hexEncode2(sha256(utf8Encode(canonical)));
7528
- }
7529
-
7530
7680
  // src/encryption/types.ts
7531
7681
  var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
7532
7682
  var ENVELOPE_VERSION = 1;
@@ -7736,7 +7886,7 @@ function encryptToNetwork(crypto2, input) {
7736
7886
  const symmetricKey = crypto2.randomBytes(32);
7737
7887
  const ciphertext = crypto2.authEncrypt(symmetricKey, input.plaintext, input.aad);
7738
7888
  const wrapped = crypto2.sealToNetworkKey(input.networkPublicKey, symmetricKey);
7739
- const encryptedSymmetricKey = base64Encode2(wrapped);
7889
+ const encryptedSymmetricKey = base64Encode(wrapped);
7740
7890
  const encryptedSymmetricKeyHash = canonicalHashHex(
7741
7891
  crypto2.sha256,
7742
7892
  encryptedSymmetricKey
@@ -7748,8 +7898,8 @@ function encryptToNetwork(crypto2, input) {
7748
7898
  keyVersion,
7749
7899
  encryptedSymmetricKey,
7750
7900
  encryptedSymmetricKeyHash,
7751
- ciphertext: base64Encode2(ciphertext),
7752
- ...input.aad !== void 0 ? { aad: base64Encode2(input.aad) } : {},
7901
+ ciphertext: base64Encode(ciphertext),
7902
+ ...input.aad !== void 0 ? { aad: base64Encode(input.aad) } : {},
7753
7903
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
7754
7904
  };
7755
7905
  return { envelope, symmetricKey };
@@ -7823,8 +7973,8 @@ function validateEnvelope(crypto2, envelope) {
7823
7973
  return { ok: true, data: e };
7824
7974
  }
7825
7975
  function decryptEnvelopeWithKey(crypto2, envelope, symmetricKey) {
7826
- const ciphertext = base64Decode2(envelope.ciphertext);
7827
- const aad = envelope.aad !== void 0 ? base64Decode2(envelope.aad) : void 0;
7976
+ const ciphertext = base64Decode(envelope.ciphertext);
7977
+ const aad = envelope.aad !== void 0 ? base64Decode(envelope.aad) : void 0;
7828
7978
  return crypto2.authDecrypt(symmetricKey, ciphertext, aad);
7829
7979
  }
7830
7980
 
@@ -7842,7 +7992,7 @@ function buildCanonicalDecryptRequest(input) {
7842
7992
  return { canonicalBody, bodyHash, receiverPublicKeyHash };
7843
7993
  }
7844
7994
  function buildDecryptFacts(input) {
7845
- const bodyHash = input.canonicalBody !== void 0 ? hexEncode2(input.crypto.sha256(utf8Encode(input.canonicalBody))) : canonicalHashHex(
7995
+ const bodyHash = input.canonicalBody !== void 0 ? hexEncode(input.crypto.sha256(utf8Encode(input.canonicalBody))) : canonicalHashHex(
7846
7996
  input.crypto.sha256,
7847
7997
  input.body
7848
7998
  );
@@ -8125,7 +8275,7 @@ function verifyDecryptResponse(input) {
8125
8275
  })
8126
8276
  };
8127
8277
  }
8128
- const expectedRequestHash = hexEncode2(
8278
+ const expectedRequestHash = hexEncode(
8129
8279
  crypto2.sha256(utf8Encode(`${invocationCid}${requestBodyHash}`))
8130
8280
  );
8131
8281
  if (response.requestHash !== expectedRequestHash) {
@@ -8149,7 +8299,7 @@ function verifyDecryptResponse(input) {
8149
8299
  const signedBytes = new TextEncoder().encode(
8150
8300
  canonicalSignedResponse(response)
8151
8301
  );
8152
- const signatureBytes = base64Decode2(response.nodeSignature);
8302
+ const signatureBytes = base64Decode(response.nodeSignature);
8153
8303
  if (!crypto2.verifyNodeSignature(response.nodeId, signedBytes, signatureBytes)) {
8154
8304
  return {
8155
8305
  ok: false,
@@ -8161,7 +8311,7 @@ function verifyDecryptResponse(input) {
8161
8311
  return { ok: true, data: response };
8162
8312
  }
8163
8313
  function openWrappedKey(crypto2, receiverPrivateKey, response) {
8164
- const wrapped = base64Decode2(response.wrappedKey);
8314
+ const wrapped = base64Decode(response.wrappedKey);
8165
8315
  return crypto2.openWithReceiverKey(receiverPrivateKey, wrapped);
8166
8316
  }
8167
8317
 
@@ -8205,7 +8355,7 @@ var EncryptionService = class extends BaseService {
8205
8355
  const usable = ensureNetworkUsableForDecrypt(discovered.data);
8206
8356
  if (!usable.ok) return usable;
8207
8357
  const descriptor = usable.data;
8208
- const networkPublicKey = base64Decode2(descriptor.publicEncryptionKey);
8358
+ const networkPublicKey = base64Decode(descriptor.publicEncryptionKey);
8209
8359
  const result = encryptToNetwork(this.crypto, {
8210
8360
  // Discovery returns the node's canonical network identifier. Persist
8211
8361
  // that spelling in the envelope so a later response can be compared
@@ -8233,7 +8383,7 @@ var EncryptionService = class extends BaseService {
8233
8383
  this.assertActive();
8234
8384
  const validated = validateEnvelope(this.crypto, envelope);
8235
8385
  if (!validated.ok) return validated;
8236
- if (options?.aad !== void 0 && validated.data.aad !== base64Encode2(options.aad)) {
8386
+ if (options?.aad !== void 0 && validated.data.aad !== base64Encode(options.aad)) {
8237
8387
  return encErr(
8238
8388
  encryptionError({
8239
8389
  code: "INVALID_INPUT",
@@ -8261,7 +8411,7 @@ var EncryptionService = class extends BaseService {
8261
8411
  );
8262
8412
  }
8263
8413
  const receiverKey = generateRandomReceiverKey({ crypto: this.crypto });
8264
- const receiverPublicKey = base64Encode2(receiverKey.publicKey);
8414
+ const receiverPublicKey = base64Encode(receiverKey.publicKey);
8265
8415
  const receiverPublicKeyHash = canonicalHashHex(
8266
8416
  this.crypto.sha256,
8267
8417
  receiverPublicKey
@@ -8476,6 +8626,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
8476
8626
  kvPrefixCovers,
8477
8627
  networkDiscoveryKey,
8478
8628
  networkError,
8629
+ nodeRetryError,
8479
8630
  notFoundError,
8480
8631
  ok,
8481
8632
  openWrappedKey,