@tinycloud/sdk-services 3.11.0 → 3.12.0-beta.16
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 +523 -29
- 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 +513 -29
- 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 +48 -11
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +5 -5
- package/dist/kv/index.d.ts +5 -5
- package/dist/kv/index.js +48 -11
- 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);
|
|
@@ -1990,11 +2227,15 @@ var KVService = class extends BaseService {
|
|
|
1990
2227
|
}
|
|
1991
2228
|
/** Delay KV routing until an asynchronously loaded read-through is attached. */
|
|
1992
2229
|
setReadThroughReady(ready) {
|
|
1993
|
-
const
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2230
|
+
const start = () => {
|
|
2231
|
+
if (typeof ready === "function") ready = Promise.resolve().then(ready);
|
|
2232
|
+
const finish = () => {
|
|
2233
|
+
if (this.readThroughReady === start) this.readThroughReady = null;
|
|
2234
|
+
};
|
|
2235
|
+
return ready.then(finish, finish);
|
|
2236
|
+
};
|
|
2237
|
+
this.readThroughReady = start;
|
|
2238
|
+
if (typeof ready !== "function") void start();
|
|
1998
2239
|
}
|
|
1999
2240
|
/**
|
|
2000
2241
|
* Get the service configuration.
|
|
@@ -2241,7 +2482,6 @@ var KVService = class extends BaseService {
|
|
|
2241
2482
|
return aligned;
|
|
2242
2483
|
}
|
|
2243
2484
|
async batchRead(keys, action, options) {
|
|
2244
|
-
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
2245
2485
|
if (!this.requireAuth()) return err(authRequiredError("kv"));
|
|
2246
2486
|
if (keys.length === 0) return ok({ results: [], count: 0 });
|
|
2247
2487
|
if (keys.length > MAX_KV_BATCH_READ_ITEMS) {
|
|
@@ -2492,7 +2732,7 @@ var KVService = class extends BaseService {
|
|
|
2492
2732
|
*/
|
|
2493
2733
|
async get(key, options) {
|
|
2494
2734
|
return this.withTelemetry("get", key, async () => {
|
|
2495
|
-
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
2735
|
+
if (options?.source !== "network" && this.readThroughReady !== null) await this.readThroughReady();
|
|
2496
2736
|
if (!this.requireAuth()) {
|
|
2497
2737
|
return err(authRequiredError("kv"));
|
|
2498
2738
|
}
|
|
@@ -2611,7 +2851,7 @@ var KVService = class extends BaseService {
|
|
|
2611
2851
|
*/
|
|
2612
2852
|
async put(key, value, options) {
|
|
2613
2853
|
return this.withTelemetry("put", key, async () => {
|
|
2614
|
-
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
2854
|
+
if (this.readThroughReady !== null) await this.readThroughReady();
|
|
2615
2855
|
if (!this.requireAuth()) {
|
|
2616
2856
|
return err(authRequiredError("kv"));
|
|
2617
2857
|
}
|
|
@@ -2696,7 +2936,7 @@ var KVService = class extends BaseService {
|
|
|
2696
2936
|
*/
|
|
2697
2937
|
async batchPut(items, options) {
|
|
2698
2938
|
return this.withTelemetry("batchPut", String(items.length), async () => {
|
|
2699
|
-
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
2939
|
+
if (this.readThroughReady !== null) await this.readThroughReady();
|
|
2700
2940
|
if (!this.requireAuth()) {
|
|
2701
2941
|
return err(authRequiredError("kv"));
|
|
2702
2942
|
}
|
|
@@ -2895,7 +3135,7 @@ var KVService = class extends BaseService {
|
|
|
2895
3135
|
*/
|
|
2896
3136
|
async list(options) {
|
|
2897
3137
|
return this.withTelemetry("list", options?.prefix, async () => {
|
|
2898
|
-
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
3138
|
+
if (options?.source !== "network" && this.readThroughReady !== null) await this.readThroughReady();
|
|
2899
3139
|
if (!this.requireAuth()) {
|
|
2900
3140
|
return err(authRequiredError("kv"));
|
|
2901
3141
|
}
|
|
@@ -3183,7 +3423,7 @@ var KVService = class extends BaseService {
|
|
|
3183
3423
|
*/
|
|
3184
3424
|
async delete(key, options) {
|
|
3185
3425
|
return this.withTelemetry("delete", key, async () => {
|
|
3186
|
-
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
3426
|
+
if (this.readThroughReady !== null) await this.readThroughReady();
|
|
3187
3427
|
if (!this.requireAuth()) {
|
|
3188
3428
|
return err(authRequiredError("kv"));
|
|
3189
3429
|
}
|
|
@@ -5786,9 +6026,9 @@ function utf8Encode(s) {
|
|
|
5786
6026
|
function utf8Decode(b) {
|
|
5787
6027
|
return new TextDecoder().decode(b);
|
|
5788
6028
|
}
|
|
5789
|
-
function canonicalHashHex(
|
|
6029
|
+
function canonicalHashHex(sha2562, value) {
|
|
5790
6030
|
const canonical = canonicalize(value);
|
|
5791
|
-
return hexEncode(
|
|
6031
|
+
return hexEncode(sha2562(utf8Encode(canonical)));
|
|
5792
6032
|
}
|
|
5793
6033
|
|
|
5794
6034
|
// src/vault/types.ts
|
|
@@ -6030,6 +6270,8 @@ function defaultVaultMessage(input) {
|
|
|
6030
6270
|
case "STORAGE_QUOTA_EXCEEDED":
|
|
6031
6271
|
case "STORAGE_LIMIT_REACHED":
|
|
6032
6272
|
return input.message;
|
|
6273
|
+
default:
|
|
6274
|
+
return input.message ?? input.code;
|
|
6033
6275
|
}
|
|
6034
6276
|
}
|
|
6035
6277
|
function vaultError(input) {
|
|
@@ -6081,6 +6323,8 @@ function ifMatchWriteError(key, error) {
|
|
|
6081
6323
|
});
|
|
6082
6324
|
}
|
|
6083
6325
|
function kvWriteError(context, error) {
|
|
6326
|
+
const routing = routingFailure(error);
|
|
6327
|
+
if (routing) return vaultError(routing);
|
|
6084
6328
|
if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
|
|
6085
6329
|
return vaultError({ code: error.code, message: error.message, meta: error.meta });
|
|
6086
6330
|
}
|
|
@@ -6387,6 +6631,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6387
6631
|
{ aad, metadata }
|
|
6388
6632
|
);
|
|
6389
6633
|
if (!envelopeResult.ok) {
|
|
6634
|
+
const routing = routingFailure(envelopeResult.error);
|
|
6635
|
+
if (routing) return vaultError(routing);
|
|
6390
6636
|
return vaultError({
|
|
6391
6637
|
code: "STORAGE_ERROR",
|
|
6392
6638
|
cause: new Error(envelopeResult.error.message)
|
|
@@ -6413,6 +6659,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6413
6659
|
}
|
|
6414
6660
|
return { ok: true, data: void 0 };
|
|
6415
6661
|
} catch (error) {
|
|
6662
|
+
const routing = routingFailure(error);
|
|
6663
|
+
if (routing) return vaultError(routing);
|
|
6416
6664
|
return vaultError({
|
|
6417
6665
|
code: "STORAGE_ERROR",
|
|
6418
6666
|
cause: toError(error)
|
|
@@ -6439,6 +6687,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6439
6687
|
...options?.source === void 0 ? {} : { source: options.source }
|
|
6440
6688
|
});
|
|
6441
6689
|
if (!valueResult.ok) {
|
|
6690
|
+
const routing = routingFailure(valueResult.error);
|
|
6691
|
+
if (routing) return vaultError(routing);
|
|
6442
6692
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6443
6693
|
}
|
|
6444
6694
|
const rawEnvelope = unwrapKVData(valueResult.data);
|
|
@@ -6459,6 +6709,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6459
6709
|
const proof = await this.decryptCapabilityProof();
|
|
6460
6710
|
const plaintextResult = await config.service.decryptEnvelope(envelope, proof);
|
|
6461
6711
|
if (!plaintextResult.ok) {
|
|
6712
|
+
const routing = routingFailure(plaintextResult.error);
|
|
6713
|
+
if (routing) return vaultError(routing);
|
|
6462
6714
|
return vaultError({
|
|
6463
6715
|
code: "DECRYPTION_FAILED",
|
|
6464
6716
|
message: plaintextResult.error.message
|
|
@@ -6478,6 +6730,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6478
6730
|
data: { value, metadata, keyId, ...etag === void 0 ? {} : { etag } }
|
|
6479
6731
|
};
|
|
6480
6732
|
} catch (error) {
|
|
6733
|
+
const routing = routingFailure(error);
|
|
6734
|
+
if (routing) return vaultError(routing);
|
|
6481
6735
|
return vaultError({
|
|
6482
6736
|
code: "STORAGE_ERROR",
|
|
6483
6737
|
cause: toError(error)
|
|
@@ -6490,6 +6744,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6490
6744
|
* Unlike {@link getNetworkEncrypted}, this is intentionally message-free so
|
|
6491
6745
|
* an operation layer can distinguish an authorized absence from a failed
|
|
6492
6746
|
* read without receiving node, envelope, plaintext, or key material.
|
|
6747
|
+
* Typed routing failures reject unchanged rather than becoming an ordinary
|
|
6748
|
+
* read/decrypt failure; the operation boundary projects their safe code.
|
|
6493
6749
|
*/
|
|
6494
6750
|
async readNetworkEncrypted(key, options) {
|
|
6495
6751
|
const config = this.networkEncryption;
|
|
@@ -6499,10 +6755,12 @@ var DataVaultService = class extends BaseService {
|
|
|
6499
6755
|
let valueResult;
|
|
6500
6756
|
try {
|
|
6501
6757
|
valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
|
|
6502
|
-
} catch {
|
|
6758
|
+
} catch (error) {
|
|
6759
|
+
if (routingFailure(error)) throw error;
|
|
6503
6760
|
return { status: "node_unreachable" };
|
|
6504
6761
|
}
|
|
6505
6762
|
if (!valueResult.ok) {
|
|
6763
|
+
if (routingFailure(valueResult.error)) throw valueResult.error;
|
|
6506
6764
|
const permissionHint = parsePermissionHint(valueResult.error.meta?.permissionHint);
|
|
6507
6765
|
if (valueResult.error.code === "AUTH_UNAUTHORIZED" && permissionHint !== void 0) {
|
|
6508
6766
|
return { status: "permission_required", hint: permissionHint };
|
|
@@ -6510,7 +6768,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6510
6768
|
if ((valueResult.error.code === "NOT_FOUND" || valueResult.error.code === "KV_NOT_FOUND") && !hasHttpResponse(valueResult.error)) {
|
|
6511
6769
|
return { status: "not_found" };
|
|
6512
6770
|
}
|
|
6513
|
-
if (valueResult.error.code === "NETWORK_ERROR" || valueResult.error.code === "TIMEOUT" || valueResult.error.code === "ABORTED") {
|
|
6771
|
+
if (valueResult.error.code === "NODE_NOT_RESPONDING" || valueResult.error.code === "NETWORK_ERROR" || valueResult.error.code === "TIMEOUT" || valueResult.error.code === "ABORTED") {
|
|
6514
6772
|
return hasHttpResponse(valueResult.error) ? { status: "read_failed" } : { status: "node_unreachable" };
|
|
6515
6773
|
}
|
|
6516
6774
|
return { status: "read_failed" };
|
|
@@ -6532,10 +6790,12 @@ var DataVaultService = class extends BaseService {
|
|
|
6532
6790
|
try {
|
|
6533
6791
|
const proof = await this.decryptCapabilityProof();
|
|
6534
6792
|
plaintextResult = await config.service.decryptEnvelope(envelope, proof);
|
|
6535
|
-
} catch {
|
|
6793
|
+
} catch (error) {
|
|
6794
|
+
if (routingFailure(error)) throw error;
|
|
6536
6795
|
return { status: "decrypt_failed" };
|
|
6537
6796
|
}
|
|
6538
6797
|
if (!plaintextResult.ok) {
|
|
6798
|
+
if (routingFailure(plaintextResult.error)) throw plaintextResult.error;
|
|
6539
6799
|
const permissionHint = parsePermissionHint(
|
|
6540
6800
|
plaintextResult.error.permissionHint
|
|
6541
6801
|
);
|
|
@@ -6545,7 +6805,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6545
6805
|
if (plaintextResult.error.code === "INVALID_ENVELOPE") {
|
|
6546
6806
|
return { status: "corrupt_envelope" };
|
|
6547
6807
|
}
|
|
6548
|
-
if (plaintextResult.error.code === "TRANSPORT_ERROR") {
|
|
6808
|
+
if (plaintextResult.error.code === "NODE_NOT_RESPONDING" || plaintextResult.error.code === "TRANSPORT_ERROR") {
|
|
6549
6809
|
return { status: "node_unreachable" };
|
|
6550
6810
|
}
|
|
6551
6811
|
return { status: "decrypt_failed" };
|
|
@@ -6797,6 +7057,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6797
7057
|
if (this.usesNetworkEncryption) {
|
|
6798
7058
|
const valueDelResult2 = await this.tc.kv.delete(`vault/${key}`);
|
|
6799
7059
|
if (!valueDelResult2.ok) {
|
|
7060
|
+
const routing = routingFailure(valueDelResult2.error);
|
|
7061
|
+
if (routing) return vaultError(routing);
|
|
6800
7062
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6801
7063
|
}
|
|
6802
7064
|
return ok(void 0);
|
|
@@ -6806,10 +7068,14 @@ var DataVaultService = class extends BaseService {
|
|
|
6806
7068
|
this.tc.kv.delete(`vault/${key}`)
|
|
6807
7069
|
]);
|
|
6808
7070
|
if (!keyDelResult.ok && !valueDelResult.ok) {
|
|
7071
|
+
const routing = routingFailure(keyDelResult.error) ?? routingFailure(valueDelResult.error);
|
|
7072
|
+
if (routing) return vaultError(routing);
|
|
6809
7073
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6810
7074
|
}
|
|
6811
7075
|
return ok(void 0);
|
|
6812
7076
|
} catch (error) {
|
|
7077
|
+
const routing = routingFailure(error);
|
|
7078
|
+
if (routing) return vaultError(routing);
|
|
6813
7079
|
return vaultError({
|
|
6814
7080
|
code: "STORAGE_ERROR",
|
|
6815
7081
|
cause: toError(error)
|
|
@@ -6851,6 +7117,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6851
7117
|
...options?.cursor === void 0 ? {} : { cursor: options.cursor }
|
|
6852
7118
|
});
|
|
6853
7119
|
if (!listResult.ok) {
|
|
7120
|
+
const routing = routingFailure(listResult.error);
|
|
7121
|
+
if (routing) return vaultError(routing);
|
|
6854
7122
|
return vaultError({
|
|
6855
7123
|
code: "STORAGE_ERROR",
|
|
6856
7124
|
cause: new Error(
|
|
@@ -6871,6 +7139,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6871
7139
|
...listResult.data.nextCursor === void 0 ? {} : { nextCursor: listResult.data.nextCursor }
|
|
6872
7140
|
});
|
|
6873
7141
|
} catch (error) {
|
|
7142
|
+
const routing = routingFailure(error);
|
|
7143
|
+
if (routing) return vaultError(routing);
|
|
6874
7144
|
return vaultError({
|
|
6875
7145
|
code: "STORAGE_ERROR",
|
|
6876
7146
|
cause: toError(error)
|
|
@@ -7702,6 +7972,8 @@ function defaultEncryptionMessage(input) {
|
|
|
7702
7972
|
return input.message ?? input.cause.message;
|
|
7703
7973
|
case "INVALID_INPUT":
|
|
7704
7974
|
return input.message;
|
|
7975
|
+
default:
|
|
7976
|
+
return input.message ?? input.code;
|
|
7705
7977
|
}
|
|
7706
7978
|
}
|
|
7707
7979
|
function encryptionError(input) {
|
|
@@ -7766,6 +8038,8 @@ async function discoverNetwork(input) {
|
|
|
7766
8038
|
return { ok: true, data: { descriptor: validated.data, source: "node" } };
|
|
7767
8039
|
}
|
|
7768
8040
|
} catch (err3) {
|
|
8041
|
+
const routing = routingFailure(err3);
|
|
8042
|
+
if (routing) return { ok: false, error: encryptionError(routing) };
|
|
7769
8043
|
}
|
|
7770
8044
|
}
|
|
7771
8045
|
if (input.wellKnown !== void 0) {
|
|
@@ -7783,6 +8057,8 @@ async function discoverNetwork(input) {
|
|
|
7783
8057
|
};
|
|
7784
8058
|
}
|
|
7785
8059
|
} catch (err3) {
|
|
8060
|
+
const routing = routingFailure(err3);
|
|
8061
|
+
if (routing) return { ok: false, error: encryptionError(routing) };
|
|
7786
8062
|
}
|
|
7787
8063
|
}
|
|
7788
8064
|
return {
|
|
@@ -7872,6 +8148,152 @@ function ensureNetworkUsableForDecrypt(descriptor) {
|
|
|
7872
8148
|
};
|
|
7873
8149
|
}
|
|
7874
8150
|
|
|
8151
|
+
// src/encryption/keyWrap.ts
|
|
8152
|
+
import { gcm } from "@noble/ciphers/aes";
|
|
8153
|
+
import { x25519 } from "@noble/curves/ed25519";
|
|
8154
|
+
import { hkdf } from "@noble/hashes/hkdf";
|
|
8155
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
8156
|
+
import { randomBytes } from "@noble/hashes/utils";
|
|
8157
|
+
var KEY_WRAP_V2_ALG = "x25519-hkdf-aes256gcm/v2";
|
|
8158
|
+
var MAGIC = utf8Encode("TCW2");
|
|
8159
|
+
var INFO = utf8Encode("tinycloud.encryption.key-wrap/v2");
|
|
8160
|
+
var MAX_HEADER = 65536;
|
|
8161
|
+
function concat(...parts) {
|
|
8162
|
+
const result = new Uint8Array(parts.reduce((length, part) => length + part.length, 0));
|
|
8163
|
+
let offset = 0;
|
|
8164
|
+
for (const part of parts) {
|
|
8165
|
+
result.set(part, offset);
|
|
8166
|
+
offset += part.length;
|
|
8167
|
+
}
|
|
8168
|
+
return result;
|
|
8169
|
+
}
|
|
8170
|
+
function equal(a, b) {
|
|
8171
|
+
if (a.length !== b.length) return false;
|
|
8172
|
+
let difference = 0;
|
|
8173
|
+
for (let i = 0; i < a.length; i++) difference |= a[i] ^ b[i];
|
|
8174
|
+
return difference === 0;
|
|
8175
|
+
}
|
|
8176
|
+
function decode32(value) {
|
|
8177
|
+
if (typeof value !== "string") throw new Error("Invalid key wrap encoding");
|
|
8178
|
+
const decoded = base64Decode(value);
|
|
8179
|
+
if (decoded.length !== 32 || base64Encode(decoded) !== value) throw new Error("Invalid key wrap encoding");
|
|
8180
|
+
return decoded;
|
|
8181
|
+
}
|
|
8182
|
+
function isWellFormed(value) {
|
|
8183
|
+
for (let i = 0; i < value.length; i++) {
|
|
8184
|
+
const unit = value.charCodeAt(i);
|
|
8185
|
+
if (unit >= 55296 && unit <= 56319) {
|
|
8186
|
+
const next = value.charCodeAt(++i);
|
|
8187
|
+
if (!(next >= 56320 && next <= 57343)) return false;
|
|
8188
|
+
} else if (unit >= 56320 && unit <= 57343) return false;
|
|
8189
|
+
}
|
|
8190
|
+
return true;
|
|
8191
|
+
}
|
|
8192
|
+
function validateContext(context) {
|
|
8193
|
+
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))) {
|
|
8194
|
+
throw new Error("Invalid key wrap context");
|
|
8195
|
+
}
|
|
8196
|
+
if (context.networkId !== "") {
|
|
8197
|
+
const resource = parseEncryptionResource(context.keyPath === "" ? context.networkId : `${context.networkId}/${context.keyPath}`);
|
|
8198
|
+
if (resource.networkId !== context.networkId || (resource.keyPath ?? "") !== context.keyPath || context.keyPath.endsWith("/")) {
|
|
8199
|
+
throw new Error("Invalid key wrap resource");
|
|
8200
|
+
}
|
|
8201
|
+
}
|
|
8202
|
+
}
|
|
8203
|
+
function isKeyWrapV2(wrapped) {
|
|
8204
|
+
return wrapped.length >= 4 && MAGIC.every((byte, index) => wrapped[index] === byte);
|
|
8205
|
+
}
|
|
8206
|
+
function readKeyWrapV2(wrapped) {
|
|
8207
|
+
if (!isKeyWrapV2(wrapped) || wrapped.length < 8) throw new Error("Invalid v2 key wrap frame");
|
|
8208
|
+
const length = new DataView(wrapped.buffer, wrapped.byteOffset, wrapped.byteLength).getUint32(4);
|
|
8209
|
+
if (length === 0 || length > MAX_HEADER || wrapped.length < 8 + length + 12 + 16) throw new Error("Invalid v2 key wrap length");
|
|
8210
|
+
const bytes = wrapped.subarray(8, 8 + length);
|
|
8211
|
+
const header = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
8212
|
+
const fields = ["v", "alg", "kdf", "aead", "salt", "ephemeralPublicKey", "recipientPublicKey", "purpose", "networkId", "keyPath", "keyVersion", "metadata"].sort();
|
|
8213
|
+
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") {
|
|
8214
|
+
throw new Error("Invalid v2 key wrap header");
|
|
8215
|
+
}
|
|
8216
|
+
validateContext(header);
|
|
8217
|
+
decode32(header.salt);
|
|
8218
|
+
decode32(header.ephemeralPublicKey);
|
|
8219
|
+
decode32(header.recipientPublicKey);
|
|
8220
|
+
if (!equal(bytes, utf8Encode(canonicalize({ ...header })))) throw new Error("Non-canonical v2 key wrap header");
|
|
8221
|
+
const end = 8 + length;
|
|
8222
|
+
return { header, aad: wrapped.subarray(0, end), nonce: wrapped.subarray(end, end + 12), ciphertext: wrapped.subarray(end + 12) };
|
|
8223
|
+
}
|
|
8224
|
+
function assertKeyWrapContext(header, expected) {
|
|
8225
|
+
validateContext(expected);
|
|
8226
|
+
for (const field of ["purpose", "networkId", "keyPath", "keyVersion"]) {
|
|
8227
|
+
if (header[field] !== expected[field]) throw new Error("Key wrap context mismatch");
|
|
8228
|
+
}
|
|
8229
|
+
if (canonicalize(header.metadata) !== canonicalize(expected.metadata)) throw new Error("Key wrap metadata mismatch");
|
|
8230
|
+
}
|
|
8231
|
+
function deriveKey(secret, ephemeral, recipient, salt) {
|
|
8232
|
+
const shared = x25519.getSharedSecret(secret, ephemeral);
|
|
8233
|
+
if (shared.every((byte) => byte === 0)) throw new Error("Invalid key agreement");
|
|
8234
|
+
try {
|
|
8235
|
+
return hkdf(sha256, shared, salt, concat(INFO, ephemeral, recipient), 32);
|
|
8236
|
+
} finally {
|
|
8237
|
+
shared.fill(0);
|
|
8238
|
+
}
|
|
8239
|
+
}
|
|
8240
|
+
function sealKeyV2(recipientPublicKey, plaintext, context) {
|
|
8241
|
+
validateContext(context);
|
|
8242
|
+
if (recipientPublicKey.length !== 32) throw new Error("Invalid recipient public key");
|
|
8243
|
+
const secret = randomBytes(32);
|
|
8244
|
+
const ephemeral = x25519.getPublicKey(secret);
|
|
8245
|
+
const salt = randomBytes(32);
|
|
8246
|
+
const header = {
|
|
8247
|
+
...context,
|
|
8248
|
+
v: 2,
|
|
8249
|
+
alg: KEY_WRAP_V2_ALG,
|
|
8250
|
+
kdf: "hkdf-sha256",
|
|
8251
|
+
aead: "aes-256-gcm",
|
|
8252
|
+
salt: base64Encode(salt),
|
|
8253
|
+
ephemeralPublicKey: base64Encode(ephemeral),
|
|
8254
|
+
recipientPublicKey: base64Encode(recipientPublicKey)
|
|
8255
|
+
};
|
|
8256
|
+
const bytes = utf8Encode(canonicalize({ ...header }));
|
|
8257
|
+
if (bytes.length > MAX_HEADER) throw new Error("Key wrap header too large");
|
|
8258
|
+
const prefix = new Uint8Array(8);
|
|
8259
|
+
prefix.set(MAGIC);
|
|
8260
|
+
new DataView(prefix.buffer).setUint32(4, bytes.length);
|
|
8261
|
+
const aad = concat(prefix, bytes);
|
|
8262
|
+
const nonce = randomBytes(12);
|
|
8263
|
+
let key;
|
|
8264
|
+
try {
|
|
8265
|
+
const shared = x25519.getSharedSecret(secret, recipientPublicKey);
|
|
8266
|
+
try {
|
|
8267
|
+
if (shared.every((byte) => byte === 0)) throw new Error("Invalid key agreement");
|
|
8268
|
+
key = hkdf(sha256, shared, salt, concat(INFO, ephemeral, recipientPublicKey), 32);
|
|
8269
|
+
} finally {
|
|
8270
|
+
shared.fill(0);
|
|
8271
|
+
}
|
|
8272
|
+
return concat(aad, nonce, gcm(key, nonce, aad).encrypt(plaintext));
|
|
8273
|
+
} finally {
|
|
8274
|
+
secret.fill(0);
|
|
8275
|
+
key?.fill(0);
|
|
8276
|
+
}
|
|
8277
|
+
}
|
|
8278
|
+
function openKeyV2(recipientPrivateKey, wrapped, expected) {
|
|
8279
|
+
const { header, aad, nonce, ciphertext } = readKeyWrapV2(wrapped);
|
|
8280
|
+
assertKeyWrapContext(header, expected);
|
|
8281
|
+
const recipient = x25519.getPublicKey(recipientPrivateKey);
|
|
8282
|
+
if (!equal(recipient, decode32(header.recipientPublicKey))) throw new Error("Key wrap recipient mismatch");
|
|
8283
|
+
const key = deriveKey(recipientPrivateKey, decode32(header.ephemeralPublicKey), recipient, decode32(header.salt));
|
|
8284
|
+
try {
|
|
8285
|
+
return gcm(key, nonce, aad).decrypt(ciphertext);
|
|
8286
|
+
} finally {
|
|
8287
|
+
key.fill(0);
|
|
8288
|
+
}
|
|
8289
|
+
}
|
|
8290
|
+
function assertKeyWrapAlgorithm(wrapped, alg, requireV2 = false) {
|
|
8291
|
+
if (alg !== "x25519-aes256gcm/v1" && alg !== KEY_WRAP_V2_ALG) throw new Error("Unsupported key wrap algorithm");
|
|
8292
|
+
if (requireV2 && alg !== KEY_WRAP_V2_ALG) throw new Error("V2 network key wrap required");
|
|
8293
|
+
if (isKeyWrapV2(wrapped) !== (alg === KEY_WRAP_V2_ALG)) throw new Error("Key wrap algorithm mismatch");
|
|
8294
|
+
if (alg === KEY_WRAP_V2_ALG) readKeyWrapV2(wrapped);
|
|
8295
|
+
}
|
|
8296
|
+
|
|
7875
8297
|
// src/encryption/envelope.ts
|
|
7876
8298
|
function encryptToNetwork(crypto2, input) {
|
|
7877
8299
|
const parsedResource = parseEncryptionResource(
|
|
@@ -7880,11 +8302,20 @@ function encryptToNetwork(crypto2, input) {
|
|
|
7880
8302
|
if (parsedResource.keyPath !== input.keyPath || input.keyPath?.endsWith("/")) {
|
|
7881
8303
|
throw new NetworkIdError("keyPath must name an exact encryption key");
|
|
7882
8304
|
}
|
|
7883
|
-
const alg = input.
|
|
8305
|
+
const alg = input.keyWrapV2 ? KEY_WRAP_V2_ALG : DEFAULT_ENCRYPTION_ALG;
|
|
8306
|
+
if (input.alg !== void 0 && input.alg !== alg) {
|
|
8307
|
+
throw new Error("Algorithm does not match keyWrapV2");
|
|
8308
|
+
}
|
|
7884
8309
|
const keyVersion = input.keyVersion ?? DEFAULT_KEY_VERSION;
|
|
7885
8310
|
const symmetricKey = crypto2.randomBytes(32);
|
|
7886
8311
|
const ciphertext = crypto2.authEncrypt(symmetricKey, input.plaintext, input.aad);
|
|
7887
|
-
const wrapped =
|
|
8312
|
+
const wrapped = input.keyWrapV2 ? sealKeyV2(input.networkPublicKey, symmetricKey, {
|
|
8313
|
+
purpose: "network-key",
|
|
8314
|
+
networkId: input.networkId,
|
|
8315
|
+
keyPath: input.keyPath ?? "",
|
|
8316
|
+
keyVersion,
|
|
8317
|
+
metadata: input.metadata ?? {}
|
|
8318
|
+
}) : crypto2.sealToNetworkKey(input.networkPublicKey, symmetricKey);
|
|
7888
8319
|
const encryptedSymmetricKey = base64Encode(wrapped);
|
|
7889
8320
|
const encryptedSymmetricKeyHash = canonicalHashHex(
|
|
7890
8321
|
crypto2.sha256,
|
|
@@ -7904,7 +8335,7 @@ function encryptToNetwork(crypto2, input) {
|
|
|
7904
8335
|
};
|
|
7905
8336
|
return { envelope, symmetricKey };
|
|
7906
8337
|
}
|
|
7907
|
-
function validateEnvelope(crypto2, envelope) {
|
|
8338
|
+
function validateEnvelope(crypto2, envelope, requireKeyWrapV2 = false) {
|
|
7908
8339
|
if (envelope === null || typeof envelope !== "object") {
|
|
7909
8340
|
return {
|
|
7910
8341
|
ok: false,
|
|
@@ -7981,9 +8412,28 @@ function validateEnvelope(crypto2, envelope) {
|
|
|
7981
8412
|
})
|
|
7982
8413
|
};
|
|
7983
8414
|
}
|
|
8415
|
+
try {
|
|
8416
|
+
validateEnvelopeKeyWrap(e, requireKeyWrapV2);
|
|
8417
|
+
} catch {
|
|
8418
|
+
return { ok: false, error: encryptionError({ code: "INVALID_ENVELOPE", message: "Envelope key wrap context is invalid" }) };
|
|
8419
|
+
}
|
|
7984
8420
|
return { ok: true, data: e };
|
|
7985
8421
|
}
|
|
8422
|
+
function validateEnvelopeKeyWrap(envelope, requireKeyWrapV2 = false) {
|
|
8423
|
+
const wrapped = base64Decode(envelope.encryptedSymmetricKey);
|
|
8424
|
+
assertKeyWrapAlgorithm(wrapped, envelope.alg, requireKeyWrapV2);
|
|
8425
|
+
if (envelope.alg === KEY_WRAP_V2_ALG) {
|
|
8426
|
+
assertKeyWrapContext(readKeyWrapV2(wrapped).header, {
|
|
8427
|
+
purpose: "network-key",
|
|
8428
|
+
networkId: envelope.networkId,
|
|
8429
|
+
keyPath: envelope.keyPath ?? "",
|
|
8430
|
+
keyVersion: envelope.keyVersion,
|
|
8431
|
+
metadata: envelope.metadata ?? {}
|
|
8432
|
+
});
|
|
8433
|
+
}
|
|
8434
|
+
}
|
|
7986
8435
|
function decryptEnvelopeWithKey(crypto2, envelope, symmetricKey) {
|
|
8436
|
+
validateEnvelopeKeyWrap(envelope);
|
|
7987
8437
|
const ciphertext = base64Decode(envelope.ciphertext);
|
|
7988
8438
|
const aad = envelope.aad !== void 0 ? base64Decode(envelope.aad) : void 0;
|
|
7989
8439
|
return crypto2.authDecrypt(symmetricKey, ciphertext, aad);
|
|
@@ -8273,7 +8723,7 @@ function verifyDecryptResponse(input) {
|
|
|
8273
8723
|
})
|
|
8274
8724
|
};
|
|
8275
8725
|
}
|
|
8276
|
-
if (response.alg !==
|
|
8726
|
+
if (response.alg !== "x25519-aes256gcm/v1" && response.alg !== KEY_WRAP_V2_ALG) {
|
|
8277
8727
|
return {
|
|
8278
8728
|
ok: false,
|
|
8279
8729
|
error: encryptionError({
|
|
@@ -8330,7 +8780,7 @@ function verifyDecryptResponse(input) {
|
|
|
8330
8780
|
})
|
|
8331
8781
|
};
|
|
8332
8782
|
}
|
|
8333
|
-
if (facts.encryptedSymmetricKeyHash !== response.encryptedSymmetricKeyHash || facts.receiverPublicKeyHash !== response.receiverPublicKeyHash || facts.networkId !== response.networkId || facts.targetNode !== response.targetNode || facts.alg !==
|
|
8783
|
+
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
8784
|
return {
|
|
8335
8785
|
ok: false,
|
|
8336
8786
|
error: encryptionError({
|
|
@@ -8353,8 +8803,13 @@ function verifyDecryptResponse(input) {
|
|
|
8353
8803
|
}
|
|
8354
8804
|
return { ok: true, data: response };
|
|
8355
8805
|
}
|
|
8356
|
-
function openWrappedKey(crypto2, receiverPrivateKey, response) {
|
|
8806
|
+
function openWrappedKey(crypto2, receiverPrivateKey, response, expected) {
|
|
8357
8807
|
const wrapped = base64Decode(response.wrappedKey);
|
|
8808
|
+
assertKeyWrapAlgorithm(wrapped, response.alg);
|
|
8809
|
+
if (response.alg === KEY_WRAP_V2_ALG) {
|
|
8810
|
+
if (!expected) throw new Error("V2 response requires expected key context");
|
|
8811
|
+
return openKeyV2(receiverPrivateKey, wrapped, expected);
|
|
8812
|
+
}
|
|
8358
8813
|
return crypto2.openWithReceiverKey(receiverPrivateKey, wrapped);
|
|
8359
8814
|
}
|
|
8360
8815
|
|
|
@@ -8367,6 +8822,14 @@ function encOk(data) {
|
|
|
8367
8822
|
function encErr(error) {
|
|
8368
8823
|
return { ok: false, error };
|
|
8369
8824
|
}
|
|
8825
|
+
function decryptionError(input) {
|
|
8826
|
+
const unavailable = input.code === "NODE_NOT_RESPONDING" || input.code === "TRANSPORT_ERROR" && input.cause.name !== "AbortError";
|
|
8827
|
+
return encryptionError(unavailable ? {
|
|
8828
|
+
...input,
|
|
8829
|
+
code: "DECRYPTION_DEPENDENCY_UNAVAILABLE",
|
|
8830
|
+
message: "The decryption service isn't responding right now. Please try again."
|
|
8831
|
+
} : input);
|
|
8832
|
+
}
|
|
8370
8833
|
var EncryptionService = class extends BaseService {
|
|
8371
8834
|
constructor(config) {
|
|
8372
8835
|
super();
|
|
@@ -8408,12 +8871,15 @@ var EncryptionService = class extends BaseService {
|
|
|
8408
8871
|
networkPublicKey,
|
|
8409
8872
|
plaintext,
|
|
8410
8873
|
...options?.aad !== void 0 ? { aad: options.aad } : {},
|
|
8411
|
-
|
|
8874
|
+
keyWrapV2: options?.keyWrapV2 ?? this._config.keyWrapV2 ?? false,
|
|
8875
|
+
...options?.alg !== void 0 ? { alg: options.alg } : {},
|
|
8412
8876
|
keyVersion: options?.keyVersion ?? descriptor.keyVersion,
|
|
8413
8877
|
...options?.metadata !== void 0 ? { metadata: options.metadata } : {}
|
|
8414
8878
|
});
|
|
8415
8879
|
return encOk(result.envelope);
|
|
8416
8880
|
} catch (error) {
|
|
8881
|
+
const routing = routingFailure(error);
|
|
8882
|
+
if (routing) return encErr(encryptionError(routing));
|
|
8417
8883
|
return encErr(
|
|
8418
8884
|
encryptionError({
|
|
8419
8885
|
code: "TRANSPORT_ERROR",
|
|
@@ -8425,7 +8891,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8425
8891
|
async decryptEnvelope(envelope, capabilityProof, options) {
|
|
8426
8892
|
try {
|
|
8427
8893
|
this.assertActive();
|
|
8428
|
-
const validated = validateEnvelope(this.crypto, envelope);
|
|
8894
|
+
const validated = validateEnvelope(this.crypto, envelope, this._config.requireKeyWrapV2 ?? false);
|
|
8429
8895
|
if (!validated.ok) return validated;
|
|
8430
8896
|
if (options?.aad !== void 0 && validated.data.aad !== base64Encode(options.aad)) {
|
|
8431
8897
|
return encErr(
|
|
@@ -8440,12 +8906,12 @@ var EncryptionService = class extends BaseService {
|
|
|
8440
8906
|
descriptor = options.descriptor;
|
|
8441
8907
|
} else {
|
|
8442
8908
|
const discovered = await this.discoverNetwork(envelope.networkId);
|
|
8443
|
-
if (!discovered.ok) return discovered;
|
|
8909
|
+
if (!discovered.ok) return encErr(decryptionError(discovered.error));
|
|
8444
8910
|
descriptor = discovered.data;
|
|
8445
8911
|
}
|
|
8446
8912
|
const usable = ensureNetworkUsableForDecrypt(descriptor);
|
|
8447
8913
|
if (!usable.ok) return usable;
|
|
8448
|
-
const targetNode = options?.targetNode ?? descriptor.members[0]?.nodeId;
|
|
8914
|
+
const targetNode = this._config.resolveTargetNode ? await this._config.resolveTargetNode(envelope.networkId) : options?.targetNode ?? descriptor.members[0]?.nodeId;
|
|
8449
8915
|
if (targetNode === void 0) {
|
|
8450
8916
|
return encErr(
|
|
8451
8917
|
encryptionError({
|
|
@@ -8454,6 +8920,9 @@ var EncryptionService = class extends BaseService {
|
|
|
8454
8920
|
})
|
|
8455
8921
|
);
|
|
8456
8922
|
}
|
|
8923
|
+
if (this._config.resolveTargetNode && (options?.targetNode !== void 0 && options.targetNode !== targetNode || !descriptor.members.some((member) => member.nodeId === targetNode))) {
|
|
8924
|
+
return encErr(encryptionError({ code: "INVALID_INPUT", message: "Encryption destination does not match the registry" }));
|
|
8925
|
+
}
|
|
8457
8926
|
const receiverKey = generateRandomReceiverKey({ crypto: this.crypto });
|
|
8458
8927
|
const receiverPublicKey = base64Encode(receiverKey.publicKey);
|
|
8459
8928
|
const receiverPublicKeyHash = canonicalHashHex(
|
|
@@ -8512,6 +8981,8 @@ var EncryptionService = class extends BaseService {
|
|
|
8512
8981
|
});
|
|
8513
8982
|
this.assertActive();
|
|
8514
8983
|
} catch (error) {
|
|
8984
|
+
const routing = routingFailure(error);
|
|
8985
|
+
if (routing) return encErr(decryptionError(routing));
|
|
8515
8986
|
if (error instanceof DecryptTransportResponseError) {
|
|
8516
8987
|
return encErr(
|
|
8517
8988
|
encryptionError(
|
|
@@ -8528,7 +8999,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8528
8999
|
);
|
|
8529
9000
|
}
|
|
8530
9001
|
return encErr(
|
|
8531
|
-
|
|
9002
|
+
decryptionError({
|
|
8532
9003
|
code: "TRANSPORT_ERROR",
|
|
8533
9004
|
cause: toError2(error)
|
|
8534
9005
|
})
|
|
@@ -8546,7 +9017,8 @@ var EncryptionService = class extends BaseService {
|
|
|
8546
9017
|
const symmetricKey = openWrappedKey(
|
|
8547
9018
|
this.crypto,
|
|
8548
9019
|
receiverKey.privateKey,
|
|
8549
|
-
verified.data
|
|
9020
|
+
verified.data,
|
|
9021
|
+
{ purpose: "receiver-key", networkId: body.networkId, keyPath: body.keyPath ?? "", keyVersion: body.keyVersion, metadata: {} }
|
|
8550
9022
|
);
|
|
8551
9023
|
const plaintext = decryptEnvelopeWithKey(
|
|
8552
9024
|
this.crypto,
|
|
@@ -8555,6 +9027,8 @@ var EncryptionService = class extends BaseService {
|
|
|
8555
9027
|
);
|
|
8556
9028
|
return encOk(plaintext);
|
|
8557
9029
|
} catch (error) {
|
|
9030
|
+
const routing = routingFailure(error);
|
|
9031
|
+
if (routing) return encErr(decryptionError(routing));
|
|
8558
9032
|
return encErr(
|
|
8559
9033
|
encryptionError({
|
|
8560
9034
|
code: "INVALID_RESPONSE",
|
|
@@ -8603,7 +9077,11 @@ export {
|
|
|
8603
9077
|
MAX_SECRET_SCOPE_LENGTH,
|
|
8604
9078
|
NETWORK_NAME_PATTERN,
|
|
8605
9079
|
NetworkIdError,
|
|
9080
|
+
NodeIdentityMismatchError,
|
|
9081
|
+
NodeNotRespondingError,
|
|
9082
|
+
PartialNodeWriteError,
|
|
8606
9083
|
PrefixedKVService,
|
|
9084
|
+
ResourceNotRoutedError,
|
|
8607
9085
|
RetryPolicySchema,
|
|
8608
9086
|
SECRET_NAME_RE,
|
|
8609
9087
|
SECRET_SCOPE_RE,
|
|
@@ -8651,6 +9129,8 @@ export {
|
|
|
8651
9129
|
checkDecryptInvocationInput,
|
|
8652
9130
|
clearTinyCloudDebugLogs,
|
|
8653
9131
|
createKVResponseSchema,
|
|
9132
|
+
createResourceRoutingFetch,
|
|
9133
|
+
createResourceTransportInvocationFetch,
|
|
8654
9134
|
createResultSchema,
|
|
8655
9135
|
createVaultCrypto,
|
|
8656
9136
|
decryptEnvelopeWithKey,
|
|
@@ -8693,6 +9173,9 @@ export {
|
|
|
8693
9173
|
requiresSecretsOptIn,
|
|
8694
9174
|
resolveSecretListPrefix,
|
|
8695
9175
|
resolveSecretPath,
|
|
9176
|
+
routeResourceRequest,
|
|
9177
|
+
routingFailure,
|
|
9178
|
+
routingResourceOf,
|
|
8696
9179
|
serviceError,
|
|
8697
9180
|
storageLimitReachedError,
|
|
8698
9181
|
storageQuotaExceededError,
|
|
@@ -8713,6 +9196,7 @@ export {
|
|
|
8713
9196
|
validateTelemetrySpanEvent,
|
|
8714
9197
|
validatedCapabilityOf,
|
|
8715
9198
|
verifyDecryptResponse,
|
|
9199
|
+
verifyResourceDestination,
|
|
8716
9200
|
wrapError
|
|
8717
9201
|
};
|
|
8718
9202
|
//# sourceMappingURL=index.js.map
|