@tinycloud/sdk-services 3.3.0-beta.7 → 3.3.0
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-Cc6Ueicm.d.cts → BaseService-BhfKzKke.d.cts} +5 -1
- package/dist/{BaseService-DOdXd7sr.d.ts → BaseService-x4AFZiEa.d.ts} +5 -1
- package/dist/encryption/index.cjs +18 -5
- 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 +18 -5
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +911 -797
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -11
- package/dist/index.d.ts +95 -11
- package/dist/index.js +911 -797
- 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 +72 -80
- 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 +72 -80
- package/dist/kv/index.js.map +1 -1
- package/dist/kv/replication/index.cjs +6 -5
- 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 +6 -5
- package/dist/kv/replication/index.js.map +1 -1
- package/dist/{scope-B-m6cKH-.d.ts → scope-BHTkk5Yk.d.ts} +13 -2
- package/dist/{scope-B7oeKdLM.d.cts → scope-DMhalRFn.d.cts} +13 -2
- package/dist/sql/index.cjs +30 -16
- 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 +30 -16
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-BjH0ZtGy.d.cts → types-eVS1Whuf.d.cts} +24 -4
- package/dist/{types-BjH0ZtGy.d.ts → types-eVS1Whuf.d.ts} +24 -4
- package/package.json +2 -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,7 +115,7 @@ __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,
|
|
@@ -182,7 +182,7 @@ var ErrorCodes = {
|
|
|
182
182
|
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
183
183
|
// Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
|
|
184
184
|
// retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
|
|
185
|
-
/** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`: nothing was applied; retry after `meta.retryAfterMs`. */
|
|
185
|
+
/** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
|
|
186
186
|
NODE_BUSY: "NODE_BUSY",
|
|
187
187
|
/** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
|
|
188
188
|
REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
|
|
@@ -223,11 +223,12 @@ var ErrorCodes = {
|
|
|
223
223
|
DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
|
|
224
224
|
};
|
|
225
225
|
var defaultRetryPolicy = {
|
|
226
|
-
maxAttempts:
|
|
226
|
+
maxAttempts: 4,
|
|
227
227
|
backoff: "exponential",
|
|
228
|
-
baseDelayMs:
|
|
229
|
-
maxDelayMs:
|
|
230
|
-
retryableErrors: [ErrorCodes.
|
|
228
|
+
baseDelayMs: 250,
|
|
229
|
+
maxDelayMs: 4e3,
|
|
230
|
+
retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
|
|
231
|
+
maxTotalMs: 12e3
|
|
231
232
|
};
|
|
232
233
|
var TelemetryEvents = {
|
|
233
234
|
SPAN: "telemetry.span",
|
|
@@ -654,7 +655,9 @@ var RetryPolicySchema = import_zod.z.object({
|
|
|
654
655
|
/** Maximum delay in milliseconds between retries */
|
|
655
656
|
maxDelayMs: import_zod.z.number().nonnegative(),
|
|
656
657
|
/** Error codes that should trigger a retry */
|
|
657
|
-
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()
|
|
658
661
|
});
|
|
659
662
|
var ServiceSessionSchema = import_zod.z.object({
|
|
660
663
|
/** The delegation header containing the UCAN */
|
|
@@ -791,448 +794,107 @@ function validateTelemetrySpanEvent(data) {
|
|
|
791
794
|
return { ok: true, data: result.data };
|
|
792
795
|
}
|
|
793
796
|
|
|
794
|
-
// src/
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
this._abortController = new AbortController();
|
|
801
|
-
this._retired = false;
|
|
802
|
-
this._invoke = this.wrapInvoke(config.invoke);
|
|
803
|
-
this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
|
|
804
|
-
this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
|
|
805
|
-
this._hosts = config.hosts;
|
|
806
|
-
this._session = config.session ?? null;
|
|
807
|
-
this._retryPolicy = {
|
|
808
|
-
...defaultRetryPolicy,
|
|
809
|
-
...config.retryPolicy
|
|
810
|
-
};
|
|
811
|
-
this._telemetryEnabled = typeof config.telemetry === "boolean" ? config.telemetry : config.telemetry?.enabled === true;
|
|
812
|
-
this._telemetryHandler = typeof config.telemetry === "object" ? config.telemetry.onEvent : void 0;
|
|
813
|
-
}
|
|
814
|
-
// ============================================================
|
|
815
|
-
// Session Management
|
|
816
|
-
// ============================================================
|
|
817
|
-
/**
|
|
818
|
-
* Get the current session.
|
|
819
|
-
*/
|
|
820
|
-
get session() {
|
|
821
|
-
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 {};
|
|
822
803
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
return !this._retired && this._session !== null;
|
|
804
|
+
}
|
|
805
|
+
function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
|
|
806
|
+
if (parsed.message) {
|
|
807
|
+
return compactMessage(parsed.message);
|
|
828
808
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
* @param session - New session or null to clear
|
|
833
|
-
*/
|
|
834
|
-
setSession(session) {
|
|
835
|
-
this.assertActive();
|
|
836
|
-
this._session = session;
|
|
837
|
-
this.emit("session.changed", { authenticated: session !== null });
|
|
838
|
-
for (const service of this._services.values()) {
|
|
839
|
-
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.`;
|
|
840
812
|
}
|
|
813
|
+
return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
|
|
841
814
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
this.assertActive();
|
|
850
|
-
return this._invoke;
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* Get the multi-resource invoke function when available.
|
|
854
|
-
*/
|
|
855
|
-
get invokeAny() {
|
|
856
|
-
this.assertActive();
|
|
857
|
-
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);
|
|
858
822
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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";
|
|
865
844
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
this.assertActive();
|
|
871
|
-
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");
|
|
872
849
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
* Register a service with the context.
|
|
878
|
-
*
|
|
879
|
-
* @param name - Service name (e.g., 'kv')
|
|
880
|
-
* @param service - Service instance
|
|
881
|
-
*/
|
|
882
|
-
registerService(name, service) {
|
|
883
|
-
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
|
+
);
|
|
884
854
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
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
|
+
);
|
|
892
861
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
getService(name) {
|
|
900
|
-
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
|
+
);
|
|
901
868
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
*
|
|
908
|
-
* @param event - Event name
|
|
909
|
-
* @param data - Event data
|
|
910
|
-
*/
|
|
911
|
-
emit(event, data) {
|
|
912
|
-
tinyCloudDebugLogger.log(event, data);
|
|
913
|
-
if (this._telemetryEnabled && this._telemetryHandler) {
|
|
914
|
-
try {
|
|
915
|
-
this._telemetryHandler(event, projectDiagnosticData(data));
|
|
916
|
-
} catch (error) {
|
|
917
|
-
console.error(`Error in telemetry handler for "${event}":`, error);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
const handlers = this._eventHandlers.get(event);
|
|
921
|
-
if (handlers) {
|
|
922
|
-
for (const handler of handlers) {
|
|
923
|
-
try {
|
|
924
|
-
handler(data);
|
|
925
|
-
} catch (error) {
|
|
926
|
-
console.error(`Error in event handler for "${event}":`, error);
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
}
|
|
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
|
+
);
|
|
930
874
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
* @param handler - Handler function
|
|
936
|
-
* @returns Unsubscribe function
|
|
937
|
-
*/
|
|
938
|
-
on(event, handler) {
|
|
939
|
-
if (!this._eventHandlers.has(event)) {
|
|
940
|
-
this._eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
941
|
-
}
|
|
942
|
-
this._eventHandlers.get(event).add(handler);
|
|
943
|
-
return () => {
|
|
944
|
-
const handlers = this._eventHandlers.get(event);
|
|
945
|
-
if (handlers) {
|
|
946
|
-
handlers.delete(handler);
|
|
947
|
-
if (handlers.size === 0) {
|
|
948
|
-
this._eventHandlers.delete(event);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
};
|
|
875
|
+
if (!NETWORK_NAME_RE.test(name)) {
|
|
876
|
+
throw new NetworkIdError(
|
|
877
|
+
`networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
878
|
+
);
|
|
952
879
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
clearEventHandlers(event) {
|
|
959
|
-
if (event) {
|
|
960
|
-
this._eventHandlers.delete(event);
|
|
961
|
-
} else {
|
|
962
|
-
this._eventHandlers.clear();
|
|
963
|
-
}
|
|
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");
|
|
964
885
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
* Get the abort signal for cancelling operations.
|
|
970
|
-
*/
|
|
971
|
-
get abortSignal() {
|
|
972
|
-
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
|
+
);
|
|
973
890
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
* Permanently retire this graph after its owner installs a replacement.
|
|
982
|
-
* Unlike `abort()`, retirement never creates a fresh controller, so captured
|
|
983
|
-
* services cannot resume calls under a superseded session.
|
|
984
|
-
*/
|
|
985
|
-
retire() {
|
|
986
|
-
if (this._retired) return;
|
|
987
|
-
this._retired = true;
|
|
988
|
-
this._session = null;
|
|
989
|
-
this._abortController.abort();
|
|
990
|
-
for (const service of this._services.values()) {
|
|
991
|
-
try {
|
|
992
|
-
service.onSignOut();
|
|
993
|
-
} catch (error) {
|
|
994
|
-
console.error("Error retiring service:", error);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
/**
|
|
999
|
-
* Abort all pending operations and notify services.
|
|
1000
|
-
* Creates a new AbortController for future operations.
|
|
1001
|
-
*/
|
|
1002
|
-
abort() {
|
|
1003
|
-
if (this._retired) return;
|
|
1004
|
-
this._abortController.abort();
|
|
1005
|
-
this._abortController = new AbortController();
|
|
1006
|
-
for (const service of this._services.values()) {
|
|
1007
|
-
try {
|
|
1008
|
-
service.onSignOut();
|
|
1009
|
-
} catch (error) {
|
|
1010
|
-
console.error("Error signing out service:", error);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
/**
|
|
1015
|
-
* Sign out - abort operations and clear session.
|
|
1016
|
-
*/
|
|
1017
|
-
signOut() {
|
|
1018
|
-
if (this._retired) return;
|
|
1019
|
-
this.abort();
|
|
1020
|
-
this.setSession(null);
|
|
1021
|
-
this.emit("session.expired", {});
|
|
1022
|
-
}
|
|
1023
|
-
// ============================================================
|
|
1024
|
-
// Retry Policy
|
|
1025
|
-
// ============================================================
|
|
1026
|
-
/**
|
|
1027
|
-
* Get the retry policy configuration.
|
|
1028
|
-
*/
|
|
1029
|
-
get retryPolicy() {
|
|
1030
|
-
return this._retryPolicy;
|
|
1031
|
-
}
|
|
1032
|
-
wrapInvoke(invoke) {
|
|
1033
|
-
return (session, service, path, action, facts) => {
|
|
1034
|
-
this.assertActive();
|
|
1035
|
-
if (!tinyCloudDebugLogger.isEnabled()) {
|
|
1036
|
-
return invoke(session, service, path, action, facts);
|
|
1037
|
-
}
|
|
1038
|
-
const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
|
|
1039
|
-
service,
|
|
1040
|
-
path,
|
|
1041
|
-
action,
|
|
1042
|
-
facts
|
|
1043
|
-
});
|
|
1044
|
-
try {
|
|
1045
|
-
const result = invoke(session, service, path, action, facts);
|
|
1046
|
-
timer.stop({ ok: true, service, path, action });
|
|
1047
|
-
return result;
|
|
1048
|
-
} catch (error) {
|
|
1049
|
-
timer.stop({ ok: false, service, path, action, error });
|
|
1050
|
-
throw error;
|
|
1051
|
-
}
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
assertActive() {
|
|
1055
|
-
if (this._retired) {
|
|
1056
|
-
throw new Error("Service graph has been retired: its session was replaced or signed out.");
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
wrapInvokeAny(invokeAny) {
|
|
1060
|
-
return (session, entries, facts) => {
|
|
1061
|
-
this.assertActive();
|
|
1062
|
-
if (!tinyCloudDebugLogger.isEnabled()) {
|
|
1063
|
-
return invokeAny(session, entries, facts);
|
|
1064
|
-
}
|
|
1065
|
-
const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
|
|
1066
|
-
entries,
|
|
1067
|
-
facts
|
|
1068
|
-
});
|
|
1069
|
-
try {
|
|
1070
|
-
const result = invokeAny(session, entries, facts);
|
|
1071
|
-
timer.stop({ ok: true, entries });
|
|
1072
|
-
return result;
|
|
1073
|
-
} catch (error) {
|
|
1074
|
-
timer.stop({ ok: false, entries, error });
|
|
1075
|
-
throw error;
|
|
1076
|
-
}
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
wrapFetch(fetchFn) {
|
|
1080
|
-
return async (url, init) => {
|
|
1081
|
-
this.assertActive();
|
|
1082
|
-
const request = {
|
|
1083
|
-
...init,
|
|
1084
|
-
signal: this.combineSignals(init?.signal)
|
|
1085
|
-
};
|
|
1086
|
-
if (!tinyCloudDebugLogger.isEnabled()) {
|
|
1087
|
-
return fetchFn(url, request);
|
|
1088
|
-
}
|
|
1089
|
-
const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", {
|
|
1090
|
-
url,
|
|
1091
|
-
method: request.method ?? "GET",
|
|
1092
|
-
init: request
|
|
1093
|
-
});
|
|
1094
|
-
try {
|
|
1095
|
-
const response = await fetchFn(url, request);
|
|
1096
|
-
timer.stop({
|
|
1097
|
-
ok: response.ok,
|
|
1098
|
-
url,
|
|
1099
|
-
method: request.method ?? "GET",
|
|
1100
|
-
status: response.status,
|
|
1101
|
-
statusText: response.statusText
|
|
1102
|
-
});
|
|
1103
|
-
return response;
|
|
1104
|
-
} catch (error) {
|
|
1105
|
-
timer.stop({
|
|
1106
|
-
ok: false,
|
|
1107
|
-
url,
|
|
1108
|
-
method: request.method ?? "GET",
|
|
1109
|
-
error
|
|
1110
|
-
});
|
|
1111
|
-
throw error;
|
|
1112
|
-
}
|
|
1113
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
combineSignals(signal) {
|
|
1116
|
-
if (!signal) return this._abortController.signal;
|
|
1117
|
-
const combined = new AbortController();
|
|
1118
|
-
const abort = (source) => combined.abort(source.reason);
|
|
1119
|
-
if (signal.aborted) {
|
|
1120
|
-
abort(signal);
|
|
1121
|
-
} else if (this._abortController.signal.aborted) {
|
|
1122
|
-
abort(this._abortController.signal);
|
|
1123
|
-
} else {
|
|
1124
|
-
signal.addEventListener("abort", () => abort(signal), { once: true });
|
|
1125
|
-
this._abortController.signal.addEventListener(
|
|
1126
|
-
"abort",
|
|
1127
|
-
() => abort(this._abortController.signal),
|
|
1128
|
-
{ once: true }
|
|
1129
|
-
);
|
|
1130
|
-
}
|
|
1131
|
-
return combined.signal;
|
|
1132
|
-
}
|
|
1133
|
-
};
|
|
1134
|
-
|
|
1135
|
-
// src/responseErrors.ts
|
|
1136
|
-
function parseServiceErrorBody(errorText) {
|
|
1137
|
-
try {
|
|
1138
|
-
return JSON.parse(errorText);
|
|
1139
|
-
} catch {
|
|
1140
|
-
return {};
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
|
|
1144
|
-
if (parsed.message) {
|
|
1145
|
-
return compactMessage(parsed.message);
|
|
1146
|
-
}
|
|
1147
|
-
if (looksLikeHtml(errorText)) {
|
|
1148
|
-
if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
|
|
1149
|
-
return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
|
|
1150
|
-
}
|
|
1151
|
-
return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
|
|
1152
|
-
}
|
|
1153
|
-
return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
|
|
1154
|
-
}
|
|
1155
|
-
function responseErrorMeta(status, statusText, errorText) {
|
|
1156
|
-
const meta = { status, statusText };
|
|
1157
|
-
const snippet = compactMessage(errorText);
|
|
1158
|
-
if (snippet) {
|
|
1159
|
-
meta.responseSnippet = snippet.slice(0, 300);
|
|
1160
|
-
}
|
|
1161
|
-
return meta;
|
|
1162
|
-
}
|
|
1163
|
-
function looksLikeHtml(text) {
|
|
1164
|
-
return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
|
|
1165
|
-
}
|
|
1166
|
-
function compactMessage(message) {
|
|
1167
|
-
const compact = message.replace(/\s+/g, " ").trim();
|
|
1168
|
-
return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
// src/errors.ts
|
|
1172
|
-
var import_bootstrap = require("@tinycloud/bootstrap");
|
|
1173
|
-
|
|
1174
|
-
// src/encryption/networkId.ts
|
|
1175
|
-
var URN_PREFIX = "urn:tinycloud:encryption:";
|
|
1176
|
-
var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
1177
|
-
var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
|
|
1178
|
-
var NetworkIdError = class extends Error {
|
|
1179
|
-
constructor(message) {
|
|
1180
|
-
super(message);
|
|
1181
|
-
this.name = "NetworkIdError";
|
|
1182
|
-
}
|
|
1183
|
-
};
|
|
1184
|
-
function parseNetworkId(networkId) {
|
|
1185
|
-
if (typeof networkId !== "string" || networkId.length === 0) {
|
|
1186
|
-
throw new NetworkIdError("networkId must be a non-empty string");
|
|
1187
|
-
}
|
|
1188
|
-
if (!networkId.startsWith(URN_PREFIX)) {
|
|
1189
|
-
throw new NetworkIdError(
|
|
1190
|
-
`networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
|
|
1191
|
-
);
|
|
1192
|
-
}
|
|
1193
|
-
const body = networkId.slice(URN_PREFIX.length);
|
|
1194
|
-
const lastColon = body.lastIndexOf(":");
|
|
1195
|
-
if (lastColon <= 0 || lastColon === body.length - 1) {
|
|
1196
|
-
throw new NetworkIdError(
|
|
1197
|
-
`networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
|
|
1198
|
-
);
|
|
1199
|
-
}
|
|
1200
|
-
const ownerDid = body.slice(0, lastColon);
|
|
1201
|
-
const name = body.slice(lastColon + 1);
|
|
1202
|
-
if (!ownerDid.startsWith("did:")) {
|
|
1203
|
-
throw new NetworkIdError(
|
|
1204
|
-
`networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
|
|
1205
|
-
);
|
|
1206
|
-
}
|
|
1207
|
-
const didParts = ownerDid.split(":");
|
|
1208
|
-
if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
|
|
1209
|
-
throw new NetworkIdError(
|
|
1210
|
-
`networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
|
|
1211
|
-
);
|
|
1212
|
-
}
|
|
1213
|
-
if (!NETWORK_NAME_RE.test(name)) {
|
|
1214
|
-
throw new NetworkIdError(
|
|
1215
|
-
`networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
1216
|
-
);
|
|
1217
|
-
}
|
|
1218
|
-
return { networkId, ownerDid, name };
|
|
1219
|
-
}
|
|
1220
|
-
function buildNetworkId(ownerDid, name) {
|
|
1221
|
-
if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
|
|
1222
|
-
throw new NetworkIdError("ownerDid must be a DID");
|
|
1223
|
-
}
|
|
1224
|
-
if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
|
|
1225
|
-
throw new NetworkIdError(
|
|
1226
|
-
`network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
|
|
1230
|
-
parseNetworkId(networkId);
|
|
1231
|
-
return networkId;
|
|
1232
|
-
}
|
|
1233
|
-
function isNetworkId(networkId) {
|
|
1234
|
-
if (typeof networkId !== "string") {
|
|
1235
|
-
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;
|
|
1236
898
|
}
|
|
1237
899
|
try {
|
|
1238
900
|
parseNetworkId(networkId);
|
|
@@ -1331,7 +993,7 @@ function nodeRetryError(service, message, response, errorText) {
|
|
|
1331
993
|
const body = errorText.trim();
|
|
1332
994
|
const [, token, stage] = /^([a-z_]+)(?::\s*([a-z_]+):)?/.exec(body) ?? [];
|
|
1333
995
|
let code;
|
|
1334
|
-
if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable")) {
|
|
996
|
+
if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable" || token === "read_audit_busy")) {
|
|
1335
997
|
code = ErrorCodes.NODE_BUSY;
|
|
1336
998
|
} else if (response.status === 503 && token === "request_timeout") {
|
|
1337
999
|
code = ErrorCodes.REQUEST_TIMEOUT;
|
|
@@ -1580,25 +1242,411 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
|
1580
1242
|
if (error.name === "AbortError") {
|
|
1581
1243
|
return abortedError(service);
|
|
1582
1244
|
}
|
|
1583
|
-
if (error.name === "TimeoutError" || error.message.toLowerCase().includes("timeout")) {
|
|
1584
|
-
return timeoutError(service);
|
|
1245
|
+
if (error.name === "TimeoutError" || error.message.toLowerCase().includes("timeout")) {
|
|
1246
|
+
return timeoutError(service);
|
|
1247
|
+
}
|
|
1248
|
+
return {
|
|
1249
|
+
code: defaultCode,
|
|
1250
|
+
message: error.message,
|
|
1251
|
+
service,
|
|
1252
|
+
cause: error
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
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;
|
|
1585
1602
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
+
}
|
|
1591
1629
|
};
|
|
1592
1630
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
}
|
|
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
|
+
};
|
|
1602
1650
|
|
|
1603
1651
|
// src/base/BaseService.ts
|
|
1604
1652
|
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
@@ -1782,6 +1830,18 @@ var BaseService = class {
|
|
|
1782
1830
|
}
|
|
1783
1831
|
return controller.signal;
|
|
1784
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
|
+
}
|
|
1785
1845
|
/**
|
|
1786
1846
|
* Create the abort signal for one request, bounded by the request timeout.
|
|
1787
1847
|
*
|
|
@@ -2149,15 +2209,6 @@ var KVService = class extends BaseService {
|
|
|
2149
2209
|
get host() {
|
|
2150
2210
|
return this.context.hosts[0];
|
|
2151
2211
|
}
|
|
2152
|
-
withJsonContentType(headers) {
|
|
2153
|
-
if (Array.isArray(headers)) {
|
|
2154
|
-
return [...headers, ["content-type", "application/json"]];
|
|
2155
|
-
}
|
|
2156
|
-
return {
|
|
2157
|
-
...headers,
|
|
2158
|
-
"content-type": "application/json"
|
|
2159
|
-
};
|
|
2160
|
-
}
|
|
2161
2212
|
/**
|
|
2162
2213
|
* Execute an invoke operation.
|
|
2163
2214
|
*
|
|
@@ -2168,20 +2219,11 @@ var KVService = class extends BaseService {
|
|
|
2168
2219
|
* @returns Fetch response
|
|
2169
2220
|
*/
|
|
2170
2221
|
async invokeOperation(path, action, body, signal, extraHeaders) {
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
session,
|
|
2174
|
-
|
|
2175
|
-
path,
|
|
2176
|
-
action
|
|
2222
|
+
return this.invokeWithRetry(
|
|
2223
|
+
`${this.host}/invoke`,
|
|
2224
|
+
() => this.context.invoke(this.context.session, "kv", path, action),
|
|
2225
|
+
{ headers: extraHeaders, body, signal }
|
|
2177
2226
|
);
|
|
2178
|
-
const requestHeaders = Array.isArray(headers) ? [...headers, ...Object.entries(extraHeaders ?? {})] : { ...headers, ...extraHeaders };
|
|
2179
|
-
return this.context.fetch(`${this.host}/invoke`, {
|
|
2180
|
-
method: "POST",
|
|
2181
|
-
headers: requestHeaders,
|
|
2182
|
-
body,
|
|
2183
|
-
signal
|
|
2184
|
-
});
|
|
2185
2227
|
}
|
|
2186
2228
|
/**
|
|
2187
2229
|
* Serialize a single put value into a fetch body.
|
|
@@ -2387,28 +2429,28 @@ var KVService = class extends BaseService {
|
|
|
2387
2429
|
}
|
|
2388
2430
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
2389
2431
|
try {
|
|
2390
|
-
const session = this.context.session;
|
|
2391
|
-
const invocationHeaders = this.context.invokeAny(
|
|
2392
|
-
session,
|
|
2393
|
-
paths.map((path) => ({
|
|
2394
|
-
spaceId: session.spaceId,
|
|
2395
|
-
service: "kv",
|
|
2396
|
-
path,
|
|
2397
|
-
action
|
|
2398
|
-
}))
|
|
2399
|
-
);
|
|
2400
2432
|
const limitHeaders = {};
|
|
2401
2433
|
if (getOptions?.maxResponseBytes !== void 0) {
|
|
2402
2434
|
limitHeaders["x-tinycloud-max-response-bytes"] = String(
|
|
2403
2435
|
getOptions.maxResponseBytes
|
|
2404
2436
|
);
|
|
2405
2437
|
}
|
|
2406
|
-
const
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
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
|
+
);
|
|
2412
2454
|
if (!response.ok) {
|
|
2413
2455
|
const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
|
|
2414
2456
|
if (response.status === 401 || response.status === 403) {
|
|
@@ -2454,10 +2496,10 @@ var KVService = class extends BaseService {
|
|
|
2454
2496
|
result: err(serviceError(item.error.code, item.error.message, "kv"))
|
|
2455
2497
|
};
|
|
2456
2498
|
}
|
|
2457
|
-
const
|
|
2499
|
+
const headers = item.headers;
|
|
2458
2500
|
const data = action === KVAction.HEAD ? void 0 : this.parseBatchValue(
|
|
2459
2501
|
item.dataBase64,
|
|
2460
|
-
|
|
2502
|
+
headers,
|
|
2461
2503
|
getOptions?.raw,
|
|
2462
2504
|
getOptions?.binary
|
|
2463
2505
|
);
|
|
@@ -2465,7 +2507,7 @@ var KVService = class extends BaseService {
|
|
|
2465
2507
|
key: keys[index],
|
|
2466
2508
|
result: ok({
|
|
2467
2509
|
data,
|
|
2468
|
-
headers: this.createBatchResponseHeaders(
|
|
2510
|
+
headers: this.createBatchResponseHeaders(headers)
|
|
2469
2511
|
})
|
|
2470
2512
|
};
|
|
2471
2513
|
});
|
|
@@ -2700,7 +2742,7 @@ var KVService = class extends BaseService {
|
|
|
2700
2742
|
));
|
|
2701
2743
|
}
|
|
2702
2744
|
const data = await this.parseResponse(response, options?.raw, options?.binary);
|
|
2703
|
-
return ok({ data, headers: this.createResponseHeaders(response.headers) });
|
|
2745
|
+
return ok({ data, headers: this.createResponseHeaders(response.headers), ...response.retry && { retry: response.retry } });
|
|
2704
2746
|
}
|
|
2705
2747
|
async batchGet(keys, options) {
|
|
2706
2748
|
return this.withTelemetry(
|
|
@@ -2790,7 +2832,8 @@ var KVService = class extends BaseService {
|
|
|
2790
2832
|
}
|
|
2791
2833
|
return ok({
|
|
2792
2834
|
data: void 0,
|
|
2793
|
-
headers: this.createResponseHeaders(response.headers)
|
|
2835
|
+
headers: this.createResponseHeaders(response.headers),
|
|
2836
|
+
...response.retry && { retry: response.retry }
|
|
2794
2837
|
});
|
|
2795
2838
|
}
|
|
2796
2839
|
/**
|
|
@@ -2863,25 +2906,23 @@ var KVService = class extends BaseService {
|
|
|
2863
2906
|
entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
|
|
2864
2907
|
);
|
|
2865
2908
|
}
|
|
2866
|
-
const
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
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 }
|
|
2874
2925
|
);
|
|
2875
|
-
const url = `${this.host}/invoke`;
|
|
2876
|
-
const init = {
|
|
2877
|
-
method: "POST",
|
|
2878
|
-
headers,
|
|
2879
|
-
body,
|
|
2880
|
-
signal
|
|
2881
|
-
};
|
|
2882
|
-
const fetchFn = this.context.fetch;
|
|
2883
|
-
requestMayHaveDispatched = true;
|
|
2884
|
-
const response = await fetchFn(url, init);
|
|
2885
2926
|
if (!response.ok) {
|
|
2886
2927
|
let errorText;
|
|
2887
2928
|
try {
|
|
@@ -3351,7 +3392,8 @@ var KVService = class extends BaseService {
|
|
|
3351
3392
|
}
|
|
3352
3393
|
return ok({
|
|
3353
3394
|
data: void 0,
|
|
3354
|
-
headers: this.createResponseHeaders(response.headers)
|
|
3395
|
+
headers: this.createResponseHeaders(response.headers),
|
|
3396
|
+
...response.retry && { retry: response.retry }
|
|
3355
3397
|
});
|
|
3356
3398
|
}
|
|
3357
3399
|
/**
|
|
@@ -3425,12 +3467,6 @@ var KVService = class extends BaseService {
|
|
|
3425
3467
|
}
|
|
3426
3468
|
const path = this.getFullPath(key, options?.prefix);
|
|
3427
3469
|
const session = this.context.session;
|
|
3428
|
-
const headers = this.context.invoke(
|
|
3429
|
-
session,
|
|
3430
|
-
"kv",
|
|
3431
|
-
path,
|
|
3432
|
-
KVAction.GET
|
|
3433
|
-
);
|
|
3434
3470
|
const body = {
|
|
3435
3471
|
space: session.spaceId,
|
|
3436
3472
|
path,
|
|
@@ -3444,12 +3480,15 @@ var KVService = class extends BaseService {
|
|
|
3444
3480
|
}
|
|
3445
3481
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
3446
3482
|
try {
|
|
3447
|
-
const response = await this.
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
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
|
+
);
|
|
3453
3492
|
if (!response.ok) {
|
|
3454
3493
|
return await this.createSignedReadUrlError(response, key, path);
|
|
3455
3494
|
}
|
|
@@ -3946,18 +3985,19 @@ var SQLService = class extends BaseService {
|
|
|
3946
3985
|
}
|
|
3947
3986
|
// === Private helpers ===
|
|
3948
3987
|
async invokeSQL(dbName, actions, body, signal) {
|
|
3949
|
-
const session = this.context.session;
|
|
3950
3988
|
const actionList = Array.isArray(actions) ? actions : [actions];
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
"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);
|
|
3957
3994
|
},
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3995
|
+
{
|
|
3996
|
+
headers: { "Content-Type": "application/json" },
|
|
3997
|
+
body: JSON.stringify(body),
|
|
3998
|
+
signal
|
|
3999
|
+
}
|
|
4000
|
+
);
|
|
3961
4001
|
}
|
|
3962
4002
|
actionForSql(sql, fallback) {
|
|
3963
4003
|
const token = firstSqlToken(sql);
|
|
@@ -4418,22 +4458,20 @@ var DuckDbService = class extends BaseService {
|
|
|
4418
4458
|
}
|
|
4419
4459
|
const request = this.createRequestSignal(options?.signal);
|
|
4420
4460
|
try {
|
|
4421
|
-
const
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
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
|
+
}
|
|
4427
4474
|
);
|
|
4428
|
-
const response = await this.context.fetch(`${this.host}/invoke`, {
|
|
4429
|
-
method: "POST",
|
|
4430
|
-
headers: {
|
|
4431
|
-
...headers,
|
|
4432
|
-
"Content-Type": "application/x-duckdb"
|
|
4433
|
-
},
|
|
4434
|
-
body: new Blob([data]),
|
|
4435
|
-
signal: request.signal
|
|
4436
|
-
});
|
|
4437
4475
|
if (!response.ok) {
|
|
4438
4476
|
return await this.handleErrorResponse(response, "import");
|
|
4439
4477
|
}
|
|
@@ -4447,18 +4485,15 @@ var DuckDbService = class extends BaseService {
|
|
|
4447
4485
|
}
|
|
4448
4486
|
// === Private helpers ===
|
|
4449
4487
|
async invokeDuckDb(dbName, action, body, signal, extraHeaders) {
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
body: JSON.stringify(body),
|
|
4460
|
-
signal
|
|
4461
|
-
});
|
|
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
|
+
);
|
|
4462
4497
|
}
|
|
4463
4498
|
async handleErrorResponse(response, operation) {
|
|
4464
4499
|
const errorText = await response.text();
|
|
@@ -4647,26 +4682,24 @@ var HooksService = class extends BaseService {
|
|
|
4647
4682
|
);
|
|
4648
4683
|
}
|
|
4649
4684
|
try {
|
|
4650
|
-
const response = await this.
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
})
|
|
4669
|
-
});
|
|
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
|
+
);
|
|
4670
4703
|
if (!response.ok) {
|
|
4671
4704
|
return err(
|
|
4672
4705
|
await responseError("hooks", "failed to register webhook", response)
|
|
@@ -4704,17 +4737,13 @@ var HooksService = class extends BaseService {
|
|
|
4704
4737
|
query.set("prefix", normalizedPrefix);
|
|
4705
4738
|
}
|
|
4706
4739
|
}
|
|
4707
|
-
const response = await this.
|
|
4740
|
+
const response = await this.invokeWithRetry(
|
|
4708
4741
|
`${this.host}/hooks/webhooks${query.size > 0 ? `?${query.toString()}` : ""}`,
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
options.service ? buildScopePath(options.service, options.pathPrefix) : "webhooks"
|
|
4715
|
-
)
|
|
4716
|
-
)
|
|
4717
|
-
}
|
|
4742
|
+
() => this.createHookHeaders(
|
|
4743
|
+
"tinycloud.hooks/list",
|
|
4744
|
+
options.service ? buildScopePath(options.service, options.pathPrefix) : "webhooks"
|
|
4745
|
+
),
|
|
4746
|
+
{ method: "GET" }
|
|
4718
4747
|
);
|
|
4719
4748
|
if (!response.ok) {
|
|
4720
4749
|
return err(
|
|
@@ -4741,20 +4770,16 @@ var HooksService = class extends BaseService {
|
|
|
4741
4770
|
return err(authRequiredError("hooks"));
|
|
4742
4771
|
}
|
|
4743
4772
|
try {
|
|
4744
|
-
const response = await this.
|
|
4773
|
+
const response = await this.invokeWithRetry(
|
|
4745
4774
|
`${this.host}/hooks/webhooks/${encodeURIComponent(id)}`,
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
) : `webhooks/${id}`
|
|
4755
|
-
)
|
|
4756
|
-
)
|
|
4757
|
-
}
|
|
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" }
|
|
4758
4783
|
);
|
|
4759
4784
|
if (!response.ok) {
|
|
4760
4785
|
return err(
|
|
@@ -5210,23 +5235,7 @@ function readProperty(value, key) {
|
|
|
5210
5235
|
return void 0;
|
|
5211
5236
|
}
|
|
5212
5237
|
}
|
|
5213
|
-
|
|
5214
|
-
return new Promise((resolve) => {
|
|
5215
|
-
const finish = () => {
|
|
5216
|
-
clearTimeout(timer);
|
|
5217
|
-
signal.removeEventListener("abort", stop);
|
|
5218
|
-
resolve();
|
|
5219
|
-
};
|
|
5220
|
-
const timer = setTimeout(finish, delayMs);
|
|
5221
|
-
timer.unref?.();
|
|
5222
|
-
const stop = () => finish();
|
|
5223
|
-
if (signal.aborted) {
|
|
5224
|
-
finish();
|
|
5225
|
-
} else {
|
|
5226
|
-
signal.addEventListener("abort", stop, { once: true });
|
|
5227
|
-
}
|
|
5228
|
-
});
|
|
5229
|
-
}
|
|
5238
|
+
var sleep = (delayMs, _attempt, signal) => abortableSleep(delayMs, signal, true);
|
|
5230
5239
|
function abortPromise(signal) {
|
|
5231
5240
|
let resolve;
|
|
5232
5241
|
const promise = new Promise((done) => {
|
|
@@ -5611,7 +5620,7 @@ var StorageFullMonitor = class {
|
|
|
5611
5620
|
var UNKNOWN_OWNER = "\0unknown";
|
|
5612
5621
|
|
|
5613
5622
|
// src/storage/status.ts
|
|
5614
|
-
var STORAGE_MANAGE_URL = "https://account.tinycloud.xyz/
|
|
5623
|
+
var STORAGE_MANAGE_URL = "https://account.tinycloud.xyz/storage";
|
|
5615
5624
|
var STORAGE_NEARLY_FULL_RATIO = 0.9;
|
|
5616
5625
|
function storageUsageState(usedBytes, limitBytes) {
|
|
5617
5626
|
if (limitBytes === null) return "ok";
|
|
@@ -5643,40 +5652,150 @@ function parseStorageStatus(body) {
|
|
|
5643
5652
|
if (status.account.plan) status.plan = status.account.plan;
|
|
5644
5653
|
status.state = storageUsageState(account.usedBytes, account.limitBytes);
|
|
5645
5654
|
}
|
|
5646
|
-
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;
|
|
5647
5732
|
}
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
constructor(config = {}) {
|
|
5652
|
-
this.quotaUrl = null;
|
|
5653
|
-
this.config = config;
|
|
5733
|
+
function hexDecode(hex) {
|
|
5734
|
+
if (hex.length % 2 !== 0) {
|
|
5735
|
+
throw new Error("hex string must have even length");
|
|
5654
5736
|
}
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
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;
|
|
5658
5745
|
}
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
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] : "=";
|
|
5662
5759
|
}
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
const data = await resp.json();
|
|
5671
|
-
return {
|
|
5672
|
-
limitBytes: data.storage_limit_bytes ?? 0
|
|
5673
|
-
};
|
|
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");
|
|
5674
5767
|
}
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
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;
|
|
5678
5786
|
}
|
|
5679
|
-
|
|
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
|
+
}
|
|
5680
5799
|
|
|
5681
5800
|
// src/vault/types.ts
|
|
5682
5801
|
var VaultPublicSpaceKVActions = [
|
|
@@ -5847,7 +5966,7 @@ function toBytes(str) {
|
|
|
5847
5966
|
function fromBytes(bytes) {
|
|
5848
5967
|
return new TextDecoder().decode(bytes);
|
|
5849
5968
|
}
|
|
5850
|
-
function
|
|
5969
|
+
function hexEncode2(bytes) {
|
|
5851
5970
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
5852
5971
|
}
|
|
5853
5972
|
function concatBytes(...arrays) {
|
|
@@ -5860,14 +5979,14 @@ function concatBytes(...arrays) {
|
|
|
5860
5979
|
}
|
|
5861
5980
|
return result;
|
|
5862
5981
|
}
|
|
5863
|
-
function
|
|
5982
|
+
function base64Encode2(bytes) {
|
|
5864
5983
|
let binary = "";
|
|
5865
5984
|
for (let i = 0; i < bytes.length; i++) {
|
|
5866
5985
|
binary += String.fromCharCode(bytes[i]);
|
|
5867
5986
|
}
|
|
5868
5987
|
return btoa(binary);
|
|
5869
5988
|
}
|
|
5870
|
-
function
|
|
5989
|
+
function base64Decode2(str) {
|
|
5871
5990
|
const binary = atob(str);
|
|
5872
5991
|
const bytes = new Uint8Array(binary.length);
|
|
5873
5992
|
for (let i = 0; i < binary.length; i++) {
|
|
@@ -5909,6 +6028,11 @@ function defaultVaultMessage(input) {
|
|
|
5909
6028
|
return input.message;
|
|
5910
6029
|
case "PERMISSION_DENIED":
|
|
5911
6030
|
return input.message;
|
|
6031
|
+
case "AAD_MISMATCH":
|
|
6032
|
+
case "ETAG_MISMATCH":
|
|
6033
|
+
case "INVALID_ETAG":
|
|
6034
|
+
case "OUTCOME_UNKNOWN":
|
|
6035
|
+
case "NODE_BUSY":
|
|
5912
6036
|
case "STORAGE_QUOTA_EXCEEDED":
|
|
5913
6037
|
case "STORAGE_LIMIT_REACHED":
|
|
5914
6038
|
return input.message;
|
|
@@ -5931,6 +6055,37 @@ function typedKvCause(error) {
|
|
|
5931
6055
|
...error.cause !== void 0 ? { cause: error.cause } : {}
|
|
5932
6056
|
});
|
|
5933
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
|
+
}
|
|
5934
6089
|
function kvWriteError(context, error) {
|
|
5935
6090
|
if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
|
|
5936
6091
|
return vaultError({ code: error.code, message: error.message, meta: error.meta });
|
|
@@ -6117,7 +6272,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6117
6272
|
}
|
|
6118
6273
|
if (existingPubKey) {
|
|
6119
6274
|
this.encryptionIdentity = {
|
|
6120
|
-
publicKey:
|
|
6275
|
+
publicKey: base64Decode2(existingPubKey),
|
|
6121
6276
|
privateKey: new Uint8Array(0)
|
|
6122
6277
|
// private key not available without signing
|
|
6123
6278
|
};
|
|
@@ -6142,7 +6297,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6142
6297
|
);
|
|
6143
6298
|
this.encryptionIdentity = this.crypto.x25519FromSeed(seed);
|
|
6144
6299
|
try {
|
|
6145
|
-
const pubKeyB64 =
|
|
6300
|
+
const pubKeyB64 = base64Encode2(this.encryptionIdentity.publicKey);
|
|
6146
6301
|
await this.tc.ensurePublicSpace();
|
|
6147
6302
|
await this.tc.publicKV.put(".well-known/vault-pubkey", pubKeyB64);
|
|
6148
6303
|
await this.tc.publicKV.put(".well-known/vault-version", CURRENT_VAULT_VERSION);
|
|
@@ -6208,6 +6363,21 @@ var DataVaultService = class extends BaseService {
|
|
|
6208
6363
|
message: "Authentication required"
|
|
6209
6364
|
});
|
|
6210
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
|
+
}
|
|
6211
6381
|
try {
|
|
6212
6382
|
const { plaintext, contentType } = this.serializeValue(value, options);
|
|
6213
6383
|
const metadata = {
|
|
@@ -6230,7 +6400,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6230
6400
|
}
|
|
6231
6401
|
const valuePath = `vault/${key}`;
|
|
6232
6402
|
const valueBody = JSON.stringify(envelopeResult.data);
|
|
6233
|
-
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);
|
|
6234
6405
|
if (!valuePutResult.ok) {
|
|
6235
6406
|
if (options?.ifNoneMatch !== void 0 && valuePutResult.error.code === ErrorCodes.KV_PRECONDITION_FAILED) {
|
|
6236
6407
|
return vaultError({
|
|
@@ -6240,6 +6411,10 @@ var DataVaultService = class extends BaseService {
|
|
|
6240
6411
|
cause: typedKvCause(valuePutResult.error)
|
|
6241
6412
|
});
|
|
6242
6413
|
}
|
|
6414
|
+
if (options?.ifMatch !== void 0) {
|
|
6415
|
+
const classified = ifMatchWriteError(key, valuePutResult.error);
|
|
6416
|
+
if (classified !== void 0) return classified;
|
|
6417
|
+
}
|
|
6243
6418
|
return kvWriteError("Failed to store encrypted value", valuePutResult.error);
|
|
6244
6419
|
}
|
|
6245
6420
|
return { ok: true, data: void 0 };
|
|
@@ -6274,6 +6449,19 @@ var DataVaultService = class extends BaseService {
|
|
|
6274
6449
|
}
|
|
6275
6450
|
const rawEnvelope = unwrapKVData(valueResult.data);
|
|
6276
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
|
+
}
|
|
6277
6465
|
const proof = await this.decryptCapabilityProof();
|
|
6278
6466
|
const plaintextResult = await config.service.decryptEnvelope(envelope, proof);
|
|
6279
6467
|
if (!plaintextResult.ok) {
|
|
@@ -6290,7 +6478,11 @@ var DataVaultService = class extends BaseService {
|
|
|
6290
6478
|
contentType,
|
|
6291
6479
|
options
|
|
6292
6480
|
);
|
|
6293
|
-
|
|
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
|
+
};
|
|
6294
6486
|
} catch (error) {
|
|
6295
6487
|
return vaultError({
|
|
6296
6488
|
code: "STORAGE_ERROR",
|
|
@@ -6336,6 +6528,12 @@ var DataVaultService = class extends BaseService {
|
|
|
6336
6528
|
} catch {
|
|
6337
6529
|
return { status: "corrupt_envelope" };
|
|
6338
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
|
+
}
|
|
6339
6537
|
let plaintextResult;
|
|
6340
6538
|
try {
|
|
6341
6539
|
const proof = await this.decryptCapabilityProof();
|
|
@@ -6367,7 +6565,11 @@ var DataVaultService = class extends BaseService {
|
|
|
6367
6565
|
contentType,
|
|
6368
6566
|
options
|
|
6369
6567
|
);
|
|
6370
|
-
|
|
6568
|
+
const etag = valueResult.data.headers?.etag;
|
|
6569
|
+
return {
|
|
6570
|
+
status: "ok",
|
|
6571
|
+
entry: { value, metadata, keyId, ...etag === void 0 ? {} : { etag } }
|
|
6572
|
+
};
|
|
6371
6573
|
} catch {
|
|
6372
6574
|
return { status: "invalid_payload" };
|
|
6373
6575
|
}
|
|
@@ -6417,6 +6619,14 @@ var DataVaultService = class extends BaseService {
|
|
|
6417
6619
|
)
|
|
6418
6620
|
});
|
|
6419
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
|
+
}
|
|
6420
6630
|
if (!this._isUnlocked || !this.masterKey) {
|
|
6421
6631
|
return vaultError({
|
|
6422
6632
|
code: "VAULT_LOCKED",
|
|
@@ -6442,7 +6652,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6442
6652
|
}
|
|
6443
6653
|
const contentType = options?.contentType ?? (value instanceof Uint8Array ? "application/octet-stream" : "application/json");
|
|
6444
6654
|
const entryKey = this.crypto.randomBytes(32);
|
|
6445
|
-
const keyId =
|
|
6655
|
+
const keyId = hexEncode2(this.crypto.sha256(entryKey)).slice(0, 16);
|
|
6446
6656
|
const encrypted = this.crypto.encrypt(entryKey, plaintext);
|
|
6447
6657
|
const keyBlob = this.crypto.encrypt(this.masterKey, entryKey);
|
|
6448
6658
|
const metadata = {
|
|
@@ -6460,7 +6670,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6460
6670
|
...metadata
|
|
6461
6671
|
});
|
|
6462
6672
|
const keyPayload = JSON.stringify({
|
|
6463
|
-
key:
|
|
6673
|
+
key: base64Encode2(keyBlob),
|
|
6464
6674
|
metadata: keyMetadata
|
|
6465
6675
|
});
|
|
6466
6676
|
const keyPutResult = await this.tc.kv.put(
|
|
@@ -6471,7 +6681,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6471
6681
|
return kvWriteError("Failed to store key blob", keyPutResult.error);
|
|
6472
6682
|
}
|
|
6473
6683
|
const valuePayload = JSON.stringify({
|
|
6474
|
-
data:
|
|
6684
|
+
data: base64Encode2(encrypted),
|
|
6475
6685
|
metadata
|
|
6476
6686
|
});
|
|
6477
6687
|
const valuePutResult = await this.tc.kv.put(
|
|
@@ -6502,6 +6712,12 @@ var DataVaultService = class extends BaseService {
|
|
|
6502
6712
|
if (this.usesNetworkEncryption) {
|
|
6503
6713
|
return this.getNetworkEncrypted(key, options);
|
|
6504
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
|
+
}
|
|
6505
6721
|
if (!this._isUnlocked || !this.masterKey) {
|
|
6506
6722
|
return vaultError({
|
|
6507
6723
|
code: "VAULT_LOCKED",
|
|
@@ -6523,7 +6739,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6523
6739
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6524
6740
|
}
|
|
6525
6741
|
const keyEnvelope = JSON.parse(keyResult.data.data);
|
|
6526
|
-
const keyBlobBytes =
|
|
6742
|
+
const keyBlobBytes = base64Decode2(keyEnvelope.key);
|
|
6527
6743
|
const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
|
|
6528
6744
|
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
6529
6745
|
raw: true,
|
|
@@ -6533,7 +6749,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6533
6749
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6534
6750
|
}
|
|
6535
6751
|
const valueEnvelope = JSON.parse(valueResult.data.data);
|
|
6536
|
-
const encryptedBytes =
|
|
6752
|
+
const encryptedBytes = base64Decode2(valueEnvelope.data);
|
|
6537
6753
|
const plaintext = this.crypto.decrypt(entryKey, encryptedBytes);
|
|
6538
6754
|
const metadata = valueEnvelope.metadata ?? {};
|
|
6539
6755
|
const contentType = metadata[VaultHeaders.CONTENT_TYPE] ?? "application/json";
|
|
@@ -6781,7 +6997,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6781
6997
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6782
6998
|
}
|
|
6783
6999
|
const keyEnvelope = JSON.parse(keyResult.data.data);
|
|
6784
|
-
const keyBlobBytes =
|
|
7000
|
+
const keyBlobBytes = base64Decode2(keyEnvelope.key);
|
|
6785
7001
|
const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
|
|
6786
7002
|
const ephemeralSeed = this.crypto.randomBytes(32);
|
|
6787
7003
|
const ephemeralKeyPair = this.crypto.x25519FromSeed(ephemeralSeed);
|
|
@@ -6800,7 +7016,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6800
7016
|
encryptedGrant
|
|
6801
7017
|
);
|
|
6802
7018
|
const grantPayload = JSON.stringify({
|
|
6803
|
-
grant:
|
|
7019
|
+
grant: base64Encode2(grantBlob),
|
|
6804
7020
|
spaceId: this.vaultConfig.spaceId,
|
|
6805
7021
|
metadata: {
|
|
6806
7022
|
[VaultHeaders.GRANT_VERSION]: "1",
|
|
@@ -6840,6 +7056,14 @@ var DataVaultService = class extends BaseService {
|
|
|
6840
7056
|
*/
|
|
6841
7057
|
async getShared(grantorDID, key, options) {
|
|
6842
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
|
+
}
|
|
6843
7067
|
if (this.usesNetworkEncryption) {
|
|
6844
7068
|
const grantorKV = options?.kv;
|
|
6845
7069
|
if (!grantorKV) {
|
|
@@ -6934,7 +7158,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6934
7158
|
});
|
|
6935
7159
|
}
|
|
6936
7160
|
const grantEnvelope = typeof grantResult.data?.data === "string" ? JSON.parse(grantResult.data.data) : grantResult.data?.data;
|
|
6937
|
-
const grantBlobBytes =
|
|
7161
|
+
const grantBlobBytes = base64Decode2(grantEnvelope.grant);
|
|
6938
7162
|
const ephemeralPubKey = grantBlobBytes.slice(0, 32);
|
|
6939
7163
|
const encryptedGrant = grantBlobBytes.slice(32);
|
|
6940
7164
|
const sharedSecret = this.crypto.x25519Dh(
|
|
@@ -6957,7 +7181,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6957
7181
|
});
|
|
6958
7182
|
}
|
|
6959
7183
|
const valueEnvelope = typeof valueResult.data?.data === "string" ? JSON.parse(valueResult.data.data) : valueResult.data?.data;
|
|
6960
|
-
const encryptedBytes =
|
|
7184
|
+
const encryptedBytes = base64Decode2(valueEnvelope.data);
|
|
6961
7185
|
const plaintext = this.crypto.decrypt(entryKey, encryptedBytes);
|
|
6962
7186
|
const metadata = valueEnvelope.metadata ?? {};
|
|
6963
7187
|
const contentType = metadata[VaultHeaders.CONTENT_TYPE] ?? "application/json";
|
|
@@ -7011,7 +7235,7 @@ var DataVaultService = class extends BaseService {
|
|
|
7011
7235
|
if (!result.ok) {
|
|
7012
7236
|
return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
|
|
7013
7237
|
}
|
|
7014
|
-
const pubKeyBytes =
|
|
7238
|
+
const pubKeyBytes = base64Decode2(result.data);
|
|
7015
7239
|
return { ok: true, data: pubKeyBytes };
|
|
7016
7240
|
} catch (error) {
|
|
7017
7241
|
return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
|
|
@@ -7130,7 +7354,7 @@ var DataVaultService = class extends BaseService {
|
|
|
7130
7354
|
}
|
|
7131
7355
|
const currentEntry = getResult.data;
|
|
7132
7356
|
const newEntryKey = this.crypto.randomBytes(32);
|
|
7133
|
-
const newKeyId =
|
|
7357
|
+
const newKeyId = hexEncode2(this.crypto.sha256(newEntryKey)).slice(
|
|
7134
7358
|
0,
|
|
7135
7359
|
16
|
|
7136
7360
|
);
|
|
@@ -7147,7 +7371,7 @@ var DataVaultService = class extends BaseService {
|
|
|
7147
7371
|
[VaultHeaders.KEY_ID]: newKeyId
|
|
7148
7372
|
};
|
|
7149
7373
|
const keyPayload = JSON.stringify({
|
|
7150
|
-
key:
|
|
7374
|
+
key: base64Encode2(newKeyBlob),
|
|
7151
7375
|
metadata: JSON.stringify({
|
|
7152
7376
|
keyId: newKeyId,
|
|
7153
7377
|
...metadata
|
|
@@ -7161,7 +7385,7 @@ var DataVaultService = class extends BaseService {
|
|
|
7161
7385
|
return kvWriteError("Failed to store rotated key blob", keyPutResult.error);
|
|
7162
7386
|
}
|
|
7163
7387
|
const valuePayload = JSON.stringify({
|
|
7164
|
-
data:
|
|
7388
|
+
data: base64Encode2(encrypted),
|
|
7165
7389
|
metadata
|
|
7166
7390
|
});
|
|
7167
7391
|
const valuePutResult = await this.tc.kv.put(
|
|
@@ -7453,116 +7677,6 @@ var SecretsService = class {
|
|
|
7453
7677
|
}
|
|
7454
7678
|
};
|
|
7455
7679
|
|
|
7456
|
-
// src/encryption/canonical.ts
|
|
7457
|
-
function canonicalize(value) {
|
|
7458
|
-
if (value === void 0) {
|
|
7459
|
-
return "";
|
|
7460
|
-
}
|
|
7461
|
-
return stringify(value);
|
|
7462
|
-
}
|
|
7463
|
-
function stringify(value) {
|
|
7464
|
-
if (value === null) return "null";
|
|
7465
|
-
switch (typeof value) {
|
|
7466
|
-
case "boolean":
|
|
7467
|
-
case "number":
|
|
7468
|
-
return JSON.stringify(value);
|
|
7469
|
-
case "string":
|
|
7470
|
-
return JSON.stringify(value);
|
|
7471
|
-
case "object": {
|
|
7472
|
-
if (Array.isArray(value)) {
|
|
7473
|
-
return `[${value.map(stringify).join(",")}]`;
|
|
7474
|
-
}
|
|
7475
|
-
const keys = Object.keys(value).sort();
|
|
7476
|
-
const parts = [];
|
|
7477
|
-
for (const k of keys) {
|
|
7478
|
-
const v = value[k];
|
|
7479
|
-
if (v === void 0) continue;
|
|
7480
|
-
parts.push(`${JSON.stringify(k)}:${stringify(v)}`);
|
|
7481
|
-
}
|
|
7482
|
-
return `{${parts.join(",")}}`;
|
|
7483
|
-
}
|
|
7484
|
-
default:
|
|
7485
|
-
throw new TypeError(
|
|
7486
|
-
`canonicalize: unsupported value type ${typeof value}`
|
|
7487
|
-
);
|
|
7488
|
-
}
|
|
7489
|
-
}
|
|
7490
|
-
var HEX = "0123456789abcdef";
|
|
7491
|
-
function hexEncode2(bytes) {
|
|
7492
|
-
let out = "";
|
|
7493
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
7494
|
-
const b = bytes[i];
|
|
7495
|
-
out += HEX[b >> 4 & 15] + HEX[b & 15];
|
|
7496
|
-
}
|
|
7497
|
-
return out;
|
|
7498
|
-
}
|
|
7499
|
-
function hexDecode(hex) {
|
|
7500
|
-
if (hex.length % 2 !== 0) {
|
|
7501
|
-
throw new Error("hex string must have even length");
|
|
7502
|
-
}
|
|
7503
|
-
const out = new Uint8Array(hex.length / 2);
|
|
7504
|
-
for (let i = 0; i < out.length; i++) {
|
|
7505
|
-
const hi = parseInt(hex[i * 2], 16);
|
|
7506
|
-
const lo = parseInt(hex[i * 2 + 1], 16);
|
|
7507
|
-
if (Number.isNaN(hi) || Number.isNaN(lo)) {
|
|
7508
|
-
throw new Error("invalid hex character");
|
|
7509
|
-
}
|
|
7510
|
-
out[i] = hi << 4 | lo;
|
|
7511
|
-
}
|
|
7512
|
-
return out;
|
|
7513
|
-
}
|
|
7514
|
-
function base64Encode2(bytes) {
|
|
7515
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
7516
|
-
let out = "";
|
|
7517
|
-
for (let i = 0; i < bytes.length; i += 3) {
|
|
7518
|
-
const b0 = bytes[i];
|
|
7519
|
-
const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
7520
|
-
const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
7521
|
-
out += chars[b0 >> 2 & 63];
|
|
7522
|
-
out += chars[(b0 << 4 | b1 >> 4) & 63];
|
|
7523
|
-
out += i + 1 < bytes.length ? chars[(b1 << 2 | b2 >> 6) & 63] : "=";
|
|
7524
|
-
out += i + 2 < bytes.length ? chars[b2 & 63] : "=";
|
|
7525
|
-
}
|
|
7526
|
-
return out;
|
|
7527
|
-
}
|
|
7528
|
-
function base64Decode2(s) {
|
|
7529
|
-
const clean = s.replace(/[^A-Za-z0-9+/=]/g, "");
|
|
7530
|
-
const len = clean.length;
|
|
7531
|
-
if (len % 4 !== 0) {
|
|
7532
|
-
throw new Error("invalid base64 input");
|
|
7533
|
-
}
|
|
7534
|
-
const padding = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
|
|
7535
|
-
const outLen = len / 4 * 3 - padding;
|
|
7536
|
-
const out = new Uint8Array(outLen);
|
|
7537
|
-
const lookup = {};
|
|
7538
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
7539
|
-
for (let i = 0; i < chars.length; i++) lookup[chars[i]] = i;
|
|
7540
|
-
let outIdx = 0;
|
|
7541
|
-
for (let i = 0; i < len; i += 4) {
|
|
7542
|
-
const v0 = lookup[clean[i]] ?? 0;
|
|
7543
|
-
const v1 = lookup[clean[i + 1]] ?? 0;
|
|
7544
|
-
const v2 = clean[i + 2] === "=" ? 0 : lookup[clean[i + 2]] ?? 0;
|
|
7545
|
-
const v3 = clean[i + 3] === "=" ? 0 : lookup[clean[i + 3]] ?? 0;
|
|
7546
|
-
const b0 = v0 << 2 | v1 >> 4;
|
|
7547
|
-
const b1 = (v1 & 15) << 4 | v2 >> 2;
|
|
7548
|
-
const b2 = (v2 & 3) << 6 | v3;
|
|
7549
|
-
if (outIdx < outLen) out[outIdx++] = b0;
|
|
7550
|
-
if (outIdx < outLen) out[outIdx++] = b1;
|
|
7551
|
-
if (outIdx < outLen) out[outIdx++] = b2;
|
|
7552
|
-
}
|
|
7553
|
-
return out;
|
|
7554
|
-
}
|
|
7555
|
-
function utf8Encode(s) {
|
|
7556
|
-
return new TextEncoder().encode(s);
|
|
7557
|
-
}
|
|
7558
|
-
function utf8Decode(b) {
|
|
7559
|
-
return new TextDecoder().decode(b);
|
|
7560
|
-
}
|
|
7561
|
-
function canonicalHashHex(sha256, value) {
|
|
7562
|
-
const canonical = canonicalize(value);
|
|
7563
|
-
return hexEncode2(sha256(utf8Encode(canonical)));
|
|
7564
|
-
}
|
|
7565
|
-
|
|
7566
7680
|
// src/encryption/types.ts
|
|
7567
7681
|
var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
|
|
7568
7682
|
var ENVELOPE_VERSION = 1;
|
|
@@ -7772,7 +7886,7 @@ function encryptToNetwork(crypto2, input) {
|
|
|
7772
7886
|
const symmetricKey = crypto2.randomBytes(32);
|
|
7773
7887
|
const ciphertext = crypto2.authEncrypt(symmetricKey, input.plaintext, input.aad);
|
|
7774
7888
|
const wrapped = crypto2.sealToNetworkKey(input.networkPublicKey, symmetricKey);
|
|
7775
|
-
const encryptedSymmetricKey =
|
|
7889
|
+
const encryptedSymmetricKey = base64Encode(wrapped);
|
|
7776
7890
|
const encryptedSymmetricKeyHash = canonicalHashHex(
|
|
7777
7891
|
crypto2.sha256,
|
|
7778
7892
|
encryptedSymmetricKey
|
|
@@ -7784,8 +7898,8 @@ function encryptToNetwork(crypto2, input) {
|
|
|
7784
7898
|
keyVersion,
|
|
7785
7899
|
encryptedSymmetricKey,
|
|
7786
7900
|
encryptedSymmetricKeyHash,
|
|
7787
|
-
ciphertext:
|
|
7788
|
-
...input.aad !== void 0 ? { aad:
|
|
7901
|
+
ciphertext: base64Encode(ciphertext),
|
|
7902
|
+
...input.aad !== void 0 ? { aad: base64Encode(input.aad) } : {},
|
|
7789
7903
|
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
7790
7904
|
};
|
|
7791
7905
|
return { envelope, symmetricKey };
|
|
@@ -7859,8 +7973,8 @@ function validateEnvelope(crypto2, envelope) {
|
|
|
7859
7973
|
return { ok: true, data: e };
|
|
7860
7974
|
}
|
|
7861
7975
|
function decryptEnvelopeWithKey(crypto2, envelope, symmetricKey) {
|
|
7862
|
-
const ciphertext =
|
|
7863
|
-
const aad = envelope.aad !== void 0 ?
|
|
7976
|
+
const ciphertext = base64Decode(envelope.ciphertext);
|
|
7977
|
+
const aad = envelope.aad !== void 0 ? base64Decode(envelope.aad) : void 0;
|
|
7864
7978
|
return crypto2.authDecrypt(symmetricKey, ciphertext, aad);
|
|
7865
7979
|
}
|
|
7866
7980
|
|
|
@@ -7878,7 +7992,7 @@ function buildCanonicalDecryptRequest(input) {
|
|
|
7878
7992
|
return { canonicalBody, bodyHash, receiverPublicKeyHash };
|
|
7879
7993
|
}
|
|
7880
7994
|
function buildDecryptFacts(input) {
|
|
7881
|
-
const bodyHash = input.canonicalBody !== void 0 ?
|
|
7995
|
+
const bodyHash = input.canonicalBody !== void 0 ? hexEncode(input.crypto.sha256(utf8Encode(input.canonicalBody))) : canonicalHashHex(
|
|
7882
7996
|
input.crypto.sha256,
|
|
7883
7997
|
input.body
|
|
7884
7998
|
);
|
|
@@ -8161,7 +8275,7 @@ function verifyDecryptResponse(input) {
|
|
|
8161
8275
|
})
|
|
8162
8276
|
};
|
|
8163
8277
|
}
|
|
8164
|
-
const expectedRequestHash =
|
|
8278
|
+
const expectedRequestHash = hexEncode(
|
|
8165
8279
|
crypto2.sha256(utf8Encode(`${invocationCid}${requestBodyHash}`))
|
|
8166
8280
|
);
|
|
8167
8281
|
if (response.requestHash !== expectedRequestHash) {
|
|
@@ -8185,7 +8299,7 @@ function verifyDecryptResponse(input) {
|
|
|
8185
8299
|
const signedBytes = new TextEncoder().encode(
|
|
8186
8300
|
canonicalSignedResponse(response)
|
|
8187
8301
|
);
|
|
8188
|
-
const signatureBytes =
|
|
8302
|
+
const signatureBytes = base64Decode(response.nodeSignature);
|
|
8189
8303
|
if (!crypto2.verifyNodeSignature(response.nodeId, signedBytes, signatureBytes)) {
|
|
8190
8304
|
return {
|
|
8191
8305
|
ok: false,
|
|
@@ -8197,7 +8311,7 @@ function verifyDecryptResponse(input) {
|
|
|
8197
8311
|
return { ok: true, data: response };
|
|
8198
8312
|
}
|
|
8199
8313
|
function openWrappedKey(crypto2, receiverPrivateKey, response) {
|
|
8200
|
-
const wrapped =
|
|
8314
|
+
const wrapped = base64Decode(response.wrappedKey);
|
|
8201
8315
|
return crypto2.openWithReceiverKey(receiverPrivateKey, wrapped);
|
|
8202
8316
|
}
|
|
8203
8317
|
|
|
@@ -8241,7 +8355,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8241
8355
|
const usable = ensureNetworkUsableForDecrypt(discovered.data);
|
|
8242
8356
|
if (!usable.ok) return usable;
|
|
8243
8357
|
const descriptor = usable.data;
|
|
8244
|
-
const networkPublicKey =
|
|
8358
|
+
const networkPublicKey = base64Decode(descriptor.publicEncryptionKey);
|
|
8245
8359
|
const result = encryptToNetwork(this.crypto, {
|
|
8246
8360
|
// Discovery returns the node's canonical network identifier. Persist
|
|
8247
8361
|
// that spelling in the envelope so a later response can be compared
|
|
@@ -8269,7 +8383,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8269
8383
|
this.assertActive();
|
|
8270
8384
|
const validated = validateEnvelope(this.crypto, envelope);
|
|
8271
8385
|
if (!validated.ok) return validated;
|
|
8272
|
-
if (options?.aad !== void 0 && validated.data.aad !==
|
|
8386
|
+
if (options?.aad !== void 0 && validated.data.aad !== base64Encode(options.aad)) {
|
|
8273
8387
|
return encErr(
|
|
8274
8388
|
encryptionError({
|
|
8275
8389
|
code: "INVALID_INPUT",
|
|
@@ -8297,7 +8411,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8297
8411
|
);
|
|
8298
8412
|
}
|
|
8299
8413
|
const receiverKey = generateRandomReceiverKey({ crypto: this.crypto });
|
|
8300
|
-
const receiverPublicKey =
|
|
8414
|
+
const receiverPublicKey = base64Encode(receiverKey.publicKey);
|
|
8301
8415
|
const receiverPublicKeyHash = canonicalHashHex(
|
|
8302
8416
|
this.crypto.sha256,
|
|
8303
8417
|
receiverPublicKey
|