@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.js CHANGED
@@ -14,6 +14,14 @@ var ErrorCodes = {
14
14
  ABORTED: "ABORTED",
15
15
  INVALID_INPUT: "INVALID_INPUT",
16
16
  PERMISSION_DENIED: "PERMISSION_DENIED",
17
+ // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
18
+ // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
19
+ /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
20
+ NODE_BUSY: "NODE_BUSY",
21
+ /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
22
+ REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
23
+ /** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
24
+ OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
17
25
  // KV-specific errors
18
26
  KV_NOT_FOUND: "KV_NOT_FOUND",
19
27
  KV_WRITE_FAILED: "KV_WRITE_FAILED",
@@ -49,11 +57,12 @@ var ErrorCodes = {
49
57
  DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
50
58
  };
51
59
  var defaultRetryPolicy = {
52
- maxAttempts: 3,
60
+ maxAttempts: 4,
53
61
  backoff: "exponential",
54
- baseDelayMs: 1e3,
55
- maxDelayMs: 1e4,
56
- retryableErrors: [ErrorCodes.NETWORK_ERROR, ErrorCodes.TIMEOUT]
62
+ baseDelayMs: 250,
63
+ maxDelayMs: 4e3,
64
+ retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
65
+ maxTotalMs: 12e3
57
66
  };
58
67
  var TelemetryEvents = {
59
68
  SPAN: "telemetry.span",
@@ -480,7 +489,9 @@ var RetryPolicySchema = z.object({
480
489
  /** Maximum delay in milliseconds between retries */
481
490
  maxDelayMs: z.number().nonnegative(),
482
491
  /** Error codes that should trigger a retry */
483
- retryableErrors: z.array(z.string())
492
+ retryableErrors: z.array(z.string()),
493
+ /** Total time budget for all attempts and waits, in milliseconds */
494
+ maxTotalMs: z.number().positive().optional()
484
495
  });
