@tinycloud/sdk-services 3.11.0 → 3.12.0-beta.10
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-Ma2MwJAd.d.ts → BaseService-BRDG69KR.d.ts} +1 -1
- package/dist/{BaseService-O0iyuFLk.d.cts → BaseService-CdBkfPGE.d.cts} +1 -1
- package/dist/{IHooksService-4QNODVy6.d.cts → IHooksService-C9p3stsB.d.cts} +1 -1
- package/dist/{IHooksService-CvTpJbQD.d.ts → IHooksService-D6eyZat1.d.ts} +1 -1
- package/dist/{ISQLService-Aly1CBsW.d.ts → ISQLService-B_4KcGA-.d.ts} +1 -1
- package/dist/{ISQLService-BOgWzHLT.d.cts → ISQLService-C6nNpcls.d.cts} +1 -1
- package/dist/context.cjs +4 -0
- package/dist/context.cjs.map +1 -1
- package/dist/context.d.cts +1 -1
- package/dist/context.d.ts +1 -1
- package/dist/context.js +4 -0
- package/dist/context.js.map +1 -1
- package/dist/encryption/index.cjs +276 -14
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +26 -28
- package/dist/encryption/index.d.ts +26 -28
- package/dist/encryption/index.js +268 -14
- package/dist/encryption/index.js.map +1 -1
- package/dist/encryption/named-keys.cjs +225 -14
- package/dist/encryption/named-keys.cjs.map +1 -1
- package/dist/encryption/named-keys.d.cts +7 -2
- package/dist/encryption/named-keys.d.ts +7 -2
- package/dist/encryption/named-keys.js +225 -14
- package/dist/encryption/named-keys.js.map +1 -1
- package/dist/encryption/resource.cjs.map +1 -1
- package/dist/encryption/resource.js.map +1 -1
- package/dist/feed/index.cjs +4 -0
- package/dist/feed/index.cjs.map +1 -1
- package/dist/feed/index.d.cts +3 -3
- package/dist/feed/index.d.ts +3 -3
- package/dist/feed/index.js +4 -0
- package/dist/feed/index.js.map +1 -1
- package/dist/index.cjs +509 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -12
- package/dist/index.d.ts +14 -12
- package/dist/index.js +499 -18
- 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 +34 -0
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +4 -4
- package/dist/kv/index.d.ts +4 -4
- package/dist/kv/index.js +34 -0
- package/dist/kv/index.js.map +1 -1
- package/dist/kv/replication/index.cjs +39 -8
- 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 +38 -7
- package/dist/kv/replication/index.js.map +1 -1
- package/dist/{scope-DEgc9f-W.d.cts → scope-DtcbyRKY.d.cts} +1 -1
- package/dist/{scope-BZr0QwxX.d.ts → scope-J20PQP3u.d.ts} +1 -1
- package/dist/sql/index.cjs +36 -0
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +4 -4
- package/dist/sql/index.d.ts +4 -4
- package/dist/sql/index.js +36 -0
- package/dist/sql/index.js.map +1 -1
- package/dist/telemetry/requestLog.cjs +4 -0
- package/dist/telemetry/requestLog.cjs.map +1 -1
- package/dist/telemetry/requestLog.d.cts +1 -1
- package/dist/telemetry/requestLog.d.ts +1 -1
- package/dist/telemetry/requestLog.js +4 -0
- package/dist/telemetry/requestLog.js.map +1 -1
- package/dist/{types-D4XbIfFJ.d.cts → types-DfqNz9Nu.d.cts} +135 -6
- package/dist/{types-CSd0RYir.d.ts → types-DjAP8dve.d.cts} +4 -0
- package/dist/{types-CSd0RYir.d.cts → types-DjAP8dve.d.ts} +4 -0
- package/dist/{types-Bf91gup6.d.ts → types-Nvi_RckJ.d.ts} +135 -6
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,10 @@ var ErrorCodes = {
|
|
|
10
10
|
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
11
11
|
AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
|
|
12
12
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
13
|
+
NODE_NOT_RESPONDING: "NODE_NOT_RESPONDING",
|
|
14
|
+
NODE_IDENTITY_MISMATCH: "NODE_IDENTITY_MISMATCH",
|
|
15
|
+
PARTIAL_NODE_WRITE: "PARTIAL_NODE_WRITE",
|
|
16
|
+
RESOURCE_NOT_ROUTED: "RESOURCE_NOT_ROUTED",
|
|
13
17
|
TIMEOUT: "TIMEOUT",
|
|
14
18
|
ABORTED: "ABORTED",
|
|
15
19
|
INVALID_INPUT: "INVALID_INPUT",
|
|
@@ -784,6 +788,237 @@ function networkDiscoveryKey(name) {
|
|
|
784
788
|
var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
|
|
785
789
|
var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
|
|
786
790
|
|
|
791
|
+
// src/resourceRouting.ts
|
|
792
|
+
async function verifyResourceDestination(target, fetchFn) {
|
|
793
|
+
if (!target.nodeDid) return;
|
|
794
|
+
let info;
|
|
795
|
+
let keys;
|
|
796
|
+
try {
|
|
797
|
+
const signal = AbortSignal.timeout(5e3);
|
|
798
|
+
const responses = await Promise.all([
|
|
799
|
+
fetchFn(`${target.url}/info`, { signal, redirect: "error" }),
|
|
800
|
+
fetchFn(`${target.url}/.well-known/tinycloud/node-keys`, { signal, redirect: "error" })
|
|
801
|
+
]);
|
|
802
|
+
if (responses.some((response) => response.status >= 500)) throw new NodeNotRespondingError([target.url]);
|
|
803
|
+
if (responses.some((response) => !response.ok)) throw new NodeIdentityMismatchError(target.url, target.nodeDid);
|
|
804
|
+
try {
|
|
805
|
+
[info, keys] = await Promise.all(responses.map((response) => response.json()));
|
|
806
|
+
} catch {
|
|
807
|
+
throw new NodeIdentityMismatchError(target.url, target.nodeDid);
|
|
808
|
+
}
|
|
809
|
+
} catch (error) {
|
|
810
|
+
if (error instanceof NodeIdentityMismatchError || error instanceof NodeNotRespondingError) throw error;
|
|
811
|
+
throw new NodeNotRespondingError([target.url], error);
|
|
812
|
+
}
|
|
813
|
+
const infoDid = typeof info === "object" && info !== null && "nodeId" in info && typeof info.nodeId === "string" ? info.nodeId : void 0;
|
|
814
|
+
const keysDid = typeof keys === "object" && keys !== null && "nodeDid" in keys && typeof keys.nodeDid === "string" ? keys.nodeDid : void 0;
|
|
815
|
+
if (infoDid !== target.nodeDid || keysDid !== target.nodeDid) {
|
|
816
|
+
throw new NodeIdentityMismatchError(target.url, target.nodeDid, infoDid !== target.nodeDid ? infoDid : keysDid);
|
|
817
|
+
}
|
|
818
|
+
return info;
|
|
819
|
+
}
|
|
820
|
+
var NodeNotRespondingError = class extends Error {
|
|
821
|
+
constructor(hosts, cause) {
|
|
822
|
+
super("The place your data is stored isn't responding right now. Please try again.");
|
|
823
|
+
this.hosts = hosts;
|
|
824
|
+
this.cause = cause;
|
|
825
|
+
this.code = "NODE_NOT_RESPONDING";
|
|
826
|
+
this.name = "NodeNotRespondingError";
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
var NodeIdentityMismatchError = class extends Error {
|
|
830
|
+
constructor(url, expectedDid, actualDid) {
|
|
831
|
+
super(`TinyCloud node identity does not match the registry at ${url}`);
|
|
832
|
+
this.url = url;
|
|
833
|
+
this.expectedDid = expectedDid;
|
|
834
|
+
this.actualDid = actualDid;
|
|
835
|
+
this.code = "NODE_IDENTITY_MISMATCH";
|
|
836
|
+
this.name = "NodeIdentityMismatchError";
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
var ResourceNotRoutedError = class extends Error {
|
|
840
|
+
constructor(resource) {
|
|
841
|
+
super(`No registered destination can serve ${resource}`);
|
|
842
|
+
this.resource = resource;
|
|
843
|
+
this.code = "RESOURCE_NOT_ROUTED";
|
|
844
|
+
this.name = "ResourceNotRoutedError";
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
var PartialNodeWriteError = class extends Error {
|
|
848
|
+
constructor(succeeded, failed) {
|
|
849
|
+
super("The write did not complete on every registered writable node.");
|
|
850
|
+
this.succeeded = succeeded;
|
|
851
|
+
this.failed = failed;
|
|
852
|
+
this.code = "PARTIAL_NODE_WRITE";
|
|
853
|
+
this.name = "PartialNodeWriteError";
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
var ROUTING_FAILURE_CODES = {
|
|
857
|
+
NODE_NOT_RESPONDING: true,
|
|
858
|
+
PARTIAL_NODE_WRITE: true,
|
|
859
|
+
NODE_IDENTITY_MISMATCH: true,
|
|
860
|
+
RESOURCE_NOT_ROUTED: true,
|
|
861
|
+
DECRYPTION_DEPENDENCY_UNAVAILABLE: true,
|
|
862
|
+
registry_unavailable: true,
|
|
863
|
+
node_not_found: true,
|
|
864
|
+
node_registry_entry_exists: true
|
|
865
|
+
};
|
|
866
|
+
function isRoutingFailureCode(code) {
|
|
867
|
+
return Object.prototype.hasOwnProperty.call(ROUTING_FAILURE_CODES, code);
|
|
868
|
+
}
|
|
869
|
+
function routingFailure(error) {
|
|
870
|
+
if (typeof error === "object" && error !== null && "code" in error && "message" in error && typeof error.code === "string" && typeof error.message === "string" && isRoutingFailureCode(error.code)) {
|
|
871
|
+
const candidate = "meta" in error ? error.meta : void 0;
|
|
872
|
+
let meta = typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) ? candidate : void 0;
|
|
873
|
+
if (error.code === "NODE_NOT_RESPONDING" && "hosts" in error && Array.isArray(error.hosts)) {
|
|
874
|
+
meta = { ...meta, hosts: error.hosts };
|
|
875
|
+
} else if (error.code === "PARTIAL_NODE_WRITE" && "succeeded" in error && Array.isArray(error.succeeded) && "failed" in error && Array.isArray(error.failed)) {
|
|
876
|
+
meta = { ...meta, succeeded: error.succeeded, failed: error.failed };
|
|
877
|
+
}
|
|
878
|
+
return { code: error.code, message: error.message, ...error instanceof Error ? { cause: error } : {}, ...meta ? { meta } : {} };
|
|
879
|
+
}
|
|
880
|
+
return void 0;
|
|
881
|
+
}
|
|
882
|
+
function authorizationOf(headers) {
|
|
883
|
+
if (headers === void 0) return void 0;
|
|
884
|
+
return (Array.isArray(headers) ? headers : Object.entries(headers)).find(([name]) => name.toLowerCase() === "authorization")?.[1];
|
|
885
|
+
}
|
|
886
|
+
function routingResourceOf(uri) {
|
|
887
|
+
if (uri.startsWith("tinycloud://")) {
|
|
888
|
+
const resource = uri.slice("tinycloud://".length).split("/", 1)[0];
|
|
889
|
+
if (resource) return resource;
|
|
890
|
+
} else if (uri.startsWith("urn:tinycloud:encryption:")) {
|
|
891
|
+
return uri;
|
|
892
|
+
} else if (uri.startsWith("tinycloud:")) {
|
|
893
|
+
return uri.split("/", 1)[0];
|
|
894
|
+
}
|
|
895
|
+
throw new ResourceNotRoutedError(uri);
|
|
896
|
+
}
|
|
897
|
+
function invocationResources(authorization) {
|
|
898
|
+
const encoded = authorization.split(".")[1];
|
|
899
|
+
if (!encoded) throw new ResourceNotRoutedError("invalid invocation");
|
|
900
|
+
const payload = JSON.parse(atob(encoded.replace(/-/g, "+").replace(/_/g, "/")));
|
|
901
|
+
if (typeof payload !== "object" || payload === null || !("att" in payload) || typeof payload.att !== "object" || payload.att === null || Array.isArray(payload.att)) {
|
|
902
|
+
throw new ResourceNotRoutedError("invalid invocation attenuation");
|
|
903
|
+
}
|
|
904
|
+
const resources = [];
|
|
905
|
+
for (const [uri, abilities] of Object.entries(payload.att)) {
|
|
906
|
+
if (typeof abilities !== "object" || abilities === null || Array.isArray(abilities)) throw new ResourceNotRoutedError(uri);
|
|
907
|
+
for (const action of Object.keys(abilities)) {
|
|
908
|
+
const resource = routingResourceOf(uri);
|
|
909
|
+
const read2 = /^(tinycloud\.(kv\/(get|list|metadata|head|sync)|sql\/(read|export)|duckdb\/(read|describe|export)|capabilities\/read|space\/(list|info)|hooks\/list))$/.test(action);
|
|
910
|
+
const access = resource.startsWith("urn:tinycloud:encryption:") ? "encryption" : action === "tinycloud.space/host" ? "host" : read2 ? "read" : "write";
|
|
911
|
+
if (!resources.some((entry) => entry.resource === resource && entry.access === access)) resources.push({ resource, access });
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (resources.length === 0) throw new ResourceNotRoutedError("empty invocation");
|
|
915
|
+
return resources;
|
|
916
|
+
}
|
|
917
|
+
async function routeResourceRequest(resources, routing, send, options) {
|
|
918
|
+
if (resources.length === 0) throw new ResourceNotRoutedError("empty request");
|
|
919
|
+
const routes = await Promise.all(resources.map(({ resource, access }) => routing.resolveResource(resource, access)));
|
|
920
|
+
if (routes.some((route) => route.length === 0)) throw new ResourceNotRoutedError(resources[routes.findIndex((route) => route.length === 0)].resource);
|
|
921
|
+
const write = options?.independentResources === true || resources.some(({ access }) => access === "write" || access === "host");
|
|
922
|
+
let targets;
|
|
923
|
+
if (options?.independentResources) {
|
|
924
|
+
targets = routes.flat();
|
|
925
|
+
const identities = /* @__PURE__ */ new Map();
|
|
926
|
+
for (const target of targets) {
|
|
927
|
+
if (identities.has(target.url) && identities.get(target.url) !== target.nodeDid) throw new ResourceNotRoutedError(target.url);
|
|
928
|
+
identities.set(target.url, target.nodeDid);
|
|
929
|
+
}
|
|
930
|
+
} else {
|
|
931
|
+
targets = routes[0].filter((target) => routes.every((route) => route.some((other) => other.url === target.url && other.nodeDid === target.nodeDid)));
|
|
932
|
+
if (targets.length === 0 || write && routes.some((route) => route.length !== targets.length)) throw new ResourceNotRoutedError(resources.map(({ resource }) => resource).join(", "));
|
|
933
|
+
}
|
|
934
|
+
targets = targets.filter((target, index) => targets.findIndex((other) => other.url === target.url) === index);
|
|
935
|
+
if (options?.nodeDid) targets = targets.filter((target) => target.nodeDid === options.nodeDid || target.nodeDid === void 0);
|
|
936
|
+
if (targets.length === 0 || options?.nodeLocal && targets.length !== 1) throw new ResourceNotRoutedError(resources.map(({ resource }) => resource).join(", "));
|
|
937
|
+
if (!write) {
|
|
938
|
+
let response2;
|
|
939
|
+
let failure;
|
|
940
|
+
for (const target of targets) {
|
|
941
|
+
try {
|
|
942
|
+
response2 = await send(target);
|
|
943
|
+
if (response2.ok || response2.status !== 404) return response2;
|
|
944
|
+
} catch (error) {
|
|
945
|
+
if (!(error instanceof NodeNotRespondingError)) throw error;
|
|
946
|
+
failure = error;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (failure) throw new NodeNotRespondingError(targets.map(({ url }) => url), failure);
|
|
950
|
+
if (response2) return response2;
|
|
951
|
+
throw new NodeNotRespondingError(targets.map(({ url }) => url), failure);
|
|
952
|
+
}
|
|
953
|
+
const succeeded = [];
|
|
954
|
+
const failed = [];
|
|
955
|
+
let response;
|
|
956
|
+
let failureResponse;
|
|
957
|
+
for (const target of targets) {
|
|
958
|
+
try {
|
|
959
|
+
const result = await send(target);
|
|
960
|
+
if (result.ok) {
|
|
961
|
+
succeeded.push(target.url);
|
|
962
|
+
response ?? (response = result);
|
|
963
|
+
} else {
|
|
964
|
+
failed.push({ url: target.url, status: result.status });
|
|
965
|
+
failureResponse ?? (failureResponse = result);
|
|
966
|
+
}
|
|
967
|
+
} catch (error) {
|
|
968
|
+
if (!routingFailure(error) && succeeded.length === 0) throw error;
|
|
969
|
+
failed.push({ url: target.url, cause: error });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (failed.length > 0) {
|
|
973
|
+
if (succeeded.length > 0 || targets.length > 1) throw new PartialNodeWriteError(succeeded, failed);
|
|
974
|
+
if (failureResponse) return failureResponse;
|
|
975
|
+
if (failed[0]?.cause && !(failed[0].cause instanceof NodeNotRespondingError)) throw failed[0].cause;
|
|
976
|
+
throw new NodeNotRespondingError(targets.map(({ url }) => url), failed[0]?.cause);
|
|
977
|
+
}
|
|
978
|
+
return response;
|
|
979
|
+
}
|
|
980
|
+
function createResourceTransportInvocationFetch(transport, session) {
|
|
981
|
+
return async (url, init) => {
|
|
982
|
+
const parsed = new URL(url);
|
|
983
|
+
if (parsed.pathname !== "/invoke") throw new ResourceNotRoutedError(parsed.pathname);
|
|
984
|
+
const authorization = authorizationOf(init?.headers);
|
|
985
|
+
if (!authorization) throw new ResourceNotRoutedError("unsigned invocation");
|
|
986
|
+
return transport(invocationResources(authorization), async (target) => {
|
|
987
|
+
const signed = await target.bindAudience(authorization);
|
|
988
|
+
const headers = (Array.isArray(init?.headers) ? init.headers : Object.entries(init?.headers ?? {})).filter(([name]) => name.toLowerCase() !== "authorization");
|
|
989
|
+
headers.push(["Authorization", signed]);
|
|
990
|
+
return target.fetch(`${target.url}${parsed.pathname}${parsed.search}`, { ...init, headers });
|
|
991
|
+
}, { invocationSession: session });
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function createResourceRoutingFetch(fetchFn, routing) {
|
|
995
|
+
return async (url, init) => {
|
|
996
|
+
const parsed = new URL(url);
|
|
997
|
+
if (parsed.pathname !== "/invoke") return fetchFn(url, init);
|
|
998
|
+
const authorization = authorizationOf(init?.headers);
|
|
999
|
+
if (!authorization) throw new ResourceNotRoutedError("unsigned invocation");
|
|
1000
|
+
const resources = invocationResources(authorization);
|
|
1001
|
+
const send = async (target) => {
|
|
1002
|
+
init?.signal?.throwIfAborted();
|
|
1003
|
+
if (routing.verifyDestination) await routing.verifyDestination(target);
|
|
1004
|
+
else await verifyResourceDestination(target, fetchFn);
|
|
1005
|
+
const signed = target.nodeDid ? await routing.bindAudience(authorization, target.nodeDid) : authorization;
|
|
1006
|
+
const headers = (Array.isArray(init?.headers) ? init.headers : Object.entries(init?.headers ?? {})).filter(([name]) => name.toLowerCase() !== "authorization");
|
|
1007
|
+
headers.push(["Authorization", signed]);
|
|
1008
|
+
const timeout = AbortSignal.timeout(5e3);
|
|
1009
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
1010
|
+
try {
|
|
1011
|
+
return await fetchFn(`${target.url}${parsed.pathname}${parsed.search}`, { ...init, headers, signal, redirect: "error" });
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
if (init?.signal?.aborted) throw error;
|
|
1014
|
+
if (routingFailure(error)) throw error;
|
|
1015
|
+
throw new NodeNotRespondingError([target.url], error);
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
return routeResourceRequest(resources, routing, send);
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
787
1022
|
// src/errors.ts
|
|
788
1023
|
function parsePermissionHint(value) {
|
|
789
1024
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
@@ -1089,6 +1324,8 @@ function isStorageFullError(error) {
|
|
|
1089
1324
|
return error?.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error?.code === ErrorCodes.STORAGE_LIMIT_REACHED;
|
|
1090
1325
|
}
|
|
1091
1326
|
function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
1327
|
+
const routing = routingFailure(error);
|
|
1328
|
+
if (routing) return { ...routing, service };
|
|
1092
1329
|
if (error instanceof Error) {
|
|
1093
1330
|
if (isRequestTimeoutError(error)) {
|
|
1094
1331
|
return timeoutError(service, error.timeoutMs, error);
|
|
@@ -5786,9 +6023,9 @@ function utf8Encode(s) {
|
|
|
5786
6023
|
function utf8Decode(b) {
|
|
5787
6024
|
return new TextDecoder().decode(b);
|
|
5788
6025
|
}
|
|
5789
|
-
function canonicalHashHex(
|
|
6026
|
+
function canonicalHashHex(sha2562, value) {
|
|
5790
6027
|
const canonical = canonicalize(value);
|
|
5791
|
-
return hexEncode(
|
|
6028
|
+
return hexEncode(sha2562(utf8Encode(canonical)));
|
|
5792
6029
|
}
|
|
5793
6030
|
|
|
5794
6031
|
// src/vault/types.ts
|
|
@@ -6030,6 +6267,8 @@ function defaultVaultMessage(input) {
|
|
|
6030
6267
|
case "STORAGE_QUOTA_EXCEEDED":
|
|
6031
6268
|
case "STORAGE_LIMIT_REACHED":
|
|
6032
6269
|
return input.message;
|
|
6270
|
+
default:
|
|
6271
|
+
return input.message ?? input.code;
|
|
6033
6272
|
}
|
|
6034
6273
|
}
|
|
6035
6274
|
function vaultError(input) {
|
|
@@ -6081,6 +6320,8 @@ function ifMatchWriteError(key, error) {
|
|
|
6081
6320
|
});
|
|
6082
6321
|
}
|
|
6083
6322
|
function kvWriteError(context, error) {
|
|
6323
|
+
const routing = routingFailure(error);
|
|
6324
|
+
if (routing) return vaultError(routing);
|
|
6084
6325
|
if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
|
|
6085
6326
|
return vaultError({ code: error.code, message: error.message, meta: error.meta });
|
|
6086
6327
|
}
|
|
@@ -6387,6 +6628,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6387
6628
|
{ aad, metadata }
|
|
6388
6629
|
);
|
|
6389
6630
|
if (!envelopeResult.ok) {
|
|
6631
|
+
const routing = routingFailure(envelopeResult.error);
|
|
6632
|
+
if (routing) return vaultError(routing);
|
|
6390
6633
|
return vaultError({
|
|
6391
6634
|
code: "STORAGE_ERROR",
|
|
6392
6635
|
cause: new Error(envelopeResult.error.message)
|
|
@@ -6413,6 +6656,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6413
6656
|
}
|
|
6414
6657
|
return { ok: true, data: void 0 };
|
|
6415
6658
|
} catch (error) {
|
|
6659
|
+
const routing = routingFailure(error);
|
|
6660
|
+
if (routing) return vaultError(routing);
|
|
6416
6661
|
return vaultError({
|
|
6417
6662
|
code: "STORAGE_ERROR",
|
|
6418
6663
|
cause: toError(error)
|
|
@@ -6439,6 +6684,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6439
6684
|
...options?.source === void 0 ? {} : { source: options.source }
|
|
6440
6685
|
});
|
|
6441
6686
|
if (!valueResult.ok) {
|
|
6687
|
+
const routing = routingFailure(valueResult.error);
|
|
6688
|
+
if (routing) return vaultError(routing);
|
|
6442
6689
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6443
6690
|
}
|
|
6444
6691
|
const rawEnvelope = unwrapKVData(valueResult.data);
|
|
@@ -6459,6 +6706,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6459
6706
|
const proof = await this.decryptCapabilityProof();
|
|
6460
6707
|
const plaintextResult = await config.service.decryptEnvelope(envelope, proof);
|
|
6461
6708
|
if (!plaintextResult.ok) {
|
|
6709
|
+
const routing = routingFailure(plaintextResult.error);
|
|
6710
|
+
if (routing) return vaultError(routing);
|
|
6462
6711
|
return vaultError({
|
|
6463
6712
|
code: "DECRYPTION_FAILED",
|
|
6464
6713
|
message: plaintextResult.error.message
|
|
@@ -6478,6 +6727,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6478
6727
|
data: { value, metadata, keyId, ...etag === void 0 ? {} : { etag } }
|
|
6479
6728
|
};
|
|
6480
6729
|
} catch (error) {
|
|
6730
|
+
const routing = routingFailure(error);
|
|
6731
|
+
if (routing) return vaultError(routing);
|
|
6481
6732
|
return vaultError({
|
|
6482
6733
|
code: "STORAGE_ERROR",
|
|
6483
6734
|
cause: toError(error)
|
|
@@ -6490,6 +6741,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6490
6741
|
* Unlike {@link getNetworkEncrypted}, this is intentionally message-free so
|
|
6491
6742
|
* an operation layer can distinguish an authorized absence from a failed
|
|
6492
6743
|
* read without receiving node, envelope, plaintext, or key material.
|
|
6744
|
+
* Typed routing failures reject unchanged rather than becoming an ordinary
|
|
6745
|
+
* read/decrypt failure; the operation boundary projects their safe code.
|
|
6493
6746
|
*/
|
|
6494
6747
|
async readNetworkEncrypted(key, options) {
|
|
6495
6748
|
const config = this.networkEncryption;
|
|
@@ -6499,10 +6752,12 @@ var DataVaultService = class extends BaseService {
|
|
|
6499
6752
|
let valueResult;
|
|
6500
6753
|
try {
|
|
6501
6754
|
valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
|
|
6502
|
-
} catch {
|
|
6755
|
+
} catch (error) {
|
|
6756
|
+
if (routingFailure(error)) throw error;
|
|
6503
6757
|
return { status: "node_unreachable" };
|
|
6504
6758
|
}
|
|
6505
6759
|
if (!valueResult.ok) {
|
|
6760
|
+
if (routingFailure(valueResult.error)) throw valueResult.error;
|
|
6506
6761
|
const permissionHint = parsePermissionHint(valueResult.error.meta?.permissionHint);
|
|
6507
6762
|
if (valueResult.error.code === "AUTH_UNAUTHORIZED" && permissionHint !== void 0) {
|
|
6508
6763
|
return { status: "permission_required", hint: permissionHint };
|
|
@@ -6510,7 +6765,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6510
6765
|
if ((valueResult.error.code === "NOT_FOUND" || valueResult.error.code === "KV_NOT_FOUND") && !hasHttpResponse(valueResult.error)) {
|
|
6511
6766
|
return { status: "not_found" };
|
|
6512
6767
|
}
|
|
6513
|
-
if (valueResult.error.code === "NETWORK_ERROR" || valueResult.error.code === "TIMEOUT" || valueResult.error.code === "ABORTED") {
|
|
6768
|
+
if (valueResult.error.code === "NODE_NOT_RESPONDING" || valueResult.error.code === "NETWORK_ERROR" || valueResult.error.code === "TIMEOUT" || valueResult.error.code === "ABORTED") {
|
|
6514
6769
|
return hasHttpResponse(valueResult.error) ? { status: "read_failed" } : { status: "node_unreachable" };
|
|
6515
6770
|
}
|
|
6516
6771
|
return { status: "read_failed" };
|
|
@@ -6532,10 +6787,12 @@ var DataVaultService = class extends BaseService {
|
|
|
6532
6787
|
try {
|
|
6533
6788
|
const proof = await this.decryptCapabilityProof();
|
|
6534
6789
|
plaintextResult = await config.service.decryptEnvelope(envelope, proof);
|
|
6535
|
-
} catch {
|
|
6790
|
+
} catch (error) {
|
|
6791
|
+
if (routingFailure(error)) throw error;
|
|
6536
6792
|
return { status: "decrypt_failed" };
|
|
6537
6793
|
}
|
|
6538
6794
|
if (!plaintextResult.ok) {
|
|
6795
|
+
if (routingFailure(plaintextResult.error)) throw plaintextResult.error;
|
|
6539
6796
|
const permissionHint = parsePermissionHint(
|
|
6540
6797
|
plaintextResult.error.permissionHint
|
|
6541
6798
|
);
|
|
@@ -6545,7 +6802,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6545
6802
|
if (plaintextResult.error.code === "INVALID_ENVELOPE") {
|
|
6546
6803
|
return { status: "corrupt_envelope" };
|
|
6547
6804
|
}
|
|
6548
|
-
if (plaintextResult.error.code === "TRANSPORT_ERROR") {
|
|
6805
|
+
if (plaintextResult.error.code === "NODE_NOT_RESPONDING" || plaintextResult.error.code === "TRANSPORT_ERROR") {
|
|
6549
6806
|
return { status: "node_unreachable" };
|
|
6550
6807
|
}
|
|
6551
6808
|
return { status: "decrypt_failed" };
|
|
@@ -6797,6 +7054,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6797
7054
|
if (this.usesNetworkEncryption) {
|
|
6798
7055
|
const valueDelResult2 = await this.tc.kv.delete(`vault/${key}`);
|
|
6799
7056
|
if (!valueDelResult2.ok) {
|
|
7057
|
+
const routing = routingFailure(valueDelResult2.error);
|
|
7058
|
+
if (routing) return vaultError(routing);
|
|
6800
7059
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6801
7060
|
}
|
|
6802
7061
|
return ok(void 0);
|
|
@@ -6806,10 +7065,14 @@ var DataVaultService = class extends BaseService {
|
|
|
6806
7065
|
this.tc.kv.delete(`vault/${key}`)
|
|
6807
7066
|
]);
|
|
6808
7067
|
if (!keyDelResult.ok && !valueDelResult.ok) {
|
|
7068
|
+
const routing = routingFailure(keyDelResult.error) ?? routingFailure(valueDelResult.error);
|
|
7069
|
+
if (routing) return vaultError(routing);
|
|
6809
7070
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6810
7071
|
}
|
|
6811
7072
|
return ok(void 0);
|
|
6812
7073
|
} catch (error) {
|
|
7074
|
+
const routing = routingFailure(error);
|
|
7075
|
+
if (routing) return vaultError(routing);
|
|
6813
7076
|
return vaultError({
|
|
6814
7077
|
code: "STORAGE_ERROR",
|
|
6815
7078
|
cause: toError(error)
|
|
@@ -6851,6 +7114,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6851
7114
|
...options?.cursor === void 0 ? {} : { cursor: options.cursor }
|
|
6852
7115
|
});
|
|
6853
7116
|
if (!listResult.ok) {
|
|
7117
|
+
const routing = routingFailure(listResult.error);
|
|
7118
|
+
if (routing) return vaultError(routing);
|
|
6854
7119
|
return vaultError({
|
|
6855
7120
|
code: "STORAGE_ERROR",
|
|
6856
7121
|
cause: new Error(
|
|
@@ -6871,6 +7136,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6871
7136
|
...listResult.data.nextCursor === void 0 ? {} : { nextCursor: listResult.data.nextCursor }
|
|
6872
7137
|
});
|
|
6873
7138
|
} catch (error) {
|
|
7139
|
+
const routing = routingFailure(error);
|
|
7140
|
+
if (routing) return vaultError(routing);
|
|
6874
7141
|
return vaultError({
|
|
6875
7142
|
code: "STORAGE_ERROR",
|
|
6876
7143
|
cause: toError(error)
|
|
@@ -7702,6 +7969,8 @@ function defaultEncryptionMessage(input) {
|
|
|
7702
7969
|
return input.message ?? input.cause.message;
|
|
7703
7970
|
case "INVALID_INPUT":
|
|
7704
7971
|
return input.message;
|
|
7972
|
+
default:
|
|
7973
|
+
return input.message ?? input.code;
|
|
7705
7974
|
}
|
|
7706
7975
|
}
|
|
7707
7976
|
function encryptionError(input) {
|
|
@@ -7766,6 +8035,8 @@ async function discoverNetwork(input) {
|
|
|
7766
8035
|
return { ok: true, data: { descriptor: validated.data, source: "node" } };
|
|
7767
8036
|
}
|
|
7768
8037
|
} catch (err3) {
|
|
8038
|
+
const routing = routingFailure(err3);
|
|
8039
|
+
if (routing) return { ok: false, error: encryptionError(routing) };
|
|
7769
8040
|
}
|
|
7770
8041
|
}
|
|
7771
8042
|
if (input.wellKnown !== void 0) {
|
|
@@ -7783,6 +8054,8 @@ async function discoverNetwork(input) {
|
|
|
7783
8054
|
};
|
|
7784
8055
|
}
|
|
7785
8056
|
} catch (err3) {
|
|
8057
|
+
const routing = routingFailure(err3);
|
|
8058
|
+
if (routing) return { ok: false, error: encryptionError(routing) };
|
|
7786
8059
|
}
|
|
7787
8060
|
}
|
|
7788
8061
|
return {
|
|
@@ -7872,6 +8145,152 @@ function ensureNetworkUsableForDecrypt(descriptor) {
|
|
|
7872
8145
|
};
|
|
7873
8146
|
}
|
|
7874
8147
|
|
|
8148
|
+
// src/encryption/keyWrap.ts
|
|
8149
|
+
import { gcm } from "@noble/ciphers/aes";
|
|
8150
|
+
import { x25519 } from "@noble/curves/ed25519";
|
|
8151
|
+
import { hkdf } from "@noble/hashes/hkdf";
|
|
8152
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
8153
|
+
import { randomBytes } from "@noble/hashes/utils";
|
|
8154
|
+
var KEY_WRAP_V2_ALG = "x25519-hkdf-aes256gcm/v2";
|
|
8155
|
+
var MAGIC = utf8Encode("TCW2");
|
|
8156
|
+
var INFO = utf8Encode("tinycloud.encryption.key-wrap/v2");
|
|
8157
|
+
var MAX_HEADER = 65536;
|
|
8158
|
+
function concat(...parts) {
|
|
8159
|
+
const result = new Uint8Array(parts.reduce((length, part) => length + part.length, 0));
|
|
8160
|
+
let offset = 0;
|
|
8161
|
+
for (const part of parts) {
|
|
8162
|
+
result.set(part, offset);
|
|
8163
|
+
offset += part.length;
|
|
8164
|
+
}
|
|
8165
|
+
return result;
|
|
8166
|
+
}
|
|
8167
|
+
function equal(a, b) {
|
|
8168
|
+
if (a.length !== b.length) return false;
|
|
8169
|
+
let difference = 0;
|
|
8170
|
+
for (let i = 0; i < a.length; i++) difference |= a[i] ^ b[i];
|
|
8171
|
+
return difference === 0;
|
|
8172
|
+
}
|
|
8173
|
+
function decode32(value) {
|
|
8174
|
+
if (typeof value !== "string") throw new Error("Invalid key wrap encoding");
|
|
8175
|
+
const decoded = base64Decode(value);
|
|
8176
|
+
if (decoded.length !== 32 || base64Encode(decoded) !== value) throw new Error("Invalid key wrap encoding");
|
|
8177
|
+
return decoded;
|
|
8178
|
+
}
|
|
8179
|
+
function isWellFormed(value) {
|
|
8180
|
+
for (let i = 0; i < value.length; i++) {
|
|
8181
|
+
const unit = value.charCodeAt(i);
|
|
8182
|
+
if (unit >= 55296 && unit <= 56319) {
|
|
8183
|
+
const next = value.charCodeAt(++i);
|
|
8184
|
+
if (!(next >= 56320 && next <= 57343)) return false;
|
|
8185
|
+
} else if (unit >= 56320 && unit <= 57343) return false;
|
|
8186
|
+
}
|
|
8187
|
+
return true;
|
|
8188
|
+
}
|
|
8189
|
+
function validateContext(context) {
|
|
8190
|
+
if (context.purpose !== "network-key" && context.purpose !== "receiver-key" || typeof context.networkId !== "string" || typeof context.keyPath !== "string" || !isWellFormed(context.networkId) || !isWellFormed(context.keyPath) || !Number.isSafeInteger(context.keyVersion) || context.keyVersion <= 0 || !context.metadata || typeof context.metadata !== "object" || Array.isArray(context.metadata) || Object.entries(context.metadata).some(([key, value]) => !isWellFormed(key) || typeof value !== "string" || !isWellFormed(value))) {
|
|
8191
|
+
throw new Error("Invalid key wrap context");
|
|
8192
|
+
}
|
|
8193
|
+
if (context.networkId !== "") {
|
|
8194
|
+
const resource = parseEncryptionResource(context.keyPath === "" ? context.networkId : `${context.networkId}/${context.keyPath}`);
|
|
8195
|
+
if (resource.networkId !== context.networkId || (resource.keyPath ?? "") !== context.keyPath || context.keyPath.endsWith("/")) {
|
|
8196
|
+
throw new Error("Invalid key wrap resource");
|
|
8197
|
+
}
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
8200
|
+
function isKeyWrapV2(wrapped) {
|
|
8201
|
+
return wrapped.length >= 4 && MAGIC.every((byte, index) => wrapped[index] === byte);
|
|
8202
|
+
}
|
|
8203
|
+
function readKeyWrapV2(wrapped) {
|
|
8204
|
+
if (!isKeyWrapV2(wrapped) || wrapped.length < 8) throw new Error("Invalid v2 key wrap frame");
|
|
8205
|
+
const length = new DataView(wrapped.buffer, wrapped.byteOffset, wrapped.byteLength).getUint32(4);
|
|
8206
|
+
if (length === 0 || length > MAX_HEADER || wrapped.length < 8 + length + 12 + 16) throw new Error("Invalid v2 key wrap length");
|
|
8207
|
+
const bytes = wrapped.subarray(8, 8 + length);
|
|
8208
|
+
const header = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
8209
|
+
const fields = ["v", "alg", "kdf", "aead", "salt", "ephemeralPublicKey", "recipientPublicKey", "purpose", "networkId", "keyPath", "keyVersion", "metadata"].sort();
|
|
8210
|
+
if (!header || typeof header !== "object" || Array.isArray(header) || Object.keys(header).sort().join(",") !== fields.join(",") || header.v !== 2 || header.alg !== KEY_WRAP_V2_ALG || header.kdf !== "hkdf-sha256" || header.aead !== "aes-256-gcm") {
|
|
8211
|
+
throw new Error("Invalid v2 key wrap header");
|
|
8212
|
+
}
|
|
8213
|
+
validateContext(header);
|
|
8214
|
+
decode32(header.salt);
|
|
8215
|
+
decode32(header.ephemeralPublicKey);
|
|
8216
|
+
decode32(header.recipientPublicKey);
|
|
8217
|
+
if (!equal(bytes, utf8Encode(canonicalize({ ...header })))) throw new Error("Non-canonical v2 key wrap header");
|
|
8218
|
+
const end = 8 + length;
|
|
8219
|
+
return { header, aad: wrapped.subarray(0, end), nonce: wrapped.subarray(end, end + 12), ciphertext: wrapped.subarray(end + 12) };
|
|
8220
|
+
}
|
|
8221
|
+
function assertKeyWrapContext(header, expected) {
|
|
8222
|
+
validateContext(expected);
|
|
8223
|
+
for (const field of ["purpose", "networkId", "keyPath", "keyVersion"]) {
|
|
8224
|
+
if (header[field] !== expected[field]) throw new Error("Key wrap context mismatch");
|
|
8225
|
+
}
|
|
8226
|
+
if (canonicalize(header.metadata) !== canonicalize(expected.metadata)) throw new Error("Key wrap metadata mismatch");
|
|
8227
|
+
}
|
|
8228
|
+
function deriveKey(secret, ephemeral, recipient, salt) {
|
|
8229
|
+
const shared = x25519.getSharedSecret(secret, ephemeral);
|
|
8230
|
+
if (shared.every((byte) => byte === 0)) throw new Error("Invalid key agreement");
|
|
8231
|
+
try {
|
|
8232
|
+
return hkdf(sha256, shared, salt, concat(INFO, ephemeral, recipient), 32);
|
|
8233
|
+
} finally {
|
|
8234
|
+
shared.fill(0);
|
|
8235
|
+
}
|
|
8236
|
+
}
|
|
8237
|
+
function sealKeyV2(recipientPublicKey, plaintext, context) {
|
|
8238
|
+
validateContext(context);
|
|
8239
|
+
if (recipientPublicKey.length !== 32) throw new Error("Invalid recipient public key");
|
|
8240
|
+
const secret = randomBytes(32);
|
|
8241
|
+
const ephemeral = x25519.getPublicKey(secret);
|
|
8242
|
+
const salt = randomBytes(32);
|
|
8243
|
+
const header = {
|
|
8244
|
+
...context,
|
|
8245
|
+
v: 2,
|
|
8246
|
+
alg: KEY_WRAP_V2_ALG,
|
|
8247
|
+
kdf: "hkdf-sha256",
|
|
8248
|
+
aead: "aes-256-gcm",
|
|
8249
|
+
salt: base64Encode(salt),
|
|
8250
|
+
ephemeralPublicKey: base64Encode(ephemeral),
|
|
8251
|
+
recipientPublicKey: base64Encode(recipientPublicKey)
|
|
8252
|
+
};
|
|
8253
|
+
const bytes = utf8Encode(canonicalize({ ...header }));
|
|
8254
|
+
if (bytes.length > MAX_HEADER) throw new Error("Key wrap header too large");
|
|
8255
|
+
const prefix = new Uint8Array(8);
|
|
8256
|
+
prefix.set(MAGIC);
|
|
8257
|
+
new DataView(prefix.buffer).setUint32(4, bytes.length);
|
|
8258
|
+
const aad = concat(prefix, bytes);
|
|
8259
|
+
const nonce = randomBytes(12);
|
|
8260
|
+
let key;
|
|
8261
|
+
try {
|
|
8262
|
+
const shared = x25519.getSharedSecret(secret, recipientPublicKey);
|
|
8263
|
+
try {
|
|
8264
|
+
if (shared.every((byte) => byte === 0)) throw new Error("Invalid key agreement");
|
|
8265
|
+
key = hkdf(sha256, shared, salt, concat(INFO, ephemeral, recipientPublicKey), 32);
|
|
8266
|
+
} finally {
|
|
8267
|
+
shared.fill(0);
|
|
8268
|
+
}
|
|
8269
|
+
return concat(aad, nonce, gcm(key, nonce, aad).encrypt(plaintext));
|
|
8270
|
+
} finally {
|
|
8271
|
+
secret.fill(0);
|
|
8272
|
+
key?.fill(0);
|
|
8273
|
+
}
|
|
8274
|
+
}
|
|
8275
|
+
function openKeyV2(recipientPrivateKey, wrapped, expected) {
|
|
8276
|
+
const { header, aad, nonce, ciphertext } = readKeyWrapV2(wrapped);
|
|
8277
|
+
assertKeyWrapContext(header, expected);
|
|
8278
|
+
const recipient = x25519.getPublicKey(recipientPrivateKey);
|
|
8279
|
+
if (!equal(recipient, decode32(header.recipientPublicKey))) throw new Error("Key wrap recipient mismatch");
|
|
8280
|
+
const key = deriveKey(recipientPrivateKey, decode32(header.ephemeralPublicKey), recipient, decode32(header.salt));
|
|
8281
|
+
try {
|
|
8282
|
+
return gcm(key, nonce, aad).decrypt(ciphertext);
|
|
8283
|
+
} finally {
|
|
8284
|
+
key.fill(0);
|
|
8285
|
+
}
|
|
8286
|
+
}
|
|
8287
|
+
function assertKeyWrapAlgorithm(wrapped, alg, requireV2 = false) {
|
|
8288
|
+
if (alg !== "x25519-aes256gcm/v1" && alg !== KEY_WRAP_V2_ALG) throw new Error("Unsupported key wrap algorithm");
|
|
8289
|
+
if (requireV2 && alg !== KEY_WRAP_V2_ALG) throw new Error("V2 network key wrap required");
|
|
8290
|
+
if (isKeyWrapV2(wrapped) !== (alg === KEY_WRAP_V2_ALG)) throw new Error("Key wrap algorithm mismatch");
|
|
8291
|
+
if (alg === KEY_WRAP_V2_ALG) readKeyWrapV2(wrapped);
|
|
8292
|
+
}
|
|
8293
|
+
|
|
7875
8294
|
// src/encryption/envelope.ts
|
|
7876
8295
|
function encryptToNetwork(crypto2, input) {
|
|
7877
8296
|
const parsedResource = parseEncryptionResource(
|
|
@@ -7880,11 +8299,20 @@ function encryptToNetwork(crypto2, input) {
|
|
|
7880
8299
|
if (parsedResource.keyPath !== input.keyPath || input.keyPath?.endsWith("/")) {
|
|
7881
8300
|
throw new NetworkIdError("keyPath must name an exact encryption key");
|
|
7882
8301
|
}
|
|
7883
|
-
const alg = input.
|
|
8302
|
+
const alg = input.keyWrapV2 ? KEY_WRAP_V2_ALG : DEFAULT_ENCRYPTION_ALG;
|
|
8303
|
+
if (input.alg !== void 0 && input.alg !== alg) {
|
|
8304
|
+
throw new Error("Algorithm does not match keyWrapV2");
|
|
8305
|
+
}
|
|
7884
8306
|
const keyVersion = input.keyVersion ?? DEFAULT_KEY_VERSION;
|
|
7885
8307
|
const symmetricKey = crypto2.randomBytes(32);
|
|
7886
8308
|
const ciphertext = crypto2.authEncrypt(symmetricKey, input.plaintext, input.aad);
|
|
7887
|
-
const wrapped =
|
|
8309
|
+
const wrapped = input.keyWrapV2 ? sealKeyV2(input.networkPublicKey, symmetricKey, {
|
|
8310
|
+
purpose: "network-key",
|
|
8311
|
+
networkId: input.networkId,
|
|
8312
|
+
keyPath: input.keyPath ?? "",
|
|
8313
|
+
keyVersion,
|
|
8314
|
+
metadata: input.metadata ?? {}
|
|
8315
|
+
}) : crypto2.sealToNetworkKey(input.networkPublicKey, symmetricKey);
|
|
7888
8316
|
const encryptedSymmetricKey = base64Encode(wrapped);
|
|
7889
8317
|
const encryptedSymmetricKeyHash = canonicalHashHex(
|
|
7890
8318
|
crypto2.sha256,
|
|
@@ -7904,7 +8332,7 @@ function encryptToNetwork(crypto2, input) {
|
|
|
7904
8332
|
};
|
|
7905
8333
|
return { envelope, symmetricKey };
|
|
7906
8334
|
}
|
|
7907
|
-
function validateEnvelope(crypto2, envelope) {
|
|
8335
|
+
function validateEnvelope(crypto2, envelope, requireKeyWrapV2 = false) {
|
|
7908
8336
|
if (envelope === null || typeof envelope !== "object") {
|
|
7909
8337
|
return {
|
|
7910
8338
|
ok: false,
|
|
@@ -7981,9 +8409,28 @@ function validateEnvelope(crypto2, envelope) {
|
|
|
7981
8409
|
})
|
|
7982
8410
|
};
|
|
7983
8411
|
}
|
|
8412
|
+
try {
|
|
8413
|
+
validateEnvelopeKeyWrap(e, requireKeyWrapV2);
|
|
8414
|
+
} catch {
|
|
8415
|
+
return { ok: false, error: encryptionError({ code: "INVALID_ENVELOPE", message: "Envelope key wrap context is invalid" }) };
|
|
8416
|
+
}
|
|
7984
8417
|
return { ok: true, data: e };
|
|
7985
8418
|
}
|
|
8419
|
+
function validateEnvelopeKeyWrap(envelope, requireKeyWrapV2 = false) {
|
|
8420
|
+
const wrapped = base64Decode(envelope.encryptedSymmetricKey);
|
|
8421
|
+
assertKeyWrapAlgorithm(wrapped, envelope.alg, requireKeyWrapV2);
|
|
8422
|
+
if (envelope.alg === KEY_WRAP_V2_ALG) {
|
|
8423
|
+
assertKeyWrapContext(readKeyWrapV2(wrapped).header, {
|
|
8424
|
+
purpose: "network-key",
|
|
8425
|
+
networkId: envelope.networkId,
|
|
8426
|
+
keyPath: envelope.keyPath ?? "",
|
|
8427
|
+
keyVersion: envelope.keyVersion,
|
|
8428
|
+
metadata: envelope.metadata ?? {}
|
|
8429
|
+
});
|
|
8430
|
+
}
|
|
8431
|
+
}
|
|
7986
8432
|
function decryptEnvelopeWithKey(crypto2, envelope, symmetricKey) {
|
|
8433
|
+
validateEnvelopeKeyWrap(envelope);
|
|
7987
8434
|
const ciphertext = base64Decode(envelope.ciphertext);
|
|
7988
8435
|
const aad = envelope.aad !== void 0 ? base64Decode(envelope.aad) : void 0;
|
|
7989
8436
|
return crypto2.authDecrypt(symmetricKey, ciphertext, aad);
|
|
@@ -8273,7 +8720,7 @@ function verifyDecryptResponse(input) {
|
|
|
8273
8720
|
})
|
|
8274
8721
|
};
|
|
8275
8722
|
}
|
|
8276
|
-
if (response.alg !==
|
|
8723
|
+
if (response.alg !== "x25519-aes256gcm/v1" && response.alg !== KEY_WRAP_V2_ALG) {
|
|
8277
8724
|
return {
|
|
8278
8725
|
ok: false,
|
|
8279
8726
|
error: encryptionError({
|
|
@@ -8330,7 +8777,7 @@ function verifyDecryptResponse(input) {
|
|
|
8330
8777
|
})
|
|
8331
8778
|
};
|
|
8332
8779
|
}
|
|
8333
|
-
if (facts.encryptedSymmetricKeyHash !== response.encryptedSymmetricKeyHash || facts.receiverPublicKeyHash !== response.receiverPublicKeyHash || facts.networkId !== response.networkId || facts.targetNode !== response.targetNode || facts.alg !==
|
|
8780
|
+
if (facts.encryptedSymmetricKeyHash !== response.encryptedSymmetricKeyHash || facts.receiverPublicKeyHash !== response.receiverPublicKeyHash || facts.networkId !== response.networkId || facts.targetNode !== response.targetNode || facts.alg !== request.alg || facts.keyVersion !== response.keyVersion) {
|
|
8334
8781
|
return {
|
|
8335
8782
|
ok: false,
|
|
8336
8783
|
error: encryptionError({
|
|
@@ -8353,8 +8800,13 @@ function verifyDecryptResponse(input) {
|
|
|
8353
8800
|
}
|
|
8354
8801
|
return { ok: true, data: response };
|
|
8355
8802
|
}
|
|
8356
|
-
function openWrappedKey(crypto2, receiverPrivateKey, response) {
|
|
8803
|
+
function openWrappedKey(crypto2, receiverPrivateKey, response, expected) {
|
|
8357
8804
|
const wrapped = base64Decode(response.wrappedKey);
|
|
8805
|
+
assertKeyWrapAlgorithm(wrapped, response.alg);
|
|
8806
|
+
if (response.alg === KEY_WRAP_V2_ALG) {
|
|
8807
|
+
if (!expected) throw new Error("V2 response requires expected key context");
|
|
8808
|
+
return openKeyV2(receiverPrivateKey, wrapped, expected);
|
|
8809
|
+
}
|
|
8358
8810
|
return crypto2.openWithReceiverKey(receiverPrivateKey, wrapped);
|
|
8359
8811
|
}
|
|
8360
8812
|
|
|
@@ -8367,6 +8819,14 @@ function encOk(data) {
|
|
|
8367
8819
|
function encErr(error) {
|
|
8368
8820
|
return { ok: false, error };
|
|
8369
8821
|
}
|
|
8822
|
+
function decryptionError(input) {
|
|
8823
|
+
const unavailable = input.code === "NODE_NOT_RESPONDING" || input.code === "TRANSPORT_ERROR" && input.cause.name !== "AbortError";
|
|
8824
|
+
return encryptionError(unavailable ? {
|
|
8825
|
+
...input,
|
|
8826
|
+
code: "DECRYPTION_DEPENDENCY_UNAVAILABLE",
|
|
8827
|
+
message: "The decryption service isn't responding right now. Please try again."
|
|
8828
|
+
} : input);
|
|
8829
|
+
}
|
|
8370
8830
|
var EncryptionService = class extends BaseService {
|
|
8371
8831
|
constructor(config) {
|
|
8372
8832
|
super();
|
|
@@ -8408,12 +8868,15 @@ var EncryptionService = class extends BaseService {
|
|
|
8408
8868
|
networkPublicKey,
|
|
8409
8869
|
plaintext,
|
|
8410
8870
|
...options?.aad !== void 0 ? { aad: options.aad } : {},
|
|
8411
|
-
|
|
8871
|
+
keyWrapV2: options?.keyWrapV2 ?? this._config.keyWrapV2 ?? false,
|
|
8872
|
+
...options?.alg !== void 0 ? { alg: options.alg } : {},
|
|
8412
8873
|
keyVersion: options?.keyVersion ?? descriptor.keyVersion,
|
|
8413
8874
|
...options?.metadata !== void 0 ? { metadata: options.metadata } : {}
|
|
8414
8875
|
});
|
|
8415
8876
|
return encOk(result.envelope);
|
|
8416
8877
|
} catch (error) {
|
|
8878
|
+
const routing = routingFailure(error);
|
|
8879
|
+
if (routing) return encErr(encryptionError(routing));
|
|
8417
8880
|
return encErr(
|
|
8418
8881
|
encryptionError({
|
|
8419
8882
|
code: "TRANSPORT_ERROR",
|
|
@@ -8425,7 +8888,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8425
8888
|
async decryptEnvelope(envelope, capabilityProof, options) {
|
|
8426
8889
|
try {
|
|
8427
8890
|
this.assertActive();
|
|
8428
|
-
const validated = validateEnvelope(this.crypto, envelope);
|
|
8891
|
+
const validated = validateEnvelope(this.crypto, envelope, this._config.requireKeyWrapV2 ?? false);
|
|
8429
8892
|
if (!validated.ok) return validated;
|
|
8430
8893
|
if (options?.aad !== void 0 && validated.data.aad !== base64Encode(options.aad)) {
|
|
8431
8894
|
return encErr(
|
|
@@ -8440,12 +8903,12 @@ var EncryptionService = class extends BaseService {
|
|
|
8440
8903
|
descriptor = options.descriptor;
|
|
8441
8904
|
} else {
|
|
8442
8905
|
const discovered = await this.discoverNetwork(envelope.networkId);
|
|
8443
|
-
if (!discovered.ok) return discovered;
|
|
8906
|
+
if (!discovered.ok) return encErr(decryptionError(discovered.error));
|
|
8444
8907
|
descriptor = discovered.data;
|
|
8445
8908
|
}
|
|
8446
8909
|
const usable = ensureNetworkUsableForDecrypt(descriptor);
|
|
8447
8910
|
if (!usable.ok) return usable;
|
|
8448
|
-
const targetNode = options?.targetNode ?? descriptor.members[0]?.nodeId;
|
|
8911
|
+
const targetNode = this._config.resolveTargetNode ? await this._config.resolveTargetNode(envelope.networkId) : options?.targetNode ?? descriptor.members[0]?.nodeId;
|
|
8449
8912
|
if (targetNode === void 0) {
|
|
8450
8913
|
return encErr(
|
|
8451
8914
|
encryptionError({
|
|
@@ -8454,6 +8917,9 @@ var EncryptionService = class extends BaseService {
|
|
|
8454
8917
|
})
|
|
8455
8918
|
);
|
|
8456
8919
|
}
|
|
8920
|
+
if (this._config.resolveTargetNode && (options?.targetNode !== void 0 && options.targetNode !== targetNode || !descriptor.members.some((member) => member.nodeId === targetNode))) {
|
|
8921
|
+
return encErr(encryptionError({ code: "INVALID_INPUT", message: "Encryption destination does not match the registry" }));
|
|
8922
|
+
}
|
|
8457
8923
|
const receiverKey = generateRandomReceiverKey({ crypto: this.crypto });
|
|
8458
8924
|
const receiverPublicKey = base64Encode(receiverKey.publicKey);
|
|
8459
8925
|
const receiverPublicKeyHash = canonicalHashHex(
|
|
@@ -8512,6 +8978,8 @@ var EncryptionService = class extends BaseService {
|
|
|
8512
8978
|
});
|
|
8513
8979
|
this.assertActive();
|
|
8514
8980
|
} catch (error) {
|
|
8981
|
+
const routing = routingFailure(error);
|
|
8982
|
+
if (routing) return encErr(decryptionError(routing));
|
|
8515
8983
|
if (error instanceof DecryptTransportResponseError) {
|
|
8516
8984
|
return encErr(
|
|
8517
8985
|
encryptionError(
|
|
@@ -8528,7 +8996,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8528
8996
|
);
|
|
8529
8997
|
}
|
|
8530
8998
|
return encErr(
|
|
8531
|
-
|
|
8999
|
+
decryptionError({
|
|
8532
9000
|
code: "TRANSPORT_ERROR",
|
|
8533
9001
|
cause: toError2(error)
|
|
8534
9002
|
})
|
|
@@ -8546,7 +9014,8 @@ var EncryptionService = class extends BaseService {
|
|
|
8546
9014
|
const symmetricKey = openWrappedKey(
|
|
8547
9015
|
this.crypto,
|
|
8548
9016
|
receiverKey.privateKey,
|
|
8549
|
-
verified.data
|
|
9017
|
+
verified.data,
|
|
9018
|
+
{ purpose: "receiver-key", networkId: body.networkId, keyPath: body.keyPath ?? "", keyVersion: body.keyVersion, metadata: {} }
|
|
8550
9019
|
);
|
|
8551
9020
|
const plaintext = decryptEnvelopeWithKey(
|
|
8552
9021
|
this.crypto,
|
|
@@ -8555,6 +9024,8 @@ var EncryptionService = class extends BaseService {
|
|
|
8555
9024
|
);
|
|
8556
9025
|
return encOk(plaintext);
|
|
8557
9026
|
} catch (error) {
|
|
9027
|
+
const routing = routingFailure(error);
|
|
9028
|
+
if (routing) return encErr(decryptionError(routing));
|
|
8558
9029
|
return encErr(
|
|
8559
9030
|
encryptionError({
|
|
8560
9031
|
code: "INVALID_RESPONSE",
|
|
@@ -8603,7 +9074,11 @@ export {
|
|
|
8603
9074
|
MAX_SECRET_SCOPE_LENGTH,
|
|
8604
9075
|
NETWORK_NAME_PATTERN,
|
|
8605
9076
|
NetworkIdError,
|
|
9077
|
+
NodeIdentityMismatchError,
|
|
9078
|
+
NodeNotRespondingError,
|
|
9079
|
+
PartialNodeWriteError,
|
|
8606
9080
|
PrefixedKVService,
|
|
9081
|
+
ResourceNotRoutedError,
|
|
8607
9082
|
RetryPolicySchema,
|
|
8608
9083
|
SECRET_NAME_RE,
|
|
8609
9084
|
SECRET_SCOPE_RE,
|
|
@@ -8651,6 +9126,8 @@ export {
|
|
|
8651
9126
|
checkDecryptInvocationInput,
|
|
8652
9127
|
clearTinyCloudDebugLogs,
|
|
8653
9128
|
createKVResponseSchema,
|
|
9129
|
+
createResourceRoutingFetch,
|
|
9130
|
+
createResourceTransportInvocationFetch,
|
|
8654
9131
|
createResultSchema,
|
|
8655
9132
|
createVaultCrypto,
|
|
8656
9133
|
decryptEnvelopeWithKey,
|
|
@@ -8693,6 +9170,9 @@ export {
|
|
|
8693
9170
|
requiresSecretsOptIn,
|
|
8694
9171
|
resolveSecretListPrefix,
|
|
8695
9172
|
resolveSecretPath,
|
|
9173
|
+
routeResourceRequest,
|
|
9174
|
+
routingFailure,
|
|
9175
|
+
routingResourceOf,
|
|
8696
9176
|
serviceError,
|
|
8697
9177
|
storageLimitReachedError,
|
|
8698
9178
|
storageQuotaExceededError,
|
|
@@ -8713,6 +9193,7 @@ export {
|
|
|
8713
9193
|
validateTelemetrySpanEvent,
|
|
8714
9194
|
validatedCapabilityOf,
|
|
8715
9195
|
verifyDecryptResponse,
|
|
9196
|
+
verifyResourceDestination,
|
|
8716
9197
|
wrapError
|
|
8717
9198
|
};
|
|
8718
9199
|
//# sourceMappingURL=index.js.map
|