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