@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.
- package/dist/{BaseService-1Ug6y7DC.d.cts → BaseService-BhfKzKke.d.cts} +5 -1
- package/dist/{BaseService-Bn07Vudy.d.ts → BaseService-x4AFZiEa.d.ts} +5 -1
- package/dist/encryption/index.cjs +25 -4
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +2 -2
- package/dist/encryption/index.d.ts +2 -2
- package/dist/encryption/index.js +25 -4
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +959 -808
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -7
- package/dist/index.d.ts +114 -7
- package/dist/index.js +958 -808
- package/dist/index.js.map +1 -1
- package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
- package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
- package/dist/kv/index.cjs +136 -99
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +4 -5
- package/dist/kv/index.d.ts +4 -5
- package/dist/kv/index.js +136 -99
- package/dist/kv/index.js.map +1 -1
- package/dist/kv/replication/grantPolicy.cjs +82 -0
- package/dist/kv/replication/grantPolicy.cjs.map +1 -0
- package/dist/kv/replication/grantPolicy.d.cts +19 -0
- package/dist/kv/replication/grantPolicy.d.ts +19 -0
- package/dist/kv/replication/grantPolicy.js +50 -0
- package/dist/kv/replication/grantPolicy.js.map +1 -0
- package/dist/kv/replication/index.cjs +576 -132
- package/dist/kv/replication/index.cjs.map +1 -1
- package/dist/kv/replication/index.d.cts +3 -3
- package/dist/kv/replication/index.d.ts +3 -3
- package/dist/kv/replication/index.js +576 -132
- package/dist/kv/replication/index.js.map +1 -1
- package/dist/{scope-BnyVPZ5E.d.ts → scope-9U4gVNFN.d.ts} +312 -7
- package/dist/{scope-CQ5kmHlt.d.cts → scope-BzdcrOZG.d.cts} +312 -7
- package/dist/sql/index.cjs +98 -50
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +2 -2
- package/dist/sql/index.d.ts +2 -2
- package/dist/sql/index.js +98 -50
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-gmdUiorz.d.cts → types-eVS1Whuf.d.cts} +29 -3
- package/dist/{types-gmdUiorz.d.ts → types-eVS1Whuf.d.ts} +29 -3
- 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:
|
|
60
|
+
maxAttempts: 4,
|
|
53
61
|
backoff: "exponential",
|
|
54
|
-
baseDelayMs:
|
|
55
|
-
maxDelayMs:
|
|
56
|
-
retryableErrors: [ErrorCodes.
|
|
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/
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
1938
|
-
|
|
1939
|
-
session,
|
|
1940
|
-
|
|
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
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
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
|
|
2333
|
+
const headers = item.headers;
|
|
2224
2334
|
const data = action === KVAction.HEAD ? void 0 : this.parseBatchValue(
|
|
2225
2335
|
item.dataBase64,
|
|
2226
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
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
|
-
|
|
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
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
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
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
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
|
-
|
|
3762
|
-
|
|
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
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
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
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
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.
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
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.
|
|
4574
|
+
const response = await this.invokeWithRetry(
|
|
4507
4575
|
`${this.host}/hooks/webhooks${query.size > 0 ? `?${query.toString()}` : ""}`,
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
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.
|
|
4607
|
+
const response = await this.invokeWithRetry(
|
|
4544
4608
|
`${this.host}/hooks/webhooks/${encodeURIComponent(id)}`,
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5449
|
-
|
|
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
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
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
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
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
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
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
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
7587
|
-
...input.aad !== void 0 ? { 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 =
|
|
7662
|
-
const aad = 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 ?
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 !==
|
|
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 =
|
|
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
|
-
|
|
8276
|
-
|
|
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
|
-
|
|
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,
|