485
496
  var ServiceSessionSchema = z.object({
486
497
  /** The delegation header containing the UCAN */
@@ -617,412 +628,107 @@ function validateTelemetrySpanEvent(data) {
617
628
  return { ok: true, data: result.data };
618
629
  }
619
630
 
620
- // src/context.ts
621
- var ServiceContext = class {
622
- constructor(config) {
623
- this._session = null;
624
- this._services = /* @__PURE__ */ new Map();
625
- this._eventHandlers = /* @__PURE__ */ new Map();
626
- this._abortController = new AbortController();
627
- this._retired = false;
628
- this._invoke = this.wrapInvoke(config.invoke);
629
- this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
630
- this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
631
- this._hosts = config.hosts;
632
- this._session = config.session ?? null;
633
- this._retryPolicy = {
634
- ...defaultRetryPolicy,
635
- ...config.retryPolicy
636
- };
637
- this._telemetryEnabled = typeof config.telemetry === "boolean" ? config.telemetry : config.telemetry?.enabled === true;
638
- this._telemetryHandler = typeof config.telemetry === "object" ? config.telemetry.onEvent : void 0;
639
- }
640
- // ============================================================
641
- // Session Management
642
- // ============================================================
643
- /**
644
- * Get the current session.
645
- */
646
- get session() {
647
- return this._retired ? null : this._session;
631
+ // src/responseErrors.ts
632
+ function parseServiceErrorBody(errorText) {
633
+ try {
634
+ return JSON.parse(errorText);
635
+ } catch {
636
+ return {};
648
637
  }
649
- /**
650
- * Check if the context has an authenticated session.
651
- */
652
- get isAuthenticated() {
653
- return !this._retired && this._session !== null;
638
+ }
639
+ function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
640
+ if (parsed.message) {
641
+ return compactMessage(parsed.message);
654
642
  }
655
- /**
656
- * Update the session and notify all registered services.
657
- *
658
- * @param session - New session or null to clear
659
- */
660
- setSession(session) {
661
- this.assertActive();
662
- this._session = session;
663
- this.emit("session.changed", { authenticated: session !== null });
664
- for (const service of this._services.values()) {
665
- service.onSessionChange(session);
643
+ if (looksLikeHtml(errorText)) {
644
+ if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
645
+ return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
666
646
  }
647
+ return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
667
648
  }
668
- // ============================================================
669
- // Platform Dependencies
670
- // ============================================================
671
- /**
672
- * Get the invoke function for WASM operations.
673
- */
674
- get invoke() {
675
- this.assertActive();
676
- return this._invoke;
677
- }
678
- /**
679
- * Get the multi-resource invoke function when available.
680
- */
681
- get invokeAny() {
682
- this.assertActive();
683
- return this._invokeAny;
649
+ return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
650
+ }
651
+ function responseErrorMeta(status, statusText, errorText) {
652
+ const meta = { status, statusText };
653
+ const snippet = compactMessage(errorText);
654
+ if (snippet) {
655
+ meta.responseSnippet = snippet.slice(0, 300);
684
656
  }
685
- /**
686
- * Get the fetch function for HTTP requests.
687
- */
688
- get fetch() {
689
- this.assertActive();
690
- return this._fetch;
657
+ return meta;
658
+ }
659
+ function looksLikeHtml(text) {
660
+ return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
661
+ }
662
+ function compactMessage(message) {
663
+ const compact = message.replace(/\s+/g, " ").trim();
664
+ return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
665
+ }
666
+
667
+ // src/errors.ts
668
+ import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
669
+
670
+ // src/encryption/networkId.ts
671
+ var URN_PREFIX = "urn:tinycloud:encryption:";
672
+ var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
673
+ var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
674
+ var NetworkIdError = class extends Error {
675
+ constructor(message) {
676
+ super(message);
677
+ this.name = "NetworkIdError";
691
678
  }
692
- /**
693
- * Get the list of TinyCloud host URLs.
694
- */
695
- get hosts() {
696
- this.assertActive();
697
- return this._hosts;
679
+ };
680
+ function parseNetworkId(networkId) {
681
+ if (typeof networkId !== "string" || networkId.length === 0) {
682
+ throw new NetworkIdError("networkId must be a non-empty string");
698
683
  }
699
- // ============================================================
700
- // Service Registry
701
- // ============================================================
702
- /**
703
- * Register a service with the context.
704
- *
705
- * @param name - Service name (e.g., 'kv')
706
- * @param service - Service instance
707
- */
708
- registerService(name, service) {
709
- this._services.set(name, service);
684
+ if (!networkId.startsWith(URN_PREFIX)) {
685
+ throw new NetworkIdError(
686
+ `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
687
+ );
710
688
  }
711
- /**
712
- * Unregister a service from the context.
713
- *
714
- * @param name - Service name to remove
715
- */
716
- unregisterService(name) {
717
- this._services.delete(name);
689
+ const body = networkId.slice(URN_PREFIX.length);
690
+ const lastColon = body.lastIndexOf(":");
691
+ if (lastColon <= 0 || lastColon === body.length - 1) {
692
+ throw new NetworkIdError(
693
+ `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
694
+ );
718
695
  }
719
- /**
720
- * Get a registered service by name.
721
- *
722
- * @param name - Service name
723
- * @returns The service instance or undefined if not registered
724
- */
725
- getService(name) {
726
- return this._services.get(name);
696
+ const ownerDid = body.slice(0, lastColon);
697
+ const name = body.slice(lastColon + 1);
698
+ if (!ownerDid.startsWith("did:")) {
699
+ throw new NetworkIdError(
700
+ `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
701
+ );
727
702
  }
728
- // ============================================================
729
- // Event System (Telemetry)
730
- // ============================================================
731
- /**
732
- * Emit a telemetry event.
733
- *
734
- * @param event - Event name
735
- * @param data - Event data
736
- */
737
- emit(event, data) {
738
- tinyCloudDebugLogger.log(event, data);
739
- if (this._telemetryEnabled && this._telemetryHandler) {
740
- try {
741
- this._telemetryHandler(event, projectDiagnosticData(data));
742
- } catch (error) {
743
- console.error(`Error in telemetry handler for "${event}":`, error);
744
- }
745
- }
746
- const handlers = this._eventHandlers.get(event);
747
- if (handlers) {
748
- for (const handler of handlers) {
749
- try {
750
- handler(data);
751
- } catch (error) {
752
- console.error(`Error in event handler for "${event}":`, error);
753
- }
754
- }
755
- }
703
+ const didParts = ownerDid.split(":");
704
+ if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
705
+ throw new NetworkIdError(
706
+ `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
707
+ );
756
708
  }
757
- /**
758
- * Subscribe to telemetry events.
759
- *
760
- * @param event - Event name to subscribe to
761
- * @param handler - Handler function
762
- * @returns Unsubscribe function
763
- */
764
- on(event, handler) {
765
- if (!this._eventHandlers.has(event)) {
766
- this._eventHandlers.set(event, /* @__PURE__ */ new Set());
767
- }
768
- this._eventHandlers.get(event).add(handler);
769
- return () => {
770
- const handlers = this._eventHandlers.get(event);
771
- if (handlers) {
772
- handlers.delete(handler);
773
- if (handlers.size === 0) {
774
- this._eventHandlers.delete(event);
775
- }
776
- }
777
- };
709
+ if (!NETWORK_NAME_RE.test(name)) {
710
+ throw new NetworkIdError(
711
+ `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
712
+ );
778
713
  }
779
- /**
780
- * Remove all event handlers for an event.
781
- *
782
- * @param event - Event name (if omitted, clears all events)
783
- */
784
- clearEventHandlers(event) {
785
- if (event) {
786
- this._eventHandlers.delete(event);
787
- } else {
788
- this._eventHandlers.clear();
789
- }
714
+ return { networkId, ownerDid, name };
715
+ }
716
+ function buildNetworkId(ownerDid, name) {
717
+ if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
718
+ throw new NetworkIdError("ownerDid must be a DID");
790
719
  }
791
- // ============================================================
792
- // Lifecycle
793
- // ============================================================
794
- /**
795
- * Get the abort signal for cancelling operations.
796
- */
797
- get abortSignal() {
798
- return this._abortController.signal;
720
+ if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
721
+ throw new NetworkIdError(
722
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
723
+ );
799
724
  }
800
- get operationAbortSignal() {
801
- return this._operationAbortSignal;
802
- }
803
- setOperationAbortSignal(signal) {
804
- this._operationAbortSignal = signal;
805
- }
806
- /**
807
- * Permanently retire this graph after its owner installs a replacement.
808
- * Unlike `abort()`, retirement never creates a fresh controller, so captured
809
- * services cannot resume calls under a superseded session.
810
- */
811
- retire() {
812
- if (this._retired) return;
813
- this._retired = true;
814
- this._session = null;
815
- this._abortController.abort();
816
- for (const service of this._services.values()) {
817
- try {
818
- service.onSignOut();
819
- } catch (error) {
820
- console.error("Error retiring service:", error);
821
- }
822
- }
823
- }
824
- /**
825
- * Abort all pending operations and notify services.
826
- * Creates a new AbortController for future operations.
827
- */
828
- abort() {
829
- if (this._retired) return;
830
- this._abortController.abort();
831
- this._abortController = new AbortController();
832
- for (const service of this._services.values()) {
833
- try {
834
- service.onSignOut();
835
- } catch (error) {
836
- console.error("Error signing out service:", error);
837
- }
838
- }
839
- }
840
- /**
841
- * Sign out - abort operations and clear session.
842
- */
843
- signOut() {
844
- if (this._retired) return;
845
- this.abort();
846
- this.setSession(null);
847
- this.emit("session.expired", {});
848
- }
849
- // ============================================================
850
- // Retry Policy
851
- // ============================================================
852
- /**
853
- * Get the retry policy configuration.
854
- */
855
- get retryPolicy() {
856
- return this._retryPolicy;
857
- }
858
- wrapInvoke(invoke) {
859
- return (session, service, path, action, facts) => {
860
- this.assertActive();
861
- if (!tinyCloudDebugLogger.isEnabled()) {
862
- return invoke(session, service, path, action, facts);
863
- }
864
- const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
865
- service,
866
- path,
867
- action,
868
- facts
869
- });
870
- try {
871
- const result = invoke(session, service, path, action, facts);
872
- timer.stop({ ok: true, service, path, action });
873
- return result;
874
- } catch (error) {
875
- timer.stop({ ok: false, service, path, action, error });
876
- throw error;
877
- }
878
- };
879
- }
880
- assertActive() {
881
- if (this._retired) {
882
- throw new Error("Service graph has been retired: its session was replaced or signed out.");
883
- }
884
- }
885
- wrapInvokeAny(invokeAny) {
886
- return (session, entries, facts) => {
887
- this.assertActive();
888
- if (!tinyCloudDebugLogger.isEnabled()) {
889
- return invokeAny(session, entries, facts);
890
- }
891
- const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
892
- entries,
893
- facts
894
- });
895
- try {
896
- const result = invokeAny(session, entries, facts);
897
- timer.stop({ ok: true, entries });
898
- return result;
899
- } catch (error) {
900
- timer.stop({ ok: false, entries, error });
901
- throw error;
902
- }
903
- };
904
- }
905
- wrapFetch(fetchFn) {
906
- return async (url, init) => {
907
- this.assertActive();
908
- const request = {
909
- ...init,
910
- signal: this.combineSignals(init?.signal)
911
- };
912
- if (!tinyCloudDebugLogger.isEnabled()) {
913
- return fetchFn(url, request);
914
- }
915
- const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", {
916
- url,
917
- method: request.method ?? "GET",
918
- init: request
919
- });
920
- try {
921
- const response = await fetchFn(url, request);
922
- timer.stop({
923
- ok: response.ok,
924
- url,
925
- method: request.method ?? "GET",
926
- status: response.status,
927
- statusText: response.statusText
928
- });
929
- return response;
930
- } catch (error) {
931
- timer.stop({
932
- ok: false,
933
- url,
934
- method: request.method ?? "GET",
935
- error
936
- });
937
- throw error;
938
- }
939
- };
940
- }
941
- combineSignals(signal) {
942
- if (!signal) return this._abortController.signal;
943
- const combined = new AbortController();
944
- const abort = (source) => combined.abort(source.reason);
945
- if (signal.aborted) {
946
- abort(signal);
947
- } else if (this._abortController.signal.aborted) {
948
- abort(this._abortController.signal);
949
- } else {
950
- signal.addEventListener("abort", () => abort(signal), { once: true });
951
- this._abortController.signal.addEventListener(
952
- "abort",
953
- () => abort(this._abortController.signal),
954
- { once: true }
955
- );
956
- }
957
- return combined.signal;
958
- }
959
- };
960
-
961
- // src/errors.ts
962
- import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
963
-
964
- // src/encryption/networkId.ts
965
- var URN_PREFIX = "urn:tinycloud:encryption:";
966
- var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
967
- var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
968
- var NetworkIdError = class extends Error {
969
- constructor(message) {
970
- super(message);
971
- this.name = "NetworkIdError";
972
- }
973
- };
974
- function parseNetworkId(networkId) {
975
- if (typeof networkId !== "string" || networkId.length === 0) {
976
- throw new NetworkIdError("networkId must be a non-empty string");
977
- }
978
- if (!networkId.startsWith(URN_PREFIX)) {
979
- throw new NetworkIdError(
980
- `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
981
- );
982
- }
983
- const body = networkId.slice(URN_PREFIX.length);
984
- const lastColon = body.lastIndexOf(":");
985
- if (lastColon <= 0 || lastColon === body.length - 1) {
986
- throw new NetworkIdError(
987
- `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
988
- );
989
- }
990
- const ownerDid = body.slice(0, lastColon);
991
- const name = body.slice(lastColon + 1);
992
- if (!ownerDid.startsWith("did:")) {
993
- throw new NetworkIdError(
994
- `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
995
- );
996
- }
997
- const didParts = ownerDid.split(":");
998
- if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
999
- throw new NetworkIdError(
1000
- `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
1001
- );
1002
- }
1003
- if (!NETWORK_NAME_RE.test(name)) {
1004
- throw new NetworkIdError(
1005
- `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1006
- );
1007
- }
1008
- return { networkId, ownerDid, name };
1009
- }
1010
- function buildNetworkId(ownerDid, name) {
1011
- if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
1012
- throw new NetworkIdError("ownerDid must be a DID");
1013
- }
1014
- if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
1015
- throw new NetworkIdError(
1016
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1017
- );
1018
- }
1019
- const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
1020
- parseNetworkId(networkId);
1021
- return networkId;
1022
- }
1023
- function isNetworkId(networkId) {
1024
- if (typeof networkId !== "string") {
1025
- return false;
725
+ const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
726
+ parseNetworkId(networkId);
727
+ return networkId;
728
+ }
729
+ function isNetworkId(networkId) {
730
+ if (typeof networkId !== "string") {
731
+ return false;
1026
732
  }
1027
733
  try {
1028
734
  parseNetworkId(networkId);
@@ -1117,6 +823,30 @@ function networkError(service, message, cause) {
1117
823
  cause
1118
824
  };
1119
825
  }
826
+ function nodeRetryError(service, message, response, errorText) {
827
+ const body = errorText.trim();
828
+ const [, token, stage] = /^([a-z_]+)(?::\s*([a-z_]+):)?/.exec(body) ?? [];
829
+ let code;
830
+ if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable" || token === "read_audit_busy")) {
831
+ code = ErrorCodes.NODE_BUSY;
832
+ } else if (response.status === 503 && token === "request_timeout") {
833
+ code = ErrorCodes.REQUEST_TIMEOUT;
834
+ } else if (response.status === 500 && token === "outcome_unknown") {
835
+ code = ErrorCodes.OUTCOME_UNKNOWN;
836
+ } else {
837
+ return void 0;
838
+ }
839
+ const retryable = code !== ErrorCodes.OUTCOME_UNKNOWN;
840
+ const retryAfter = response.headers.get("retry-after")?.trim();
841
+ return serviceError(code, message, service, {
842
+ meta: {
843
+ ...responseErrorMeta(response.status, response.statusText, errorText),
844
+ retryable,
845
+ ...token === "sql_busy" && stage ? { stage } : {},
846
+ ...retryable && retryAfter && /^\d{1,9}$/.test(retryAfter) ? { retryAfterMs: Number(retryAfter) * 1e3 } : {}
847
+ }
848
+ });
849
+ }
1120
850
  var RequestTimeoutError = class extends Error {
1121
851
  constructor(timeoutMs) {
1122
852
  super(`Request timed out after ${timeoutMs}ms.`);
@@ -1356,15 +1086,401 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1356
1086
  cause: error
1357
1087
  };
1358
1088
  }
1359
- return {
1360
- code: defaultCode,
1361
- message: String(error),
1362
- service
1363
- };
1364
- }
1365
- function errorResult(error) {
1366
- return err(error);
1367
- }
1089
+ return {
1090
+ code: defaultCode,
1091
+ message: String(error),
1092
+ service
1093
+ };
1094
+ }
1095
+ function errorResult(error) {
1096
+ return err(error);
1097
+ }
1098
+
1099
+ // src/sleep.ts
1100
+ var abortableSleep = (ms, signal, unref = false) => new Promise((resolve) => {
1101
+ const done = () => {
1102
+ clearTimeout(timer);
1103
+ signal.removeEventListener("abort", done);
1104
+ resolve();
1105
+ };
1106
+ const timer = setTimeout(done, ms);
1107
+ if (unref) timer.unref?.();
1108
+ signal.aborted ? done() : signal.addEventListener("abort", done);
1109
+ });
1110
+
1111
+ // src/context.ts
1112
+ var ServiceContext = class {
1113
+ constructor(config) {
1114
+ this._session = null;
1115
+ this._services = /* @__PURE__ */ new Map();
1116
+ this._eventHandlers = /* @__PURE__ */ new Map();
1117
+ this._abortController = new AbortController();
1118
+ this._retired = false;
1119
+ this._invoke = this.wrapInvoke(config.invoke);
1120
+ this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
1121
+ this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
1122
+ this._hosts = config.hosts;
1123
+ this._session = config.session ?? null;
1124
+ this._retryPolicy = {
1125
+ ...defaultRetryPolicy,
1126
+ ...config.retryPolicy
1127
+ };
1128
+ this._telemetryEnabled = typeof config.telemetry === "boolean" ? config.telemetry : config.telemetry?.enabled === true;
1129
+ this._telemetryHandler = typeof config.telemetry === "object" ? config.telemetry.onEvent : void 0;
1130
+ }
1131
+ // ============================================================
1132
+ // Session Management
1133
+ // ============================================================
1134
+ /**
1135
+ * Get the current session.
1136
+ */
1137
+ get session() {
1138
+ return this._retired ? null : this._session;
1139
+ }
1140
+ /**
1141
+ * Check if the context has an authenticated session.
1142
+ */
1143
+ get isAuthenticated() {
1144
+ return !this._retired && this._session !== null;
1145
+ }
1146
+ /**
1147
+ * Update the session and notify all registered services.
1148
+ *
1149
+ * @param session - New session or null to clear
1150
+ */
1151
+ setSession(session) {
1152
+ this.assertActive();
1153
+ this._session = session;
1154
+ this.emit("session.changed", { authenticated: session !== null });
1155
+ for (const service of this._services.values()) {
1156
+ service.onSessionChange(session);
1157
+ }
1158
+ }
1159
+ // ============================================================
1160
+ // Platform Dependencies
1161
+ // ============================================================
1162
+ /**
1163
+ * Get the invoke function for WASM operations.
1164
+ */
1165
+ get invoke() {
1166
+ this.assertActive();
1167
+ return this._invoke;
1168
+ }
1169
+ /**
1170
+ * Get the multi-resource invoke function when available.
1171
+ */
1172
+ get invokeAny() {
1173
+ this.assertActive();
1174
+ return this._invokeAny;
1175
+ }
1176
+ /**
1177
+ * Get the fetch function for HTTP requests.
1178
+ */
1179
+ get fetch() {
1180
+ this.assertActive();
1181
+ return this._fetch;
1182
+ }
1183
+ /**
1184
+ * Get the list of TinyCloud host URLs.
1185
+ */
1186
+ get hosts() {
1187
+ this.assertActive();
1188
+ return this._hosts;
1189
+ }
1190
+ // ============================================================
1191
+ // Service Registry
1192
+ // ============================================================
1193
+ /**
1194
+ * Register a service with the context.
1195
+ *
1196
+ * @param name - Service name (e.g., 'kv')
1197
+ * @param service - Service instance
1198
+ */
1199
+ registerService(name, service) {
1200
+ this._services.set(name, service);
1201
+ }
1202
+ /**
1203
+ * Unregister a service from the context.
1204
+ *
1205
+ * @param name - Service name to remove
1206
+ */
1207
+ unregisterService(name) {
1208
+ this._services.delete(name);
1209
+ }
1210
+ /**
1211
+ * Get a registered service by name.
1212
+ *
1213
+ * @param name - Service name
1214
+ * @returns The service instance or undefined if not registered
1215
+ */
1216
+ getService(name) {
1217
+ return this._services.get(name);
1218
+ }
1219
+ // ============================================================
1220
+ // Event System (Telemetry)
1221
+ // ============================================================
1222
+ /**
1223
+ * Emit a telemetry event.
1224
+ *
1225
+ * @param event - Event name
1226
+ * @param data - Event data
1227
+ */
1228
+ emit(event, data) {
1229
+ tinyCloudDebugLogger.log(event, data);
1230
+ if (this._telemetryEnabled && this._telemetryHandler) {
1231
+ try {
1232
+ this._telemetryHandler(event, projectDiagnosticData(data));
1233
+ } catch (error) {
1234
+ console.error(`Error in telemetry handler for "${event}":`, error);
1235
+ }
1236
+ }
1237
+ const handlers = this._eventHandlers.get(event);
1238
+ if (handlers) {
1239
+ for (const handler of handlers) {
1240
+ try {
1241
+ handler(data);
1242
+ } catch (error) {
1243
+ console.error(`Error in event handler for "${event}":`, error);
1244
+ }
1245
+ }
1246
+ }
1247
+ }
1248
+ /**
1249
+ * Subscribe to telemetry events.
1250
+ *
1251
+ * @param event - Event name to subscribe to
1252
+ * @param handler - Handler function
1253
+ * @returns Unsubscribe function
1254
+ */
1255
+ on(event, handler) {
1256
+ if (!this._eventHandlers.has(event)) {
1257
+ this._eventHandlers.set(event, /* @__PURE__ */ new Set());
1258
+ }
1259
+ this._eventHandlers.get(event).add(handler);
1260
+ return () => {
1261
+ const handlers = this._eventHandlers.get(event);
1262
+ if (handlers) {
1263
+ handlers.delete(handler);
1264
+ if (handlers.size === 0) {
1265
+ this._eventHandlers.delete(event);
1266
+ }
1267
+ }
1268
+ };
1269
+ }
1270
+ /**
1271
+ * Remove all event handlers for an event.
1272
+ *
1273
+ * @param event - Event name (if omitted, clears all events)
1274
+ */
1275
+ clearEventHandlers(event) {
1276
+ if (event) {
1277
+ this._eventHandlers.delete(event);
1278
+ } else {
1279
+ this._eventHandlers.clear();
1280
+ }
1281
+ }
1282
+ // ============================================================
1283
+ // Lifecycle
1284
+ // ============================================================
1285
+ /**
1286
+ * Get the abort signal for cancelling operations.
1287
+ */
1288
+ get abortSignal() {
1289
+ return this._abortController.signal;
1290
+ }
1291
+ get operationAbortSignal() {
1292
+ return this._operationAbortSignal;
1293
+ }
1294
+ setOperationAbortSignal(signal) {
1295
+ this._operationAbortSignal = signal;
1296
+ }
1297
+ /**
1298
+ * Permanently retire this graph after its owner installs a replacement.
1299
+ * Unlike `abort()`, retirement never creates a fresh controller, so captured
1300
+ * services cannot resume calls under a superseded session.
1301
+ */
1302
+ retire() {
1303
+ if (this._retired) return;
1304
+ this._retired = true;
1305
+ this._session = null;
1306
+ this._abortController.abort();
1307
+ for (const service of this._services.values()) {
1308
+ try {
1309
+ service.onSignOut();
1310
+ } catch (error) {
1311
+ console.error("Error retiring service:", error);
1312
+ }
1313
+ }
1314
+ }
1315
+ /**
1316
+ * Abort all pending operations and notify services.
1317
+ * Creates a new AbortController for future operations.
1318
+ */
1319
+ abort() {
1320
+ if (this._retired) return;
1321
+ this._abortController.abort();
1322
+ this._abortController = new AbortController();
1323
+ for (const service of this._services.values()) {
1324
+ try {
1325
+ service.onSignOut();
1326
+ } catch (error) {
1327
+ console.error("Error signing out service:", error);
1328
+ }
1329
+ }
1330
+ }
1331
+ /**
1332
+ * Sign out - abort operations and clear session.
1333
+ */
1334
+ signOut() {
1335
+ if (this._retired) return;
1336
+ this.abort();
1337
+ this.setSession(null);
1338
+ this.emit("session.expired", {});
1339
+ }
1340
+ // ============================================================
1341
+ // Retry Policy
1342
+ // ============================================================
1343
+ /**
1344
+ * Get the retry policy configuration.
1345
+ */
1346
+ get retryPolicy() {
1347
+ return this._retryPolicy;
1348
+ }
1349
+ wrapInvoke(invoke) {
1350
+ return (session, service, path, action, facts) => {
1351
+ this.assertActive();
1352
+ if (!tinyCloudDebugLogger.isEnabled()) {
1353
+ return invoke(session, service, path, action, facts);
1354
+ }
1355
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
1356
+ service,
1357
+ path,
1358
+ action,
1359
+ facts
1360
+ });
1361
+ try {
1362
+ const result = invoke(session, service, path, action, facts);
1363
+ timer.stop({ ok: true, service, path, action });
1364
+ return result;
1365
+ } catch (error) {
1366
+ timer.stop({ ok: false, service, path, action, error });
1367
+ throw error;
1368
+ }
1369
+ };
1370
+ }
1371
+ assertActive() {
1372
+ if (this._retired) {
1373
+ throw new Error("Service graph has been retired: its session was replaced or signed out.");
1374
+ }
1375
+ }
1376
+ wrapInvokeAny(invokeAny) {
1377
+ return (session, entries, facts) => {
1378
+ this.assertActive();
1379
+ if (!tinyCloudDebugLogger.isEnabled()) {
1380
+ return invokeAny(session, entries, facts);
1381
+ }
1382
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
1383
+ entries,
1384
+ facts
1385
+ });
1386
+ try {
1387
+ const result = invokeAny(session, entries, facts);
1388
+ timer.stop({ ok: true, entries });
1389
+ return result;
1390
+ } catch (error) {
1391
+ timer.stop({ ok: false, entries, error });
1392
+ throw error;
1393
+ }
1394
+ };
1395
+ }
1396
+ /** Retry `NODE_BUSY` / `REQUEST_TIMEOUT`; `build` must re-sign per attempt (the node 409s a repeated invocation). */
1397
+ async invokeWithRetry(url, build) {
1398
+ this.assertActive();
1399
+ const policy = this._retryPolicy;
1400
+ const startedAt = Date.now();
1401
+ let backoffMs = 0;
1402
+ let signal;
1403
+ for (let attempt = 1; ; attempt++) {
1404
+ const init = await build();
1405
+ signal ?? (signal = this.combineSignals(init.signal));
1406
+ const response = await this._fetch(url, init);
1407
+ const done = () => {
1408
+ try {
1409
+ response.retry = { attempts: attempt, backoffMs };
1410
+ } catch {
1411
+ }
1412
+ return response;
1413
+ };
1414
+ if (attempt >= policy.maxAttempts || response.status !== 503 || !response.clone) return done();
1415
+ const text = await response.clone().text().catch(() => "");
1416
+ const error = nodeRetryError("", "", response, text);
1417
+ if (!error || error.meta?.retryable !== true || !policy.retryableErrors.includes(error.code)) {
1418
+ return done();
1419
+ }
1420
+ const remaining = (policy.maxTotalMs ?? Infinity) - (Date.now() - startedAt);
1421
+ if (remaining <= 0) return done();
1422
+ const retryAfterMs = error.meta.retryAfterMs;
1423
+ const step = Math.min(
1424
+ policy.maxDelayMs,
1425
+ policy.backoff === "exponential" ? policy.baseDelayMs * 2 ** (attempt - 1) : policy.backoff === "linear" ? policy.baseDelayMs * attempt : 0
1426
+ );
1427
+ const delay = Math.min(
1428
+ typeof retryAfterMs === "number" ? retryAfterMs : Math.random() * step,
1429
+ remaining
1430
+ );
1431
+ response.body?.cancel?.()?.catch?.(() => {
1432
+ });
1433
+ await abortableSleep(delay, signal);
1434
+ if (signal.aborted) throw signal.reason;
1435
+ backoffMs += delay;
1436
+ }
1437
+ }
1438
+ wrapFetch(fetchFn) {
1439
+ return async (url, init) => {
1440
+ this.assertActive();
1441
+ const request = {
1442
+ ...init,
1443
+ signal: this.combineSignals(init?.signal)
1444
+ };
1445
+ if (!tinyCloudDebugLogger.isEnabled()) {
1446
+ return fetchFn(url, request);
1447
+ }
1448
+ const meta = { url, method: request.method ?? "GET" };
1449
+ const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", { ...meta, init: request });
1450
+ try {
1451
+ const response = await fetchFn(url, request);
1452
+ timer.stop({
1453
+ ...meta,
1454
+ ok: response.ok,
1455
+ status: response.status,
1456
+ statusText: response.statusText
1457
+ });
1458
+ return response;
1459
+ } catch (error) {
1460
+ timer.stop({ ...meta, ok: false, error });
1461
+ throw error;
1462
+ }
1463
+ };
1464
+ }
1465
+ combineSignals(signal) {
1466
+ if (!signal) return this._abortController.signal;
1467
+ const combined = new AbortController();
1468
+ const abort = (source) => combined.abort(source.reason);
1469
+ if (signal.aborted) {
1470
+ abort(signal);
1471
+ } else if (this._abortController.signal.aborted) {
1472
+ abort(this._abortController.signal);
1473
+ } else {
1474
+ signal.addEventListener("abort", () => abort(signal), { once: true });
1475
+ this._abortController.signal.addEventListener(
1476
+ "abort",
1477
+ () => abort(this._abortController.signal),
1478
+ { once: true }
1479
+ );
1480
+ }
1481
+ return combined.signal;
1482
+ }
1483
+ };
1368
1484
 
1369
1485
  // src/base/BaseService.ts
1370
1486
  var MAX_TIMER_DELAY_MS = 2147483647;
@@ -1548,6 +1664,18 @@ var BaseService = class {
1548
1664
  }
1549
1665
  return controller.signal;
1550
1666
  }
1667
+ /** POST a signed invocation with retry; `mint` re-signs per attempt, `init.headers` add to the signed ones. */
1668
+ invokeWithRetry(url, mint, { headers: extra, ...init } = {}) {
1669
+ const build = () => {
1670
+ const signed = mint();
1671
+ return {
1672
+ method: "POST",
1673
+ ...init,
1674
+ headers: Array.isArray(signed) ? [...signed, ...Object.entries(extra ?? {})] : { ...signed, ...extra }
1675
+ };
1676
+ };
1677
+ return this.context.invokeWithRetry ? this.context.invokeWithRetry(url, build) : this.context.fetch(url, build());
1678
+ }
1551
1679
  /**
1552
1680
  * Create the abort signal for one request, bounded by the request timeout.
1553
1681
  *
@@ -1915,15 +2043,6 @@ var KVService = class extends BaseService {
1915
2043
  get host() {
1916
2044
  return this.context.hosts[0];
1917
2045
  }
1918
- withJsonContentType(headers) {
1919
- if (Array.isArray(headers)) {
1920
- return [...headers, ["content-type", "application/json"]];
1921
- }
1922
- return {
1923
- ...headers,
1924
- "content-type": "application/json"
1925
- };
1926
- }
1927
2046
  /**
1928
2047
  * Execute an invoke operation.
1929
2048
  *
@@ -1934,20 +2053,11 @@ var KVService = class extends BaseService {
1934
2053
  * @returns Fetch response
1935
2054
  */
1936
2055
  async invokeOperation(path, action, body, signal, extraHeaders) {
1937
- const session = this.context.session;
1938
- const headers = this.context.invoke(
1939
- session,
1940
- "kv",
1941
- path,
1942
- action
2056
+ return this.invokeWithRetry(
2057
+ `${this.host}/invoke`,
2058
+ () => this.context.invoke(this.context.session, "kv", path, action),
2059
+ { headers: extraHeaders, body, signal }
1943
2060
  );
1944
- const requestHeaders = Array.isArray(headers) ? [...headers, ...Object.entries(extraHeaders ?? {})] : { ...headers, ...extraHeaders };
1945
- return this.context.fetch(`${this.host}/invoke`, {
1946
- method: "POST",
1947
- headers: requestHeaders,
1948
- body,
1949
- signal
1950
- });
1951
2061
  }
1952
2062
  /**
1953
2063
  * Serialize a single put value into a fetch body.
@@ -2153,28 +2263,28 @@ var KVService = class extends BaseService {
2153
2263
  }
2154
2264
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2155
2265
  try {
2156
- const session = this.context.session;
2157
- const invocationHeaders = this.context.invokeAny(
2158
- session,
2159
- paths.map((path) => ({
2160
- spaceId: session.spaceId,
2161
- service: "kv",
2162
- path,
2163
- action
2164
- }))
2165
- );
2166
2266
  const limitHeaders = {};
2167
2267
  if (getOptions?.maxResponseBytes !== void 0) {
2168
2268
  limitHeaders["x-tinycloud-max-response-bytes"] = String(
2169
2269
  getOptions.maxResponseBytes
2170
2270
  );
2171
2271
  }
2172
- const headers = Array.isArray(invocationHeaders) ? [...invocationHeaders, ...Object.entries(limitHeaders)] : { ...invocationHeaders, ...limitHeaders };
2173
- const response = await this.context.fetch(`${this.host}/invoke`, {
2174
- method: "POST",
2175
- headers,
2176
- signal: request.signal
2177
- });
2272
+ const response = await this.invokeWithRetry(
2273
+ `${this.host}/invoke`,
2274
+ () => {
2275
+ const session = this.context.session;
2276
+ return this.context.invokeAny(
2277
+ session,
2278
+ paths.map((path) => ({
2279
+ spaceId: session.spaceId,
2280
+ service: "kv",
2281
+ path,
2282
+ action
2283
+ }))
2284
+ );
2285
+ },
2286
+ { headers: limitHeaders, signal: request.signal }
2287
+ );
2178
2288
  if (!response.ok) {
2179
2289
  const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
2180
2290
  if (response.status === 401 || response.status === 403) {
@@ -2220,10 +2330,10 @@ var KVService = class extends BaseService {
2220
2330
  result: err(serviceError(item.error.code, item.error.message, "kv"))
2221
2331
  };
2222
2332
  }
2223
- const headers2 = item.headers;
2333
+ const headers = item.headers;
2224
2334
  const data = action === KVAction.HEAD ? void 0 : this.parseBatchValue(
2225
2335
  item.dataBase64,
2226
- headers2,
2336
+ headers,
2227
2337
  getOptions?.raw,
2228
2338
  getOptions?.binary
2229
2339
  );
@@ -2231,7 +2341,7 @@ var KVService = class extends BaseService {
2231
2341
  key: keys[index],
2232
2342
  result: ok({
2233
2343
  data,
2234
- headers: this.createBatchResponseHeaders(headers2)
2344
+ headers: this.createBatchResponseHeaders(headers)
2235
2345
  })
2236
2346
  };
2237
2347
  });
@@ -2455,15 +2565,18 @@ var KVService = class extends BaseService {
2455
2565
  { meta: { status: response.status, statusText: response.statusText } }
2456
2566
  ));
2457
2567
  }
2568
+ const message = `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
2569
+ const retryError = nodeRetryError("kv", message, response, errorText);
2570
+ if (retryError) return err(retryError);
2458
2571
  return err(serviceError(
2459
2572
  ErrorCodes.NETWORK_ERROR,
2460
- `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2573
+ message,
2461
2574
  "kv",
2462
2575
  { meta: { status: response.status, statusText: response.statusText } }
2463
2576
  ));
2464
2577
  }
2465
2578
  const data = await this.parseResponse(response, options?.raw, options?.binary);
2466
- return ok({ data, headers: this.createResponseHeaders(response.headers) });
2579
+ return ok({ data, headers: this.createResponseHeaders(response.headers), ...response.retry && { retry: response.retry } });
2467
2580
  }
2468
2581
  async batchGet(keys, options) {
2469
2582
  return this.withTelemetry(
@@ -2539,26 +2652,22 @@ var KVService = class extends BaseService {
2539
2652
  { meta: { status: response.status, statusText: response.statusText } }
2540
2653
  ));
2541
2654
  }
2542
- if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
2543
- return err(serviceError(
2544
- ErrorCodes.KV_CONFLICT,
2545
- `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
2546
- "kv",
2547
- { meta: { status: response.status, statusText: response.statusText } }
2548
- ));
2549
- }
2655
+ const message = `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
2656
+ const retryError = nodeRetryError("kv", message, response, errorText);
2657
+ if (retryError) return err(retryError);
2550
2658
  const quotaError = this.handleQuotaErrorResponse(response, errorText, key);
2551
2659
  if (quotaError) return quotaError;
2552
2660
  return err(serviceError(
2553
2661
  ErrorCodes.KV_WRITE_FAILED,
2554
- `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2662
+ message,
2555
2663
  "kv",
2556
2664
  { meta: { status: response.status, statusText: response.statusText } }
2557
2665
  ));
2558
2666
  }
2559
2667
  return ok({
2560
2668
  data: void 0,
2561
- headers: this.createResponseHeaders(response.headers)
2669
+ headers: this.createResponseHeaders(response.headers),
2670
+ ...response.retry && { retry: response.retry }
2562
2671
  });
2563
2672
  }
2564
2673
  /**
@@ -2631,25 +2740,23 @@ var KVService = class extends BaseService {
2631
2740
  entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
2632
2741
  );
2633
2742
  }
2634
- const headers = this.context.invokeAny(
2635
- session,
2636
- paths.map((path) => ({
2637
- spaceId: session.spaceId,
2638
- service: "kv",
2639
- path,
2640
- action: KVAction.PUT
2641
- }))
2743
+ const response = await this.invokeWithRetry(
2744
+ `${this.host}/invoke`,
2745
+ () => {
2746
+ const headers = this.context.invokeAny(
2747
+ session,
2748
+ paths.map((path) => ({
2749
+ spaceId: session.spaceId,
2750
+ service: "kv",
2751
+ path,
2752
+ action: KVAction.PUT
2753
+ }))
2754
+ );
2755
+ requestMayHaveDispatched = true;
2756
+ return headers;
2757
+ },
2758
+ { body, signal }
2642
2759
  );
2643
- const url = `${this.host}/invoke`;
2644
- const init = {
2645
- method: "POST",
2646
- headers,
2647
- body,
2648
- signal
2649
- };
2650
- const fetchFn = this.context.fetch;
2651
- requestMayHaveDispatched = true;
2652
- const response = await fetchFn(url, init);
2653
2760
  if (!response.ok) {
2654
2761
  let errorText;
2655
2762
  try {
@@ -2851,9 +2958,12 @@ var KVService = class extends BaseService {
2851
2958
  );
2852
2959
  }
2853
2960
  const errorText = await response.text();
2961
+ const message = `Failed to list keys: ${response.status} - ${errorText}`;
2962
+ const retryError = nodeRetryError("kv", message, response, errorText);
2963
+ if (retryError) return err(retryError);
2854
2964
  return err(serviceError(
2855
2965
  ErrorCodes.NETWORK_ERROR,
2856
- `Failed to list keys: ${response.status} - ${errorText}`,
2966
+ message,
2857
2967
  "kv",
2858
2968
  { meta: { status: response.status, statusText: response.statusText } }
2859
2969
  ));
@@ -3104,24 +3214,20 @@ var KVService = class extends BaseService {
3104
3214
  { meta: { status: response.status, statusText: response.statusText } }
3105
3215
  ));
3106
3216
  }
3107
- if (response.status === 503 && options?.ifMatch !== void 0) {
3108
- return err(serviceError(
3109
- ErrorCodes.KV_CONFLICT,
3110
- `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
3111
- "kv",
3112
- { meta: { status: response.status, statusText: response.statusText } }
3113
- ));
3114
- }
3217
+ const message = `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
3218
+ const retryError = nodeRetryError("kv", message, response, errorText);
3219
+ if (retryError) return err(retryError);
3115
3220
  return err(serviceError(
3116
3221
  ErrorCodes.NETWORK_ERROR,
3117
- `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
3222
+ message,
3118
3223
  "kv",
3119
3224
  { meta: { status: response.status, statusText: response.statusText } }
3120
3225
  ));
3121
3226
  }
3122
3227
  return ok({
3123
3228
  data: void 0,
3124
- headers: this.createResponseHeaders(response.headers)
3229
+ headers: this.createResponseHeaders(response.headers),
3230
+ ...response.retry && { retry: response.retry }
3125
3231
  });
3126
3232
  }
3127
3233
  /**
@@ -3155,10 +3261,13 @@ var KVService = class extends BaseService {
3155
3261
  return await this.classifyNotFound(response, key);
3156
3262
  }
3157
3263
  const errorText = await response.text();
3264
+ const message = `Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
3265
+ const retryError = nodeRetryError("kv", message, response, errorText);
3266
+ if (retryError) return err(retryError);
3158
3267
  return err(
3159
3268
  serviceError(
3160
3269
  ErrorCodes.NETWORK_ERROR,
3161
- `Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
3270
+ message,
3162
3271
  "kv",
3163
3272
  { meta: { status: response.status, statusText: response.statusText } }
3164
3273
  )
@@ -3192,12 +3301,6 @@ var KVService = class extends BaseService {
3192
3301
  }
3193
3302
  const path = this.getFullPath(key, options?.prefix);
3194
3303
  const session = this.context.session;
3195
- const headers = this.context.invoke(
3196
- session,
3197
- "kv",
3198
- path,
3199
- KVAction.GET
3200
- );
3201
3304
  const body = {
3202
3305
  space: session.spaceId,
3203
3306
  path,
@@ -3211,12 +3314,15 @@ var KVService = class extends BaseService {
3211
3314
  }
3212
3315
  const request = this.createRequestSignal(options?.signal, options?.timeout);
3213
3316
  try {
3214
- const response = await this.context.fetch(`${this.host}/signed/kv`, {
3215
- method: "POST",
3216
- headers: this.withJsonContentType(headers),
3217
- body: JSON.stringify(body),
3218
- signal: request.signal
3219
- });
3317
+ const response = await this.invokeWithRetry(
3318
+ `${this.host}/signed/kv`,
3319
+ () => this.context.invoke(session, "kv", path, KVAction.GET),
3320
+ {
3321
+ headers: { "content-type": "application/json" },
3322
+ body: JSON.stringify(body),
3323
+ signal: request.signal
3324
+ }
3325
+ );
3220
3326
  if (!response.ok) {
3221
3327
  return await this.createSignedReadUrlError(response, key, path);
3222
3328
  }
@@ -3350,42 +3456,6 @@ function requiresSecretsOptIn(space, prefix) {
3350
3456
  return prefix.split("/", 1)[0] === "vault";
3351
3457
  }
3352
3458
 
3353
- // src/responseErrors.ts
3354
- function parseServiceErrorBody(errorText) {
3355
- try {
3356
- return JSON.parse(errorText);
3357
- } catch {
3358
- return {};
3359
- }
3360
- }
3361
- function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
3362
- if (parsed.message) {
3363
- return compactMessage(parsed.message);
3364
- }
3365
- if (looksLikeHtml(errorText)) {
3366
- if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
3367
- return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
3368
- }
3369
- return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
3370
- }
3371
- return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
3372
- }
3373
- function responseErrorMeta(status, statusText, errorText) {
3374
- const meta = { status, statusText };
3375
- const snippet = compactMessage(errorText);
3376
- if (snippet) {
3377
- meta.responseSnippet = snippet.slice(0, 300);
3378
- }
3379
- return meta;
3380
- }
3381
- function looksLikeHtml(text) {
3382
- return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
3383
- }
3384
- function compactMessage(message) {
3385
- const compact = message.replace(/\s+/g, " ").trim();
3386
- return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
3387
- }
3388
-
3389
3459
  // src/sql/SQLMigrations.ts
3390
3460
  var SQLMigrations = class {
3391
3461
  constructor(service, dbName) {
@@ -3749,18 +3819,19 @@ var SQLService = class extends BaseService {
3749
3819
  }
3750
3820
  // === Private helpers ===
3751
3821
  async invokeSQL(dbName, actions, body, signal) {
3752
- const session = this.context.session;
3753
3822
  const actionList = Array.isArray(actions) ? actions : [actions];
3754
- const headers = actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
3755
- return this.context.fetch(`${this.host}/invoke`, {
3756
- method: "POST",
3757
- headers: {
3758
- ...headers,
3759
- "Content-Type": "application/json"
3823
+ return this.invokeWithRetry(
3824
+ `${this.host}/invoke`,
3825
+ () => {
3826
+ const session = this.context.session;
3827
+ return actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
3760
3828
  },
3761
- body: JSON.stringify(body),
3762
- signal
3763
- });
3829
+ {
3830
+ headers: { "Content-Type": "application/json" },
3831
+ body: JSON.stringify(body),
3832
+ signal
3833
+ }
3834
+ );
3764
3835
  }
3765
3836
  actionForSql(sql, fallback) {
3766
3837
  const token = firstSqlToken(sql);
@@ -3801,6 +3872,8 @@ var SQLService = class extends BaseService {
3801
3872
  if (response.status === 413 && parseStorageRejection(errorText)) {
3802
3873
  return err(storageRejectionError("sql", 413, meta, errorText));
3803
3874
  }
3875
+ const retryError = nodeRetryError("sql", `SQL ${operation} failed: ${response.status} - ${errorText}`, response, errorText);
3876
+ if (retryError) return err(retryError);
3804
3877
  const errorBody = parseServiceErrorBody(errorText);
3805
3878
  const errorCode = this.mapHttpStatusToErrorCode(
3806
3879
  response.status,
@@ -4219,22 +4292,20 @@ var DuckDbService = class extends BaseService {
4219
4292
  }
4220
4293
  const request = this.createRequestSignal(options?.signal);
4221
4294
  try {
4222
- const session = this.context.session;
4223
- const headers = this.context.invoke(
4224
- session,
4225
- "duckdb",
4226
- dbName,
4227
- DuckDbAction.IMPORT
4295
+ const response = await this.invokeWithRetry(
4296
+ `${this.host}/invoke`,
4297
+ () => this.context.invoke(
4298
+ this.context.session,
4299
+ "duckdb",
4300
+ dbName,
4301
+ DuckDbAction.IMPORT
4302
+ ),
4303
+ {
4304
+ headers: { "Content-Type": "application/x-duckdb" },
4305
+ body: new Blob([data]),
4306
+ signal: request.signal
4307
+ }
4228
4308
  );
4229
- const response = await this.context.fetch(`${this.host}/invoke`, {
4230
- method: "POST",
4231
- headers: {
4232
- ...headers,
4233
- "Content-Type": "application/x-duckdb"
4234
- },
4235
- body: new Blob([data]),
4236
- signal: request.signal
4237
- });
4238
4309
  if (!response.ok) {
4239
4310
  return await this.handleErrorResponse(response, "import");
4240
4311
  }
@@ -4248,18 +4319,15 @@ var DuckDbService = class extends BaseService {
4248
4319
  }
4249
4320
  // === Private helpers ===
4250
4321
  async invokeDuckDb(dbName, action, body, signal, extraHeaders) {
4251
- const session = this.context.session;
4252
- const headers = this.context.invoke(session, "duckdb", dbName, action);
4253
- return this.context.fetch(`${this.host}/invoke`, {
4254
- method: "POST",
4255
- headers: {
4256
- ...headers,
4257
- "Content-Type": "application/json",
4258
- ...extraHeaders
4259
- },
4260
- body: JSON.stringify(body),
4261
- signal
4262
- });
4322
+ return this.invokeWithRetry(
4323
+ `${this.host}/invoke`,
4324
+ () => this.context.invoke(this.context.session, "duckdb", dbName, action),
4325
+ {
4326
+ headers: { "Content-Type": "application/json", ...extraHeaders },
4327
+ body: JSON.stringify(body),
4328
+ signal
4329
+ }
4330
+ );
4263
4331
  }
4264
4332
  async handleErrorResponse(response, operation) {
4265
4333
  const errorText = await response.text();
@@ -4270,6 +4338,8 @@ var DuckDbService = class extends BaseService {
4270
4338
  if (response.status === 413 && parseStorageRejection(errorText)) {
4271
4339
  return err(storageRejectionError("duckdb", 413, meta, errorText));
4272
4340
  }
4341
+ const retryError = nodeRetryError("duckdb", `DuckDB ${operation} failed: ${response.status} - ${errorText}`, response, errorText);
4342
+ if (retryError) return err(retryError);
4273
4343
  const errorBody = parseServiceErrorBody(errorText);
4274
4344
  const errorCode = this.mapHttpStatusToErrorCode(
4275
4345
  response.status,
@@ -4446,26 +4516,24 @@ var HooksService = class extends BaseService {
4446
4516
  );
4447
4517
  }
4448
4518
  try {
4449
- const response = await this.context.fetch(`${this.host}/hooks/webhooks`, {
4450
- method: "POST",
4451
- headers: {
4452
- ...serviceHeadersToRecord(
4453
- this.createHookHeaders(
4454
- "tinycloud.hooks/register",
4455
- buildScopePath(webhook.service, webhook.pathPrefix)
4456
- )
4457
- ),
4458
- "content-type": "application/json"
4459
- },
4460
- body: JSON.stringify({
4461
- space: webhook.space,
4462
- service: webhook.service,
4463
- pathPrefix: normalizePathPrefix(webhook.pathPrefix),
4464
- abilities: webhook.abilities ?? [],
4465
- callbackUrl: webhook.callbackUrl,
4466
- secret: webhook.secret
4467
- })
4468
- });
4519
+ const response = await this.invokeWithRetry(
4520
+ `${this.host}/hooks/webhooks`,
4521
+ () => this.createHookHeaders(
4522
+ "tinycloud.hooks/register",
4523
+ buildScopePath(webhook.service, webhook.pathPrefix)
4524
+ ),
4525
+ {
4526
+ headers: { "content-type": "application/json" },
4527
+ body: JSON.stringify({
4528
+ space: webhook.space,
4529
+ service: webhook.service,
4530
+ pathPrefix: normalizePathPrefix(webhook.pathPrefix),
4531
+ abilities: webhook.abilities ?? [],
4532
+ callbackUrl: webhook.callbackUrl,
4533
+ secret: webhook.secret
4534
+ })
4535
+ }
4536
+ );
4469
4537
  if (!response.ok) {
4470
4538
  return err(
4471
4539
  await responseError("hooks", "failed to register webhook", response)
@@ -4503,17 +4571,13 @@ var HooksService = class extends BaseService {
4503
4571
  query.set("prefix", normalizedPrefix);
4504
4572
  }
4505
4573
  }
4506
- const response = await this.context.fetch(
4574
+ const response = await this.invokeWithRetry(
4507
4575
  `${this.host}/hooks/webhooks${query.size > 0 ? `?${query.toString()}` : ""}`,
4508
- {
4509
- method: "GET",
4510
- headers: serviceHeadersToRecord(
4511
- this.createHookHeaders(
4512
- "tinycloud.hooks/list",
4513
- options.service ? buildScopePath(options.service, options.pathPrefix) : "webhooks"
4514
- )
4515
- )
4516
- }
4576
+ () => this.createHookHeaders(
4577
+ "tinycloud.hooks/list",
4578
+ options.service ? buildScopePath(options.service, options.pathPrefix) : "webhooks"
4579
+ ),
4580
+ { method: "GET" }
4517
4581
  );
4518
4582
  if (!response.ok) {
4519
4583
  return err(
@@ -4540,20 +4604,16 @@ var HooksService = class extends BaseService {
4540
4604
  return err(authRequiredError("hooks"));
4541
4605
  }
4542
4606
  try {
4543
- const response = await this.context.fetch(
4607
+ const response = await this.invokeWithRetry(
4544
4608
  `${this.host}/hooks/webhooks/${encodeURIComponent(id)}`,
4545
- {
4546
- method: "DELETE",
4547
- headers: serviceHeadersToRecord(
4548
- this.createHookHeaders(
4549
- "tinycloud.hooks/unregister",
4550
- options.target ? buildScopePath(
4551
- options.target.service,
4552
- options.target.pathPrefix
4553
- ) : `webhooks/${id}`
4554
- )
4555
- )
4556
- }
4609
+ () => this.createHookHeaders(
4610
+ "tinycloud.hooks/unregister",
4611
+ options.target ? buildScopePath(
4612
+ options.target.service,
4613
+ options.target.pathPrefix
4614
+ ) : `webhooks/${id}`
4615
+ ),
4616
+ { method: "DELETE" }
4557
4617
  );
4558
4618
  if (!response.ok) {
4559
4619
  return err(
@@ -5009,23 +5069,7 @@ function readProperty(value, key) {
5009
5069
  return void 0;
5010
5070
  }
5011
5071
  }
5012
- function sleep(delayMs, _attempt, signal) {
5013
- return new Promise((resolve) => {
5014
- const finish = () => {
5015
- clearTimeout(timer);
5016
- signal.removeEventListener("abort", stop);
5017
- resolve();
5018
- };
5019
- const timer = setTimeout(finish, delayMs);
5020
- timer.unref?.();
5021
- const stop = () => finish();
5022
- if (signal.aborted) {
5023
- finish();
5024
- } else {
5025
- signal.addEventListener("abort", stop, { once: true });
5026
- }
5027
- });
5028
- }
5072
+ var sleep = (delayMs, _attempt, signal) => abortableSleep(delayMs, signal, true);
5029
5073
  function abortPromise(signal) {
5030
5074
  let resolve;
5031
5075
  const promise = new Promise((done) => {
@@ -5442,40 +5486,150 @@ function parseStorageStatus(body) {
5442
5486
  if (status.account.plan) status.plan = status.account.plan;
5443
5487
  status.state = storageUsageState(account.usedBytes, account.limitBytes);
5444
5488
  }
5445
- return status;
5489
+ return status;
5490
+ }
5491
+
5492
+ // src/quota/TinyCloudQuota.ts
5493
+ var TinyCloudQuota = class {
5494
+ constructor(config = {}) {
5495
+ this.quotaUrl = null;
5496
+ this.config = config;
5497
+ }
5498
+ /** Set the quota URL discovered from the /info endpoint */
5499
+ setQuotaUrl(url) {
5500
+ this.quotaUrl = url;
5501
+ }
5502
+ /** Whether a quota service is available */
5503
+ get available() {
5504
+ return this.quotaUrl !== null;
5505
+ }
5506
+ /** Query quota status for a space from the quota URL */
5507
+ async getQuota(spaceId) {
5508
+ if (!this.quotaUrl) return null;
5509
+ const resp = await fetch(
5510
+ `${this.quotaUrl}/api/quota/${encodeURIComponent(spaceId)}`
5511
+ );
5512
+ if (!resp.ok) return null;
5513
+ const data = await resp.json();
5514
+ return {
5515
+ limitBytes: data.storage_limit_bytes ?? 0
5516
+ };
5517
+ }
5518
+ /** Trigger the upgrade callback when a quota error is encountered */
5519
+ handleQuotaError(info) {
5520
+ this.config.onUpgradeRequired?.(info);
5521
+ }
5522
+ };
5523
+
5524
+ // src/encryption/canonical.ts
5525
+ function canonicalize(value) {
5526
+ if (value === void 0) {
5527
+ return "";
5528
+ }
5529
+ return stringify(value);
5530
+ }
5531
+ function stringify(value) {
5532
+ if (value === null) return "null";
5533
+ switch (typeof value) {
5534
+ case "boolean":
5535
+ case "number":
5536
+ return JSON.stringify(value);
5537
+ case "string":
5538
+ return JSON.stringify(value);
5539
+ case "object": {
5540
+ if (Array.isArray(value)) {
5541
+ return `[${value.map(stringify).join(",")}]`;
5542
+ }
5543
+ const keys = Object.keys(value).sort();
5544
+ const parts = [];
5545
+ for (const k of keys) {
5546
+ const v = value[k];
5547
+ if (v === void 0) continue;
5548
+ parts.push(`${JSON.stringify(k)}:${stringify(v)}`);
5549
+ }
5550
+ return `{${parts.join(",")}}`;
5551
+ }
5552
+ default:
5553
+ throw new TypeError(
5554
+ `canonicalize: unsupported value type ${typeof value}`
5555
+ );
5556
+ }
5557
+ }
5558
+ var HEX = "0123456789abcdef";
5559
+ function hexEncode(bytes) {
5560
+ let out = "";
5561
+ for (let i = 0; i < bytes.length; i++) {
5562
+ const b = bytes[i];
5563
+ out += HEX[b >> 4 & 15] + HEX[b & 15];
5564
+ }
5565
+ return out;
5446
5566
  }
5447
-
5448
- // src/quota/TinyCloudQuota.ts
5449
- var TinyCloudQuota = class {
5450
- constructor(config = {}) {
5451
- this.quotaUrl = null;
5452
- this.config = config;
5567
+ function hexDecode(hex) {
5568
+ if (hex.length % 2 !== 0) {
5569
+ throw new Error("hex string must have even length");
5453
5570
  }
5454
- /** Set the quota URL discovered from the /info endpoint */
5455
- setQuotaUrl(url) {
5456
- this.quotaUrl = url;
5571
+ const out = new Uint8Array(hex.length / 2);
5572
+ for (let i = 0; i < out.length; i++) {
5573
+ const hi = parseInt(hex[i * 2], 16);
5574
+ const lo = parseInt(hex[i * 2 + 1], 16);
5575
+ if (Number.isNaN(hi) || Number.isNaN(lo)) {
5576
+ throw new Error("invalid hex character");
5577
+ }
5578
+ out[i] = hi << 4 | lo;
5457
5579
  }
5458
- /** Whether a quota service is available */
5459
- get available() {
5460
- return this.quotaUrl !== null;
5580
+ return out;
5581
+ }
5582
+ function base64Encode(bytes) {
5583
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
5584
+ let out = "";
5585
+ for (let i = 0; i < bytes.length; i += 3) {
5586
+ const b0 = bytes[i];
5587
+ const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
5588
+ const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
5589
+ out += chars[b0 >> 2 & 63];
5590
+ out += chars[(b0 << 4 | b1 >> 4) & 63];
5591
+ out += i + 1 < bytes.length ? chars[(b1 << 2 | b2 >> 6) & 63] : "=";
5592
+ out += i + 2 < bytes.length ? chars[b2 & 63] : "=";
5461
5593
  }
5462
- /** Query quota status for a space from the quota URL */
5463
- async getQuota(spaceId) {
5464
- if (!this.quotaUrl) return null;
5465
- const resp = await fetch(
5466
- `${this.quotaUrl}/api/quota/${encodeURIComponent(spaceId)}`
5467
- );
5468
- if (!resp.ok) return null;
5469
- const data = await resp.json();
5470
- return {
5471
- limitBytes: data.storage_limit_bytes ?? 0
5472
- };
5594
+ return out;
5595
+ }
5596
+ function base64Decode(s) {
5597
+ const clean = s.replace(/[^A-Za-z0-9+/=]/g, "");
5598
+ const len = clean.length;
5599
+ if (len % 4 !== 0) {
5600
+ throw new Error("invalid base64 input");
5473
5601
  }
5474
- /** Trigger the upgrade callback when a quota error is encountered */
5475
- handleQuotaError(info) {
5476
- this.config.onUpgradeRequired?.(info);
5602
+ const padding = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
5603
+ const outLen = len / 4 * 3 - padding;
5604
+ const out = new Uint8Array(outLen);
5605
+ const lookup = {};
5606
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
5607
+ for (let i = 0; i < chars.length; i++) lookup[chars[i]] = i;
5608
+ let outIdx = 0;
5609
+ for (let i = 0; i < len; i += 4) {
5610
+ const v0 = lookup[clean[i]] ?? 0;
5611
+ const v1 = lookup[clean[i + 1]] ?? 0;
5612
+ const v2 = clean[i + 2] === "=" ? 0 : lookup[clean[i + 2]] ?? 0;
5613
+ const v3 = clean[i + 3] === "=" ? 0 : lookup[clean[i + 3]] ?? 0;
5614
+ const b0 = v0 << 2 | v1 >> 4;
5615
+ const b1 = (v1 & 15) << 4 | v2 >> 2;
5616
+ const b2 = (v2 & 3) << 6 | v3;
5617
+ if (outIdx < outLen) out[outIdx++] = b0;
5618
+ if (outIdx < outLen) out[outIdx++] = b1;
5619
+ if (outIdx < outLen) out[outIdx++] = b2;
5477
5620
  }
5478
- };
5621
+ return out;
5622
+ }
5623
+ function utf8Encode(s) {
5624
+ return new TextEncoder().encode(s);
5625
+ }
5626
+ function utf8Decode(b) {
5627
+ return new TextDecoder().decode(b);
5628
+ }
5629
+ function canonicalHashHex(sha256, value) {
5630
+ const canonical = canonicalize(value);
5631
+ return hexEncode(sha256(utf8Encode(canonical)));
5632
+ }
5479
5633
 
5480
5634
  // src/vault/types.ts
5481
5635
  var VaultPublicSpaceKVActions = [
@@ -5646,7 +5800,7 @@ function toBytes(str) {
5646
5800
  function fromBytes(bytes) {
5647
5801
  return new TextDecoder().decode(bytes);
5648
5802
  }
5649
- function hexEncode(bytes) {
5803
+ function hexEncode2(bytes) {
5650
5804
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
5651
5805
  }
5652
5806
  function concatBytes(...arrays) {
@@ -5659,14 +5813,14 @@ function concatBytes(...arrays) {
5659
5813
  }
5660
5814
  return result;
5661
5815
  }
5662
- function base64Encode(bytes) {
5816
+ function base64Encode2(bytes) {
5663
5817
  let binary = "";
5664
5818
  for (let i = 0; i < bytes.length; i++) {
5665
5819
  binary += String.fromCharCode(bytes[i]);
5666
5820
  }
5667
5821
  return btoa(binary);
5668
5822
  }
5669
- function base64Decode(str) {
5823
+ function base64Decode2(str) {
5670
5824
  const binary = atob(str);
5671
5825
  const bytes = new Uint8Array(binary.length);
5672
5826
  for (let i = 0; i < binary.length; i++) {
@@ -5708,6 +5862,11 @@ function defaultVaultMessage(input) {
5708
5862
  return input.message;
5709
5863
  case "PERMISSION_DENIED":
5710
5864
  return input.message;
5865
+ case "AAD_MISMATCH":
5866
+ case "ETAG_MISMATCH":
5867
+ case "INVALID_ETAG":
5868
+ case "OUTCOME_UNKNOWN":
5869
+ case "NODE_BUSY":
5711
5870
  case "STORAGE_QUOTA_EXCEEDED":
5712
5871
  case "STORAGE_LIMIT_REACHED":
5713
5872
  return input.message;
@@ -5730,6 +5889,37 @@ function typedKvCause(error) {
5730
5889
  ...error.cause !== void 0 ? { cause: error.cause } : {}
5731
5890
  });
5732
5891
  }
5892
+ function canonicalAadSpaceId(spaceId) {
5893
+ return spaceId.replace(/0x[0-9a-fA-F]{40}/g, (address) => address.toLowerCase());
5894
+ }
5895
+ function checkEnvelopeAad(value, spaceId, key) {
5896
+ const e = value;
5897
+ if (typeof e !== "object" || e === null || Array.isArray(e) || !["networkId", "alg", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext"].every(
5898
+ (field) => typeof e[field] === "string" && e[field] !== ""
5899
+ ) || !Number.isInteger(e.keyVersion) || e.aad !== void 0 && typeof e.aad !== "string") {
5900
+ return "corrupt";
5901
+ }
5902
+ let aad = "";
5903
+ try {
5904
+ if (typeof e.aad === "string") aad = fromBytes(base64Decode(e.aad));
5905
+ } catch {
5906
+ }
5907
+ const suffix = `:${key}`;
5908
+ return aad.endsWith(suffix) && canonicalAadSpaceId(aad.slice(0, -suffix.length)) === canonicalAadSpaceId(`tinycloud.vault:${spaceId}`) ? "ok" : "mismatch";
5909
+ }
5910
+ var AMBIGUOUS_WRITE_STATUSES = [500, 502, 503, 504, 520, 522, 524];
5911
+ function ifMatchWriteError(key, error) {
5912
+ const status = error.meta?.status;
5913
+ 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;
5914
+ if (code === void 0) return void 0;
5915
+ return vaultError({
5916
+ code,
5917
+ key,
5918
+ message: error.message,
5919
+ ...error.meta !== void 0 ? { meta: error.meta } : {},
5920
+ cause: typedKvCause(error)
5921
+ });
5922
+ }
5733
5923
  function kvWriteError(context, error) {
5734
5924
  if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
5735
5925
  return vaultError({ code: error.code, message: error.message, meta: error.meta });
@@ -5916,7 +6106,7 @@ var DataVaultService = class extends BaseService {
5916
6106
  }
5917
6107
  if (existingPubKey) {
5918
6108
  this.encryptionIdentity = {
5919
- publicKey: base64Decode(existingPubKey),
6109
+ publicKey: base64Decode2(existingPubKey),
5920
6110
  privateKey: new Uint8Array(0)
5921
6111
  // private key not available without signing
5922
6112
  };
@@ -5941,7 +6131,7 @@ var DataVaultService = class extends BaseService {
5941
6131
  );
5942
6132
  this.encryptionIdentity = this.crypto.x25519FromSeed(seed);
5943
6133
  try {
5944
- const pubKeyB64 = base64Encode(this.encryptionIdentity.publicKey);
6134
+ const pubKeyB64 = base64Encode2(this.encryptionIdentity.publicKey);
5945
6135
  await this.tc.ensurePublicSpace();
5946
6136
  await this.tc.publicKV.put(".well-known/vault-pubkey", pubKeyB64);
5947
6137
  await this.tc.publicKV.put(".well-known/vault-version", CURRENT_VAULT_VERSION);
@@ -6007,6 +6197,21 @@ var DataVaultService = class extends BaseService {
6007
6197
  message: "Authentication required"
6008
6198
  });
6009
6199
  }
6200
+ if (options?.ifMatch !== void 0) {
6201
+ if (options.ifNoneMatch !== void 0) {
6202
+ return vaultError({
6203
+ code: "STORAGE_ERROR",
6204
+ cause: new Error("ifMatch cannot be combined with ifNoneMatch")
6205
+ });
6206
+ }
6207
+ if (!/^"[^"\s]+"$/.test(options.ifMatch)) {
6208
+ return vaultError({
6209
+ code: "INVALID_ETAG",
6210
+ key,
6211
+ 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"
6212
+ });
6213
+ }
6214
+ }
6010
6215
  try {
6011
6216
  const { plaintext, contentType } = this.serializeValue(value, options);
6012
6217
  const metadata = {
@@ -6029,7 +6234,8 @@ var DataVaultService = class extends BaseService {
6029
6234
  }
6030
6235
  const valuePath = `vault/${key}`;
6031
6236
  const valueBody = JSON.stringify(envelopeResult.data);
6032
- const valuePutResult = options?.ifNoneMatch !== void 0 ? await this.tc.kv.put(valuePath, valueBody, { ifNoneMatch: options.ifNoneMatch }) : await this.tc.kv.put(valuePath, valueBody);
6237
+ const putOptions = options?.ifMatch !== void 0 ? { ifMatch: options.ifMatch } : options?.ifNoneMatch !== void 0 ? { ifNoneMatch: options.ifNoneMatch } : void 0;
6238
+ const valuePutResult = putOptions !== void 0 ? await this.tc.kv.put(valuePath, valueBody, putOptions) : await this.tc.kv.put(valuePath, valueBody);
6033
6239
  if (!valuePutResult.ok) {
6034
6240
  if (options?.ifNoneMatch !== void 0 && valuePutResult.error.code === ErrorCodes.KV_PRECONDITION_FAILED) {
6035
6241
  return vaultError({
@@ -6039,6 +6245,10 @@ var DataVaultService = class extends BaseService {
6039
6245
  cause: typedKvCause(valuePutResult.error)
6040
6246
  });
6041
6247
  }
6248
+ if (options?.ifMatch !== void 0) {
6249
+ const classified = ifMatchWriteError(key, valuePutResult.error);
6250
+ if (classified !== void 0) return classified;
6251
+ }
6042
6252
  return kvWriteError("Failed to store encrypted value", valuePutResult.error);
6043
6253
  }
6044
6254
  return { ok: true, data: void 0 };
@@ -6073,6 +6283,19 @@ var DataVaultService = class extends BaseService {
6073
6283
  }
6074
6284
  const rawEnvelope = unwrapKVData(valueResult.data);
6075
6285
  const envelope = typeof rawEnvelope === "string" ? JSON.parse(rawEnvelope) : rawEnvelope;
6286
+ if (options?.expectAad === true) {
6287
+ const verdict = checkEnvelopeAad(envelope, this.vaultConfig.spaceId, key);
6288
+ if (verdict === "corrupt") {
6289
+ return vaultError({ code: "DECRYPTION_FAILED", message: "envelope is malformed" });
6290
+ }
6291
+ if (verdict === "mismatch") {
6292
+ return vaultError({
6293
+ code: "AAD_MISMATCH",
6294
+ key,
6295
+ message: `Envelope at ${key} is not bound to this space and key`
6296
+ });
6297
+ }
6298
+ }
6076
6299
  const proof = await this.decryptCapabilityProof();
6077
6300
  const plaintextResult = await config.service.decryptEnvelope(envelope, proof);
6078
6301
  if (!plaintextResult.ok) {
@@ -6089,7 +6312,11 @@ var DataVaultService = class extends BaseService {
6089
6312
  contentType,
6090
6313
  options
6091
6314
  );
6092
- return { ok: true, data: { value, metadata, keyId } };
6315
+ const etag = options?.source === "network" ? valueResult.data.headers?.etag : void 0;
6316
+ return {
6317
+ ok: true,
6318
+ data: { value, metadata, keyId, ...etag === void 0 ? {} : { etag } }
6319
+ };
6093
6320
  } catch (error) {
6094
6321
  return vaultError({
6095
6322
  code: "STORAGE_ERROR",
@@ -6135,6 +6362,12 @@ var DataVaultService = class extends BaseService {
6135
6362
  } catch {
6136
6363
  return { status: "corrupt_envelope" };
6137
6364
  }
6365
+ if (options?.expectAad === true) {
6366
+ const verdict = checkEnvelopeAad(envelope, this.vaultConfig.spaceId, key);
6367
+ if (verdict !== "ok") {
6368
+ return { status: verdict === "corrupt" ? "corrupt_envelope" : "aad_mismatch" };
6369
+ }
6370
+ }
6138
6371
  let plaintextResult;
6139
6372
  try {
6140
6373
  const proof = await this.decryptCapabilityProof();
@@ -6166,7 +6399,11 @@ var DataVaultService = class extends BaseService {
6166
6399
  contentType,
6167
6400
  options
6168
6401
  );
6169
- return { status: "ok", entry: { value, metadata, keyId } };
6402
+ const etag = valueResult.data.headers?.etag;
6403
+ return {
6404
+ status: "ok",
6405
+ entry: { value, metadata, keyId, ...etag === void 0 ? {} : { etag } }
6406
+ };
6170
6407
  } catch {
6171
6408
  return { status: "invalid_payload" };
6172
6409
  }
@@ -6216,6 +6453,14 @@ var DataVaultService = class extends BaseService {
6216
6453
  )
6217
6454
  });
6218
6455
  }
6456
+ if (options?.ifMatch !== void 0) {
6457
+ return vaultError({
6458
+ code: "STORAGE_ERROR",
6459
+ cause: new Error(
6460
+ "ifMatch is only supported by network-encrypted vaults"
6461
+ )
6462
+ });
6463
+ }
6219
6464
  if (!this._isUnlocked || !this.masterKey) {
6220
6465
  return vaultError({
6221
6466
  code: "VAULT_LOCKED",
@@ -6241,7 +6486,7 @@ var DataVaultService = class extends BaseService {
6241
6486
  }
6242
6487
  const contentType = options?.contentType ?? (value instanceof Uint8Array ? "application/octet-stream" : "application/json");
6243
6488
  const entryKey = this.crypto.randomBytes(32);
6244
- const keyId = hexEncode(this.crypto.sha256(entryKey)).slice(0, 16);
6489
+ const keyId = hexEncode2(this.crypto.sha256(entryKey)).slice(0, 16);
6245
6490
  const encrypted = this.crypto.encrypt(entryKey, plaintext);
6246
6491
  const keyBlob = this.crypto.encrypt(this.masterKey, entryKey);
6247
6492
  const metadata = {
@@ -6259,7 +6504,7 @@ var DataVaultService = class extends BaseService {
6259
6504
  ...metadata
6260
6505
  });
6261
6506
  const keyPayload = JSON.stringify({
6262
- key: base64Encode(keyBlob),
6507
+ key: base64Encode2(keyBlob),
6263
6508
  metadata: keyMetadata
6264
6509
  });
6265
6510
  const keyPutResult = await this.tc.kv.put(
@@ -6270,7 +6515,7 @@ var DataVaultService = class extends BaseService {
6270
6515
  return kvWriteError("Failed to store key blob", keyPutResult.error);
6271
6516
  }
6272
6517
  const valuePayload = JSON.stringify({
6273
- data: base64Encode(encrypted),
6518
+ data: base64Encode2(encrypted),
6274
6519
  metadata
6275
6520
  });
6276
6521
  const valuePutResult = await this.tc.kv.put(
@@ -6301,6 +6546,12 @@ var DataVaultService = class extends BaseService {
6301
6546
  if (this.usesNetworkEncryption) {
6302
6547
  return this.getNetworkEncrypted(key, options);
6303
6548
  }
6549
+ if (options?.expectAad === true) {
6550
+ return vaultError({
6551
+ code: "STORAGE_ERROR",
6552
+ cause: new Error("expectAad is only supported by network-encrypted vaults")
6553
+ });
6554
+ }
6304
6555
  if (!this._isUnlocked || !this.masterKey) {
6305
6556
  return vaultError({
6306
6557
  code: "VAULT_LOCKED",
@@ -6322,7 +6573,7 @@ var DataVaultService = class extends BaseService {
6322
6573
  return vaultError({ code: "KEY_NOT_FOUND", key });
6323
6574
  }
6324
6575
  const keyEnvelope = JSON.parse(keyResult.data.data);
6325
- const keyBlobBytes = base64Decode(keyEnvelope.key);
6576
+ const keyBlobBytes = base64Decode2(keyEnvelope.key);
6326
6577
  const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
6327
6578
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
6328
6579
  raw: true,
@@ -6332,7 +6583,7 @@ var DataVaultService = class extends BaseService {
6332
6583
  return vaultError({ code: "KEY_NOT_FOUND", key });
6333
6584
  }
6334
6585
  const valueEnvelope = JSON.parse(valueResult.data.data);
6335
- const encryptedBytes = base64Decode(valueEnvelope.data);
6586
+ const encryptedBytes = base64Decode2(valueEnvelope.data);
6336
6587
  const plaintext = this.crypto.decrypt(entryKey, encryptedBytes);
6337
6588
  const metadata = valueEnvelope.metadata ?? {};
6338
6589
  const contentType = metadata[VaultHeaders.CONTENT_TYPE] ?? "application/json";
@@ -6580,7 +6831,7 @@ var DataVaultService = class extends BaseService {
6580
6831
  return vaultError({ code: "KEY_NOT_FOUND", key });
6581
6832
  }
6582
6833
  const keyEnvelope = JSON.parse(keyResult.data.data);
6583
- const keyBlobBytes = base64Decode(keyEnvelope.key);
6834
+ const keyBlobBytes = base64Decode2(keyEnvelope.key);
6584
6835
  const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
6585
6836
  const ephemeralSeed = this.crypto.randomBytes(32);
6586
6837
  const ephemeralKeyPair = this.crypto.x25519FromSeed(ephemeralSeed);
@@ -6599,7 +6850,7 @@ var DataVaultService = class extends BaseService {
6599
6850
  encryptedGrant
6600
6851
  );
6601
6852
  const grantPayload = JSON.stringify({
6602
- grant: base64Encode(grantBlob),
6853
+ grant: base64Encode2(grantBlob),
6603
6854
  spaceId: this.vaultConfig.spaceId,
6604
6855
  metadata: {
6605
6856
  [VaultHeaders.GRANT_VERSION]: "1",
@@ -6639,6 +6890,14 @@ var DataVaultService = class extends BaseService {
6639
6890
  */
6640
6891
  async getShared(grantorDID, key, options) {
6641
6892
  return this.withTelemetry("getShared", key, async () => {
6893
+ if (options?.expectAad === true) {
6894
+ return vaultError({
6895
+ code: "STORAGE_ERROR",
6896
+ cause: new Error(
6897
+ "expectAad is not supported by getShared: the AAD binds the grantor's space, which this vault does not know"
6898
+ )
6899
+ });
6900
+ }
6642
6901
  if (this.usesNetworkEncryption) {
6643
6902
  const grantorKV = options?.kv;
6644
6903
  if (!grantorKV) {
@@ -6733,7 +6992,7 @@ var DataVaultService = class extends BaseService {
6733
6992
  });
6734
6993
  }
6735
6994
  const grantEnvelope = typeof grantResult.data?.data === "string" ? JSON.parse(grantResult.data.data) : grantResult.data?.data;
6736
- const grantBlobBytes = base64Decode(grantEnvelope.grant);
6995
+ const grantBlobBytes = base64Decode2(grantEnvelope.grant);
6737
6996
  const ephemeralPubKey = grantBlobBytes.slice(0, 32);
6738
6997
  const encryptedGrant = grantBlobBytes.slice(32);
6739
6998
  const sharedSecret = this.crypto.x25519Dh(
@@ -6756,7 +7015,7 @@ var DataVaultService = class extends BaseService {
6756
7015
  });
6757
7016
  }
6758
7017
  const valueEnvelope = typeof valueResult.data?.data === "string" ? JSON.parse(valueResult.data.data) : valueResult.data?.data;
6759
- const encryptedBytes = base64Decode(valueEnvelope.data);
7018
+ const encryptedBytes = base64Decode2(valueEnvelope.data);
6760
7019
  const plaintext = this.crypto.decrypt(entryKey, encryptedBytes);
6761
7020
  const metadata = valueEnvelope.metadata ?? {};
6762
7021
  const contentType = metadata[VaultHeaders.CONTENT_TYPE] ?? "application/json";
@@ -6810,7 +7069,7 @@ var DataVaultService = class extends BaseService {
6810
7069
  if (!result.ok) {
6811
7070
  return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
6812
7071
  }
6813
- const pubKeyBytes = base64Decode(result.data);
7072
+ const pubKeyBytes = base64Decode2(result.data);
6814
7073
  return { ok: true, data: pubKeyBytes };
6815
7074
  } catch (error) {
6816
7075
  return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
@@ -6929,7 +7188,7 @@ var DataVaultService = class extends BaseService {
6929
7188
  }
6930
7189
  const currentEntry = getResult.data;
6931
7190
  const newEntryKey = this.crypto.randomBytes(32);
6932
- const newKeyId = hexEncode(this.crypto.sha256(newEntryKey)).slice(
7191
+ const newKeyId = hexEncode2(this.crypto.sha256(newEntryKey)).slice(
6933
7192
  0,
6934
7193
  16
6935
7194
  );
@@ -6946,7 +7205,7 @@ var DataVaultService = class extends BaseService {
6946
7205
  [VaultHeaders.KEY_ID]: newKeyId
6947
7206
  };
6948
7207
  const keyPayload = JSON.stringify({
6949
- key: base64Encode(newKeyBlob),
7208
+ key: base64Encode2(newKeyBlob),
6950
7209
  metadata: JSON.stringify({
6951
7210
  keyId: newKeyId,
6952
7211
  ...metadata
@@ -6960,7 +7219,7 @@ var DataVaultService = class extends BaseService {
6960
7219
  return kvWriteError("Failed to store rotated key blob", keyPutResult.error);
6961
7220
  }
6962
7221
  const valuePayload = JSON.stringify({
6963
- data: base64Encode(encrypted),
7222
+ data: base64Encode2(encrypted),
6964
7223
  metadata
6965
7224
  });
6966
7225
  const valuePutResult = await this.tc.kv.put(
@@ -7252,116 +7511,6 @@ var SecretsService = class {
7252
7511
  }
7253
7512
  };
7254
7513
 
7255
- // src/encryption/canonical.ts
7256
- function canonicalize(value) {
7257
- if (value === void 0) {
7258
- return "";
7259
- }
7260
- return stringify(value);
7261
- }
7262
- function stringify(value) {
7263
- if (value === null) return "null";
7264
- switch (typeof value) {
7265
- case "boolean":
7266
- case "number":
7267
- return JSON.stringify(value);
7268
- case "string":
7269
- return JSON.stringify(value);
7270
- case "object": {
7271
- if (Array.isArray(value)) {
7272
- return `[${value.map(stringify).join(",")}]`;
7273
- }
7274
- const keys = Object.keys(value).sort();
7275
- const parts = [];
7276
- for (const k of keys) {
7277
- const v = value[k];
7278
- if (v === void 0) continue;
7279
- parts.push(`${JSON.stringify(k)}:${stringify(v)}`);
7280
- }
7281
- return `{${parts.join(",")}}`;
7282
- }
7283
- default:
7284
- throw new TypeError(
7285
- `canonicalize: unsupported value type ${typeof value}`
7286
- );
7287
- }
7288
- }
7289
- var HEX = "0123456789abcdef";
7290
- function hexEncode2(bytes) {
7291
- let out = "";
7292
- for (let i = 0; i < bytes.length; i++) {
7293
- const b = bytes[i];
7294
- out += HEX[b >> 4 & 15] + HEX[b & 15];
7295
- }
7296
- return out;
7297
- }
7298
- function hexDecode(hex) {
7299
- if (hex.length % 2 !== 0) {
7300
- throw new Error("hex string must have even length");
7301
- }
7302
- const out = new Uint8Array(hex.length / 2);
7303
- for (let i = 0; i < out.length; i++) {
7304
- const hi = parseInt(hex[i * 2], 16);
7305
- const lo = parseInt(hex[i * 2 + 1], 16);
7306
- if (Number.isNaN(hi) || Number.isNaN(lo)) {
7307
- throw new Error("invalid hex character");
7308
- }
7309
- out[i] = hi << 4 | lo;
7310
- }
7311
- return out;
7312
- }
7313
- function base64Encode2(bytes) {
7314
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7315
- let out = "";
7316
- for (let i = 0; i < bytes.length; i += 3) {
7317
- const b0 = bytes[i];
7318
- const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
7319
- const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
7320
- out += chars[b0 >> 2 & 63];
7321
- out += chars[(b0 << 4 | b1 >> 4) & 63];
7322
- out += i + 1 < bytes.length ? chars[(b1 << 2 | b2 >> 6) & 63] : "=";
7323
- out += i + 2 < bytes.length ? chars[b2 & 63] : "=";
7324
- }
7325
- return out;
7326
- }
7327
- function base64Decode2(s) {
7328
- const clean = s.replace(/[^A-Za-z0-9+/=]/g, "");
7329
- const len = clean.length;
7330
- if (len % 4 !== 0) {
7331
- throw new Error("invalid base64 input");
7332
- }
7333
- const padding = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
7334
- const outLen = len / 4 * 3 - padding;
7335
- const out = new Uint8Array(outLen);
7336
- const lookup = {};
7337
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7338
- for (let i = 0; i < chars.length; i++) lookup[chars[i]] = i;
7339
- let outIdx = 0;
7340
- for (let i = 0; i < len; i += 4) {
7341
- const v0 = lookup[clean[i]] ?? 0;
7342
- const v1 = lookup[clean[i + 1]] ?? 0;
7343
- const v2 = clean[i + 2] === "=" ? 0 : lookup[clean[i + 2]] ?? 0;
7344
- const v3 = clean[i + 3] === "=" ? 0 : lookup[clean[i + 3]] ?? 0;
7345
- const b0 = v0 << 2 | v1 >> 4;
7346
- const b1 = (v1 & 15) << 4 | v2 >> 2;
7347
- const b2 = (v2 & 3) << 6 | v3;
7348
- if (outIdx < outLen) out[outIdx++] = b0;
7349
- if (outIdx < outLen) out[outIdx++] = b1;
7350
- if (outIdx < outLen) out[outIdx++] = b2;
7351
- }
7352
- return out;
7353
- }
7354
- function utf8Encode(s) {
7355
- return new TextEncoder().encode(s);
7356
- }
7357
- function utf8Decode(b) {
7358
- return new TextDecoder().decode(b);
7359
- }
7360
- function canonicalHashHex(sha256, value) {
7361
- const canonical = canonicalize(value);
7362
- return hexEncode2(sha256(utf8Encode(canonical)));
7363
- }
7364
-
7365
7514
  // src/encryption/types.ts
7366
7515
  var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
7367
7516
  var ENVELOPE_VERSION = 1;
@@ -7571,7 +7720,7 @@ function encryptToNetwork(crypto2, input) {
7571
7720
  const symmetricKey = crypto2.randomBytes(32);
7572
7721
  const ciphertext = crypto2.authEncrypt(symmetricKey, input.plaintext, input.aad);
7573
7722
  const wrapped = crypto2.sealToNetworkKey(input.networkPublicKey, symmetricKey);
7574
- const encryptedSymmetricKey = base64Encode2(wrapped);
7723
+ const encryptedSymmetricKey = base64Encode(wrapped);
7575
7724
  const encryptedSymmetricKeyHash = canonicalHashHex(
7576
7725
  crypto2.sha256,
7577
7726
  encryptedSymmetricKey
@@ -7583,8 +7732,8 @@ function encryptToNetwork(crypto2, input) {
7583
7732
  keyVersion,
7584
7733
  encryptedSymmetricKey,
7585
7734
  encryptedSymmetricKeyHash,
7586
- ciphertext: base64Encode2(ciphertext),
7587
- ...input.aad !== void 0 ? { aad: base64Encode2(input.aad) } : {},
7735
+ ciphertext: base64Encode(ciphertext),
7736
+ ...input.aad !== void 0 ? { aad: base64Encode(input.aad) } : {},
7588
7737
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
7589
7738
  };
7590
7739
  return { envelope, symmetricKey };
@@ -7658,8 +7807,8 @@ function validateEnvelope(crypto2, envelope) {
7658
7807
  return { ok: true, data: e };
7659
7808
  }
7660
7809
  function decryptEnvelopeWithKey(crypto2, envelope, symmetricKey) {
7661
- const ciphertext = base64Decode2(envelope.ciphertext);
7662
- const aad = envelope.aad !== void 0 ? base64Decode2(envelope.aad) : void 0;
7810
+ const ciphertext = base64Decode(envelope.ciphertext);
7811
+ const aad = envelope.aad !== void 0 ? base64Decode(envelope.aad) : void 0;
7663
7812
  return crypto2.authDecrypt(symmetricKey, ciphertext, aad);
7664
7813
  }
7665
7814
 
@@ -7677,7 +7826,7 @@ function buildCanonicalDecryptRequest(input) {
7677
7826
  return { canonicalBody, bodyHash, receiverPublicKeyHash };
7678
7827
  }
7679
7828
  function buildDecryptFacts(input) {
7680
- const bodyHash = input.canonicalBody !== void 0 ? hexEncode2(input.crypto.sha256(utf8Encode(input.canonicalBody))) : canonicalHashHex(
7829
+ const bodyHash = input.canonicalBody !== void 0 ? hexEncode(input.crypto.sha256(utf8Encode(input.canonicalBody))) : canonicalHashHex(
7681
7830
  input.crypto.sha256,
7682
7831
  input.body
7683
7832
  );
@@ -7960,7 +8109,7 @@ function verifyDecryptResponse(input) {
7960
8109
  })
7961
8110
  };
7962
8111
  }
7963
- const expectedRequestHash = hexEncode2(
8112
+ const expectedRequestHash = hexEncode(
7964
8113
  crypto2.sha256(utf8Encode(`${invocationCid}${requestBodyHash}`))
7965
8114
  );
7966
8115
  if (response.requestHash !== expectedRequestHash) {
@@ -7984,7 +8133,7 @@ function verifyDecryptResponse(input) {
7984
8133
  const signedBytes = new TextEncoder().encode(
7985
8134
  canonicalSignedResponse(response)
7986
8135
  );
7987
- const signatureBytes = base64Decode2(response.nodeSignature);
8136
+ const signatureBytes = base64Decode(response.nodeSignature);
7988
8137
  if (!crypto2.verifyNodeSignature(response.nodeId, signedBytes, signatureBytes)) {
7989
8138
  return {
7990
8139
  ok: false,
@@ -7996,7 +8145,7 @@ function verifyDecryptResponse(input) {
7996
8145
  return { ok: true, data: response };
7997
8146
  }
7998
8147
  function openWrappedKey(crypto2, receiverPrivateKey, response) {
7999
- const wrapped = base64Decode2(response.wrappedKey);
8148
+ const wrapped = base64Decode(response.wrappedKey);
8000
8149
  return crypto2.openWithReceiverKey(receiverPrivateKey, wrapped);
8001
8150
  }
8002
8151
 
@@ -8040,7 +8189,7 @@ var EncryptionService = class extends BaseService {
8040
8189
  const usable = ensureNetworkUsableForDecrypt(discovered.data);
8041
8190
  if (!usable.ok) return usable;
8042
8191
  const descriptor = usable.data;
8043
- const networkPublicKey = base64Decode2(descriptor.publicEncryptionKey);
8192
+ const networkPublicKey = base64Decode(descriptor.publicEncryptionKey);
8044
8193
  const result = encryptToNetwork(this.crypto, {
8045
8194
  // Discovery returns the node's canonical network identifier. Persist
8046
8195
  // that spelling in the envelope so a later response can be compared
@@ -8068,7 +8217,7 @@ var EncryptionService = class extends BaseService {
8068
8217
  this.assertActive();
8069
8218
  const validated = validateEnvelope(this.crypto, envelope);
8070
8219
  if (!validated.ok) return validated;
8071
- if (options?.aad !== void 0 && validated.data.aad !== base64Encode2(options.aad)) {
8220
+ if (options?.aad !== void 0 && validated.data.aad !== base64Encode(options.aad)) {
8072
8221
  return encErr(
8073
8222
  encryptionError({
8074
8223
  code: "INVALID_INPUT",
@@ -8096,7 +8245,7 @@ var EncryptionService = class extends BaseService {
8096
8245
  );
8097
8246
  }
8098
8247
  const receiverKey = generateRandomReceiverKey({ crypto: this.crypto });
8099
- const receiverPublicKey = base64Encode2(receiverKey.publicKey);
8248
+ const receiverPublicKey = base64Encode(receiverKey.publicKey);
8100
8249
  const receiverPublicKeyHash = canonicalHashHex(
8101
8250
  this.crypto.sha256,
8102
8251
  receiverPublicKey
@@ -8272,8 +8421,8 @@ export {
8272
8421
  authRequiredError,
8273
8422
  authUnauthorizedError,
8274
8423
  authorizationVerdictOf,
8275
- base64Decode2 as base64Decode,
8276
- base64Encode2 as base64Encode,
8424
+ base64Decode,
8425
+ base64Encode,
8277
8426
  buildCanonicalDecryptRequest,
8278
8427
  buildDecryptAttenuation,
8279
8428
  buildDecryptFacts,
@@ -8303,13 +8452,14 @@ export {
8303
8452
  generateRandomReceiverKey,
8304
8453
  getTinyCloudDebugLogs,
8305
8454
  hexDecode,
8306
- hexEncode2 as hexEncode,
8455
+ hexEncode,
8307
8456
  installTinyCloudDebugGlobals,
8308
8457
  isNetworkId,
8309
8458
  isStorageFullError,
8310
8459
  kvPrefixCovers,
8311
8460
  networkDiscoveryKey,
8312
8461
  networkError,
8462
+ nodeRetryError,
8313
8463
  notFoundError,
8314
8464
  ok,
8315
8465
  openWrappedKey,