@tinycloud/sdk-services 3.11.0-beta.2 → 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.cjs
CHANGED
|
@@ -51,7 +51,11 @@ __export(src_exports, {
|
|
|
51
51
|
MAX_SECRET_SCOPE_LENGTH: () => MAX_SECRET_SCOPE_LENGTH,
|
|
52
52
|
NETWORK_NAME_PATTERN: () => NETWORK_NAME_PATTERN,
|
|
53
53
|
NetworkIdError: () => NetworkIdError,
|
|
54
|
+
NodeIdentityMismatchError: () => NodeIdentityMismatchError,
|
|
55
|
+
NodeNotRespondingError: () => NodeNotRespondingError,
|
|
56
|
+
PartialNodeWriteError: () => PartialNodeWriteError,
|
|
54
57
|
PrefixedKVService: () => PrefixedKVService,
|
|
58
|
+
ResourceNotRoutedError: () => ResourceNotRoutedError,
|
|
55
59
|
RetryPolicySchema: () => RetryPolicySchema,
|
|
56
60
|
SECRET_NAME_RE: () => SECRET_NAME_RE,
|
|
57
61
|
SECRET_SCOPE_RE: () => SECRET_SCOPE_RE,
|
|
@@ -99,6 +103,8 @@ __export(src_exports, {
|
|
|
99
103
|
checkDecryptInvocationInput: () => checkDecryptInvocationInput,
|
|
100
104
|
clearTinyCloudDebugLogs: () => clearTinyCloudDebugLogs,
|
|
101
105
|
createKVResponseSchema: () => createKVResponseSchema,
|
|
106
|
+
createResourceRoutingFetch: () => createResourceRoutingFetch,
|
|
107
|
+
createResourceTransportInvocationFetch: () => createResourceTransportInvocationFetch,
|
|
102
108
|
createResultSchema: () => createResultSchema,
|
|
103
109
|
createVaultCrypto: () => createVaultCrypto,
|
|
104
110
|
decryptEnvelopeWithKey: () => decryptEnvelopeWithKey,
|
|
@@ -141,6 +147,9 @@ __export(src_exports, {
|
|
|
141
147
|
requiresSecretsOptIn: () => requiresSecretsOptIn,
|
|
142
148
|
resolveSecretListPrefix: () => resolveSecretListPrefix,
|
|
143
149
|
resolveSecretPath: () => resolveSecretPath,
|
|
150
|
+
routeResourceRequest: () => routeResourceRequest,
|
|
151
|
+
routingFailure: () => routingFailure,
|
|
152
|
+
routingResourceOf: () => routingResourceOf,
|
|
144
153
|
serviceError: () => serviceError,
|
|
145
154
|
storageLimitReachedError: () => storageLimitReachedError,
|
|
146
155
|
storageQuotaExceededError: () => storageQuotaExceededError,
|
|
@@ -161,6 +170,7 @@ __export(src_exports, {
|
|
|
161
170
|
validateTelemetrySpanEvent: () => validateTelemetrySpanEvent,
|
|
162
171
|
validatedCapabilityOf: () => validatedCapabilityOf,
|
|
163
172
|
verifyDecryptResponse: () => verifyDecryptResponse,
|
|
173
|
+
verifyResourceDestination: () => verifyResourceDestination,
|
|
164
174
|
wrapError: () => wrapError
|
|
165
175
|
});
|
|
166
176
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -177,6 +187,10 @@ var ErrorCodes = {
|
|
|
177
187
|
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
178
188
|
AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
|
|
179
189
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
190
|
+
NODE_NOT_RESPONDING: "NODE_NOT_RESPONDING",
|
|
191
|
+
NODE_IDENTITY_MISMATCH: "NODE_IDENTITY_MISMATCH",
|
|
192
|
+
PARTIAL_NODE_WRITE: "PARTIAL_NODE_WRITE",
|
|
193
|
+
RESOURCE_NOT_ROUTED: "RESOURCE_NOT_ROUTED",
|
|
180
194
|
TIMEOUT: "TIMEOUT",
|
|
181
195
|
ABORTED: "ABORTED",
|
|
182
196
|
INVALID_INPUT: "INVALID_INPUT",
|
|
@@ -951,6 +965,237 @@ function networkDiscoveryKey(name) {
|
|
|
951
965
|
var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
|
|
952
966
|
var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
|
|
953
967
|
|
|
968
|
+
// src/resourceRouting.ts
|
|
969
|
+
async function verifyResourceDestination(target, fetchFn) {
|
|
970
|
+
if (!target.nodeDid) return;
|
|
971
|
+
let info;
|
|
972
|
+
let keys;
|
|
973
|
+
try {
|
|
974
|
+
const signal = AbortSignal.timeout(5e3);
|
|
975
|
+
const responses = await Promise.all([
|
|
976
|
+
fetchFn(`${target.url}/info`, { signal, redirect: "error" }),
|
|
977
|
+
fetchFn(`${target.url}/.well-known/tinycloud/node-keys`, { signal, redirect: "error" })
|
|
978
|
+
]);
|
|
979
|
+
if (responses.some((response) => response.status >= 500)) throw new NodeNotRespondingError([target.url]);
|
|
980
|
+
if (responses.some((response) => !response.ok)) throw new NodeIdentityMismatchError(target.url, target.nodeDid);
|
|
981
|
+
try {
|
|
982
|
+
[info, keys] = await Promise.all(responses.map((response) => response.json()));
|
|
983
|
+
} catch {
|
|
984
|
+
throw new NodeIdentityMismatchError(target.url, target.nodeDid);
|
|
985
|
+
}
|
|
986
|
+
} catch (error) {
|
|
987
|
+
if (error instanceof NodeIdentityMismatchError || error instanceof NodeNotRespondingError) throw error;
|
|
988
|
+
throw new NodeNotRespondingError([target.url], error);
|
|
989
|
+
}
|
|
990
|
+
const infoDid = typeof info === "object" && info !== null && "nodeId" in info && typeof info.nodeId === "string" ? info.nodeId : void 0;
|
|
991
|
+
const keysDid = typeof keys === "object" && keys !== null && "nodeDid" in keys && typeof keys.nodeDid === "string" ? keys.nodeDid : void 0;
|
|
992
|
+
if (infoDid !== target.nodeDid || keysDid !== target.nodeDid) {
|
|
993
|
+
throw new NodeIdentityMismatchError(target.url, target.nodeDid, infoDid !== target.nodeDid ? infoDid : keysDid);
|
|
994
|
+
}
|
|
995
|
+
return info;
|
|
996
|
+
}
|
|
997
|
+
var NodeNotRespondingError = class extends Error {
|
|
998
|
+
constructor(hosts, cause) {
|
|
999
|
+
super("The place your data is stored isn't responding right now. Please try again.");
|
|
1000
|
+
this.hosts = hosts;
|
|
1001
|
+
this.cause = cause;
|
|
1002
|
+
this.code = "NODE_NOT_RESPONDING";
|
|
1003
|
+
this.name = "NodeNotRespondingError";
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
var NodeIdentityMismatchError = class extends Error {
|
|
1007
|
+
constructor(url, expectedDid, actualDid) {
|
|
1008
|
+
super(`TinyCloud node identity does not match the registry at ${url}`);
|
|
1009
|
+
this.url = url;
|
|
1010
|
+
this.expectedDid = expectedDid;
|
|
1011
|
+
this.actualDid = actualDid;
|
|
1012
|
+
this.code = "NODE_IDENTITY_MISMATCH";
|
|
1013
|
+
this.name = "NodeIdentityMismatchError";
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
var ResourceNotRoutedError = class extends Error {
|
|
1017
|
+
constructor(resource) {
|
|
1018
|
+
super(`No registered destination can serve ${resource}`);
|
|
1019
|
+
this.resource = resource;
|
|
1020
|
+
this.code = "RESOURCE_NOT_ROUTED";
|
|
1021
|
+
this.name = "ResourceNotRoutedError";
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
var PartialNodeWriteError = class extends Error {
|
|
1025
|
+
constructor(succeeded, failed) {
|
|
1026
|
+
super("The write did not complete on every registered writable node.");
|
|
1027
|
+
this.succeeded = succeeded;
|
|
1028
|
+
this.failed = failed;
|
|
1029
|
+
this.code = "PARTIAL_NODE_WRITE";
|
|
1030
|
+
this.name = "PartialNodeWriteError";
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
var ROUTING_FAILURE_CODES = {
|
|
1034
|
+
NODE_NOT_RESPONDING: true,
|
|
1035
|
+
PARTIAL_NODE_WRITE: true,
|
|
1036
|
+
NODE_IDENTITY_MISMATCH: true,
|
|
1037
|
+
RESOURCE_NOT_ROUTED: true,
|
|
1038
|
+
DECRYPTION_DEPENDENCY_UNAVAILABLE: true,
|
|
1039
|
+
registry_unavailable: true,
|
|
1040
|
+
node_not_found: true,
|
|
1041
|
+
node_registry_entry_exists: true
|
|
1042
|
+
};
|
|
1043
|
+
function isRoutingFailureCode(code) {
|
|
1044
|
+
return Object.prototype.hasOwnProperty.call(ROUTING_FAILURE_CODES, code);
|
|
1045
|
+
}
|
|
1046
|
+
function routingFailure(error) {
|
|
1047
|
+
if (typeof error === "object" && error !== null && "code" in error && "message" in error && typeof error.code === "string" && typeof error.message === "string" && isRoutingFailureCode(error.code)) {
|
|
1048
|
+
const candidate = "meta" in error ? error.meta : void 0;
|
|
1049
|
+
let meta = typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) ? candidate : void 0;
|
|
1050
|
+
if (error.code === "NODE_NOT_RESPONDING" && "hosts" in error && Array.isArray(error.hosts)) {
|
|
1051
|
+
meta = { ...meta, hosts: error.hosts };
|
|
1052
|
+
} else if (error.code === "PARTIAL_NODE_WRITE" && "succeeded" in error && Array.isArray(error.succeeded) && "failed" in error && Array.isArray(error.failed)) {
|
|
1053
|
+
meta = { ...meta, succeeded: error.succeeded, failed: error.failed };
|
|
1054
|
+
}
|
|
1055
|
+
return { code: error.code, message: error.message, ...error instanceof Error ? { cause: error } : {}, ...meta ? { meta } : {} };
|
|
1056
|
+
}
|
|
1057
|
+
return void 0;
|
|
1058
|
+
}
|
|
1059
|
+
function authorizationOf(headers) {
|
|
1060
|
+
if (headers === void 0) return void 0;
|
|
1061
|
+
return (Array.isArray(headers) ? headers : Object.entries(headers)).find(([name]) => name.toLowerCase() === "authorization")?.[1];
|
|
1062
|
+
}
|
|
1063
|
+
function routingResourceOf(uri) {
|
|
1064
|
+
if (uri.startsWith("tinycloud://")) {
|
|
1065
|
+
const resource = uri.slice("tinycloud://".length).split("/", 1)[0];
|
|
1066
|
+
if (resource) return resource;
|
|
1067
|
+
} else if (uri.startsWith("urn:tinycloud:encryption:")) {
|
|
1068
|
+
return uri;
|
|
1069
|
+
} else if (uri.startsWith("tinycloud:")) {
|
|
1070
|
+
return uri.split("/", 1)[0];
|
|
1071
|
+
}
|
|
1072
|
+
throw new ResourceNotRoutedError(uri);
|
|
1073
|
+
}
|
|
1074
|
+
function invocationResources(authorization) {
|
|
1075
|
+
const encoded = authorization.split(".")[1];
|
|
1076
|
+
if (!encoded) throw new ResourceNotRoutedError("invalid invocation");
|
|
1077
|
+
const payload = JSON.parse(atob(encoded.replace(/-/g, "+").replace(/_/g, "/")));
|
|
1078
|
+
if (typeof payload !== "object" || payload === null || !("att" in payload) || typeof payload.att !== "object" || payload.att === null || Array.isArray(payload.att)) {
|
|
1079
|
+
throw new ResourceNotRoutedError("invalid invocation attenuation");
|
|
1080
|
+
}
|
|
1081
|
+
const resources = [];
|
|
1082
|
+
for (const [uri, abilities] of Object.entries(payload.att)) {
|
|
1083
|
+
if (typeof abilities !== "object" || abilities === null || Array.isArray(abilities)) throw new ResourceNotRoutedError(uri);
|
|
1084
|
+
for (const action of Object.keys(abilities)) {
|
|
1085
|
+
const resource = routingResourceOf(uri);
|
|
1086
|
+
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);
|
|
1087
|
+
const access = resource.startsWith("urn:tinycloud:encryption:") ? "encryption" : action === "tinycloud.space/host" ? "host" : read2 ? "read" : "write";
|
|
1088
|
+
if (!resources.some((entry) => entry.resource === resource && entry.access === access)) resources.push({ resource, access });
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (resources.length === 0) throw new ResourceNotRoutedError("empty invocation");
|
|
1092
|
+
return resources;
|
|
1093
|
+
}
|
|
1094
|
+
async function routeResourceRequest(resources, routing, send, options) {
|
|
1095
|
+
if (resources.length === 0) throw new ResourceNotRoutedError("empty request");
|
|
1096
|
+
const routes = await Promise.all(resources.map(({ resource, access }) => routing.resolveResource(resource, access)));
|
|
1097
|
+
if (routes.some((route) => route.length === 0)) throw new ResourceNotRoutedError(resources[routes.findIndex((route) => route.length === 0)].resource);
|
|
1098
|
+
const write = options?.independentResources === true || resources.some(({ access }) => access === "write" || access === "host");
|
|
1099
|
+
let targets;
|
|
1100
|
+
if (options?.independentResources) {
|
|
1101
|
+
targets = routes.flat();
|
|
1102
|
+
const identities = /* @__PURE__ */ new Map();
|
|
1103
|
+
for (const target of targets) {
|
|
1104
|
+
if (identities.has(target.url) && identities.get(target.url) !== target.nodeDid) throw new ResourceNotRoutedError(target.url);
|
|
1105
|
+
identities.set(target.url, target.nodeDid);
|
|
1106
|
+
}
|
|
1107
|
+
} else {
|
|
1108
|
+
targets = routes[0].filter((target) => routes.every((route) => route.some((other) => other.url === target.url && other.nodeDid === target.nodeDid)));
|
|
1109
|
+
if (targets.length === 0 || write && routes.some((route) => route.length !== targets.length)) throw new ResourceNotRoutedError(resources.map(({ resource }) => resource).join(", "));
|
|
1110
|
+
}
|
|
1111
|
+
targets = targets.filter((target, index) => targets.findIndex((other) => other.url === target.url) === index);
|
|
1112
|
+
if (options?.nodeDid) targets = targets.filter((target) => target.nodeDid === options.nodeDid || target.nodeDid === void 0);
|
|
1113
|
+
if (targets.length === 0 || options?.nodeLocal && targets.length !== 1) throw new ResourceNotRoutedError(resources.map(({ resource }) => resource).join(", "));
|
|
1114
|
+
if (!write) {
|
|
1115
|
+
let response2;
|
|
1116
|
+
let failure;
|
|
1117
|
+
for (const target of targets) {
|
|
1118
|
+
try {
|
|
1119
|
+
response2 = await send(target);
|
|
1120
|
+
if (response2.ok || response2.status !== 404) return response2;
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
if (!(error instanceof NodeNotRespondingError)) throw error;
|
|
1123
|
+
failure = error;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (failure) throw new NodeNotRespondingError(targets.map(({ url }) => url), failure);
|
|
1127
|
+
if (response2) return response2;
|
|
1128
|
+
throw new NodeNotRespondingError(targets.map(({ url }) => url), failure);
|
|
1129
|
+
}
|
|
1130
|
+
const succeeded = [];
|
|
1131
|
+
const failed = [];
|
|
1132
|
+
let response;
|
|
1133
|
+
let failureResponse;
|
|
1134
|
+
for (const target of targets) {
|
|
1135
|
+
try {
|
|
1136
|
+
const result = await send(target);
|
|
1137
|
+
if (result.ok) {
|
|
1138
|
+
succeeded.push(target.url);
|
|
1139
|
+
response ?? (response = result);
|
|
1140
|
+
} else {
|
|
1141
|
+
failed.push({ url: target.url, status: result.status });
|
|
1142
|
+
failureResponse ?? (failureResponse = result);
|
|
1143
|
+
}
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
if (!routingFailure(error) && succeeded.length === 0) throw error;
|
|
1146
|
+
failed.push({ url: target.url, cause: error });
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (failed.length > 0) {
|
|
1150
|
+
if (succeeded.length > 0 || targets.length > 1) throw new PartialNodeWriteError(succeeded, failed);
|
|
1151
|
+
if (failureResponse) return failureResponse;
|
|
1152
|
+
if (failed[0]?.cause && !(failed[0].cause instanceof NodeNotRespondingError)) throw failed[0].cause;
|
|
1153
|
+
throw new NodeNotRespondingError(targets.map(({ url }) => url), failed[0]?.cause);
|
|
1154
|
+
}
|
|
1155
|
+
return response;
|
|
1156
|
+
}
|
|
1157
|
+
function createResourceTransportInvocationFetch(transport, session) {
|
|
1158
|
+
return async (url, init) => {
|
|
1159
|
+
const parsed = new URL(url);
|
|
1160
|
+
if (parsed.pathname !== "/invoke") throw new ResourceNotRoutedError(parsed.pathname);
|
|
1161
|
+
const authorization = authorizationOf(init?.headers);
|
|
1162
|
+
if (!authorization) throw new ResourceNotRoutedError("unsigned invocation");
|
|
1163
|
+
return transport(invocationResources(authorization), async (target) => {
|
|
1164
|
+
const signed = await target.bindAudience(authorization);
|
|
1165
|
+
const headers = (Array.isArray(init?.headers) ? init.headers : Object.entries(init?.headers ?? {})).filter(([name]) => name.toLowerCase() !== "authorization");
|
|
1166
|
+
headers.push(["Authorization", signed]);
|
|
1167
|
+
return target.fetch(`${target.url}${parsed.pathname}${parsed.search}`, { ...init, headers });
|
|
1168
|
+
}, { invocationSession: session });
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
function createResourceRoutingFetch(fetchFn, routing) {
|
|
1172
|
+
return async (url, init) => {
|
|
1173
|
+
const parsed = new URL(url);
|
|
1174
|
+
if (parsed.pathname !== "/invoke") return fetchFn(url, init);
|
|
1175
|
+
const authorization = authorizationOf(init?.headers);
|
|
1176
|
+
if (!authorization) throw new ResourceNotRoutedError("unsigned invocation");
|
|
1177
|
+
const resources = invocationResources(authorization);
|
|
1178
|
+
const send = async (target) => {
|
|
1179
|
+
init?.signal?.throwIfAborted();
|
|
1180
|
+
if (routing.verifyDestination) await routing.verifyDestination(target);
|
|
1181
|
+
else await verifyResourceDestination(target, fetchFn);
|
|
1182
|
+
const signed = target.nodeDid ? await routing.bindAudience(authorization, target.nodeDid) : authorization;
|
|
1183
|
+
const headers = (Array.isArray(init?.headers) ? init.headers : Object.entries(init?.headers ?? {})).filter(([name]) => name.toLowerCase() !== "authorization");
|
|
1184
|
+
headers.push(["Authorization", signed]);
|
|
1185
|
+
const timeout = AbortSignal.timeout(5e3);
|
|
1186
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
1187
|
+
try {
|
|
1188
|
+
return await fetchFn(`${target.url}${parsed.pathname}${parsed.search}`, { ...init, headers, signal, redirect: "error" });
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
if (init?.signal?.aborted) throw error;
|
|
1191
|
+
if (routingFailure(error)) throw error;
|
|
1192
|
+
throw new NodeNotRespondingError([target.url], error);
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
return routeResourceRequest(resources, routing, send);
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
954
1199
|
// src/errors.ts
|
|
955
1200
|
function parsePermissionHint(value) {
|
|
956
1201
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
@@ -1256,6 +1501,8 @@ function isStorageFullError(error) {
|
|
|
1256
1501
|
return error?.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error?.code === ErrorCodes.STORAGE_LIMIT_REACHED;
|
|
1257
1502
|
}
|
|
1258
1503
|
function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
1504
|
+
const routing = routingFailure(error);
|
|
1505
|
+
if (routing) return { ...routing, service };
|
|
1259
1506
|
if (error instanceof Error) {
|
|
1260
1507
|
if (isRequestTimeoutError(error)) {
|
|
1261
1508
|
return timeoutError(service, error.timeoutMs, error);
|
|
@@ -5953,9 +6200,9 @@ function utf8Encode(s) {
|
|
|
5953
6200
|
function utf8Decode(b) {
|
|
5954
6201
|
return new TextDecoder().decode(b);
|
|
5955
6202
|
}
|
|
5956
|
-
function canonicalHashHex(
|
|
6203
|
+
function canonicalHashHex(sha2562, value) {
|
|
5957
6204
|
const canonical = canonicalize(value);
|
|
5958
|
-
return hexEncode(
|
|
6205
|
+
return hexEncode(sha2562(utf8Encode(canonical)));
|
|
5959
6206
|
}
|
|
5960
6207
|
|
|
5961
6208
|
// src/vault/types.ts
|
|
@@ -6197,6 +6444,8 @@ function defaultVaultMessage(input) {
|
|
|
6197
6444
|
case "STORAGE_QUOTA_EXCEEDED":
|
|
6198
6445
|
case "STORAGE_LIMIT_REACHED":
|
|
6199
6446
|
return input.message;
|
|
6447
|
+
default:
|
|
6448
|
+
return input.message ?? input.code;
|
|
6200
6449
|
}
|
|
6201
6450
|
}
|
|
6202
6451
|
function vaultError(input) {
|
|
@@ -6248,6 +6497,8 @@ function ifMatchWriteError(key, error) {
|
|
|
6248
6497
|
});
|
|
6249
6498
|
}
|
|
6250
6499
|
function kvWriteError(context, error) {
|
|
6500
|
+
const routing = routingFailure(error);
|
|
6501
|
+
if (routing) return vaultError(routing);
|
|
6251
6502
|
if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
|
|
6252
6503
|
return vaultError({ code: error.code, message: error.message, meta: error.meta });
|
|
6253
6504
|
}
|
|
@@ -6554,6 +6805,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6554
6805
|
{ aad, metadata }
|
|
6555
6806
|
);
|
|
6556
6807
|
if (!envelopeResult.ok) {
|
|
6808
|
+
const routing = routingFailure(envelopeResult.error);
|
|
6809
|
+
if (routing) return vaultError(routing);
|
|
6557
6810
|
return vaultError({
|
|
6558
6811
|
code: "STORAGE_ERROR",
|
|
6559
6812
|
cause: new Error(envelopeResult.error.message)
|
|
@@ -6580,6 +6833,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6580
6833
|
}
|
|
6581
6834
|
return { ok: true, data: void 0 };
|
|
6582
6835
|
} catch (error) {
|
|
6836
|
+
const routing = routingFailure(error);
|
|
6837
|
+
if (routing) return vaultError(routing);
|
|
6583
6838
|
return vaultError({
|
|
6584
6839
|
code: "STORAGE_ERROR",
|
|
6585
6840
|
cause: toError(error)
|
|
@@ -6606,6 +6861,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6606
6861
|
...options?.source === void 0 ? {} : { source: options.source }
|
|
6607
6862
|
});
|
|
6608
6863
|
if (!valueResult.ok) {
|
|
6864
|
+
const routing = routingFailure(valueResult.error);
|
|
6865
|
+
if (routing) return vaultError(routing);
|
|
6609
6866
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6610
6867
|
}
|
|
6611
6868
|
const rawEnvelope = unwrapKVData(valueResult.data);
|
|
@@ -6626,6 +6883,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6626
6883
|
const proof = await this.decryptCapabilityProof();
|
|
6627
6884
|
const plaintextResult = await config.service.decryptEnvelope(envelope, proof);
|
|
6628
6885
|
if (!plaintextResult.ok) {
|
|
6886
|
+
const routing = routingFailure(plaintextResult.error);
|
|
6887
|
+
if (routing) return vaultError(routing);
|
|
6629
6888
|
return vaultError({
|
|
6630
6889
|
code: "DECRYPTION_FAILED",
|
|
6631
6890
|
message: plaintextResult.error.message
|
|
@@ -6645,6 +6904,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6645
6904
|
data: { value, metadata, keyId, ...etag === void 0 ? {} : { etag } }
|
|
6646
6905
|
};
|
|
6647
6906
|
} catch (error) {
|
|
6907
|
+
const routing = routingFailure(error);
|
|
6908
|
+
if (routing) return vaultError(routing);
|
|
6648
6909
|
return vaultError({
|
|
6649
6910
|
code: "STORAGE_ERROR",
|
|
6650
6911
|
cause: toError(error)
|
|
@@ -6657,6 +6918,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6657
6918
|
* Unlike {@link getNetworkEncrypted}, this is intentionally message-free so
|
|
6658
6919
|
* an operation layer can distinguish an authorized absence from a failed
|
|
6659
6920
|
* read without receiving node, envelope, plaintext, or key material.
|
|
6921
|
+
* Typed routing failures reject unchanged rather than becoming an ordinary
|
|
6922
|
+
* read/decrypt failure; the operation boundary projects their safe code.
|
|
6660
6923
|
*/
|
|
6661
6924
|
async readNetworkEncrypted(key, options) {
|
|
6662
6925
|
const config = this.networkEncryption;
|
|
@@ -6666,10 +6929,12 @@ var DataVaultService = class extends BaseService {
|
|
|
6666
6929
|
let valueResult;
|
|
6667
6930
|
try {
|
|
6668
6931
|
valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
|
|
6669
|
-
} catch {
|
|
6932
|
+
} catch (error) {
|
|
6933
|
+
if (routingFailure(error)) throw error;
|
|
6670
6934
|
return { status: "node_unreachable" };
|
|
6671
6935
|
}
|
|
6672
6936
|
if (!valueResult.ok) {
|
|
6937
|
+
if (routingFailure(valueResult.error)) throw valueResult.error;
|
|
6673
6938
|
const permissionHint = parsePermissionHint(valueResult.error.meta?.permissionHint);
|
|
6674
6939
|
if (valueResult.error.code === "AUTH_UNAUTHORIZED" && permissionHint !== void 0) {
|
|
6675
6940
|
return { status: "permission_required", hint: permissionHint };
|
|
@@ -6677,7 +6942,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6677
6942
|
if ((valueResult.error.code === "NOT_FOUND" || valueResult.error.code === "KV_NOT_FOUND") && !hasHttpResponse(valueResult.error)) {
|
|
6678
6943
|
return { status: "not_found" };
|
|
6679
6944
|
}
|
|
6680
|
-
if (valueResult.error.code === "NETWORK_ERROR" || valueResult.error.code === "TIMEOUT" || valueResult.error.code === "ABORTED") {
|
|
6945
|
+
if (valueResult.error.code === "NODE_NOT_RESPONDING" || valueResult.error.code === "NETWORK_ERROR" || valueResult.error.code === "TIMEOUT" || valueResult.error.code === "ABORTED") {
|
|
6681
6946
|
return hasHttpResponse(valueResult.error) ? { status: "read_failed" } : { status: "node_unreachable" };
|
|
6682
6947
|
}
|
|
6683
6948
|
return { status: "read_failed" };
|
|
@@ -6699,10 +6964,12 @@ var DataVaultService = class extends BaseService {
|
|
|
6699
6964
|
try {
|
|
6700
6965
|
const proof = await this.decryptCapabilityProof();
|
|
6701
6966
|
plaintextResult = await config.service.decryptEnvelope(envelope, proof);
|
|
6702
|
-
} catch {
|
|
6967
|
+
} catch (error) {
|
|
6968
|
+
if (routingFailure(error)) throw error;
|
|
6703
6969
|
return { status: "decrypt_failed" };
|
|
6704
6970
|
}
|
|
6705
6971
|
if (!plaintextResult.ok) {
|
|
6972
|
+
if (routingFailure(plaintextResult.error)) throw plaintextResult.error;
|
|
6706
6973
|
const permissionHint = parsePermissionHint(
|
|
6707
6974
|
plaintextResult.error.permissionHint
|
|
6708
6975
|
);
|
|
@@ -6712,7 +6979,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6712
6979
|
if (plaintextResult.error.code === "INVALID_ENVELOPE") {
|
|
6713
6980
|
return { status: "corrupt_envelope" };
|
|
6714
6981
|
}
|
|
6715
|
-
if (plaintextResult.error.code === "TRANSPORT_ERROR") {
|
|
6982
|
+
if (plaintextResult.error.code === "NODE_NOT_RESPONDING" || plaintextResult.error.code === "TRANSPORT_ERROR") {
|
|
6716
6983
|
return { status: "node_unreachable" };
|
|
6717
6984
|
}
|
|
6718
6985
|
return { status: "decrypt_failed" };
|
|
@@ -6964,6 +7231,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6964
7231
|
if (this.usesNetworkEncryption) {
|
|
6965
7232
|
const valueDelResult2 = await this.tc.kv.delete(`vault/${key}`);
|
|
6966
7233
|
if (!valueDelResult2.ok) {
|
|
7234
|
+
const routing = routingFailure(valueDelResult2.error);
|
|
7235
|
+
if (routing) return vaultError(routing);
|
|
6967
7236
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6968
7237
|
}
|
|
6969
7238
|
return ok(void 0);
|
|
@@ -6973,10 +7242,14 @@ var DataVaultService = class extends BaseService {
|
|
|
6973
7242
|
this.tc.kv.delete(`vault/${key}`)
|
|
6974
7243
|
]);
|
|
6975
7244
|
if (!keyDelResult.ok && !valueDelResult.ok) {
|
|
7245
|
+
const routing = routingFailure(keyDelResult.error) ?? routingFailure(valueDelResult.error);
|
|
7246
|
+
if (routing) return vaultError(routing);
|
|
6976
7247
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6977
7248
|
}
|
|
6978
7249
|
return ok(void 0);
|
|
6979
7250
|
} catch (error) {
|
|
7251
|
+
const routing = routingFailure(error);
|
|
7252
|
+
if (routing) return vaultError(routing);
|
|
6980
7253
|
return vaultError({
|
|
6981
7254
|
code: "STORAGE_ERROR",
|
|
6982
7255
|
cause: toError(error)
|
|
@@ -7018,6 +7291,8 @@ var DataVaultService = class extends BaseService {
|
|
|
7018
7291
|
...options?.cursor === void 0 ? {} : { cursor: options.cursor }
|
|
7019
7292
|
});
|
|
7020
7293
|
if (!listResult.ok) {
|
|
7294
|
+
const routing = routingFailure(listResult.error);
|
|
7295
|
+
if (routing) return vaultError(routing);
|
|
7021
7296
|
return vaultError({
|
|
7022
7297
|
code: "STORAGE_ERROR",
|
|
7023
7298
|
cause: new Error(
|
|
@@ -7038,6 +7313,8 @@ var DataVaultService = class extends BaseService {
|
|
|
7038
7313
|
...listResult.data.nextCursor === void 0 ? {} : { nextCursor: listResult.data.nextCursor }
|
|
7039
7314
|
});
|
|
7040
7315
|
} catch (error) {
|
|
7316
|
+
const routing = routingFailure(error);
|
|
7317
|
+
if (routing) return vaultError(routing);
|
|
7041
7318
|
return vaultError({
|
|
7042
7319
|
code: "STORAGE_ERROR",
|
|
7043
7320
|
cause: toError(error)
|
|
@@ -7869,6 +8146,8 @@ function defaultEncryptionMessage(input) {
|
|
|
7869
8146
|
return input.message ?? input.cause.message;
|
|
7870
8147
|
case "INVALID_INPUT":
|
|
7871
8148
|
return input.message;
|
|
8149
|
+
default:
|
|
8150
|
+
return input.message ?? input.code;
|
|
7872
8151
|
}
|
|
7873
8152
|
}
|
|
7874
8153
|
function encryptionError(input) {
|
|
@@ -7933,6 +8212,8 @@ async function discoverNetwork(input) {
|
|
|
7933
8212
|
return { ok: true, data: { descriptor: validated.data, source: "node" } };
|
|
7934
8213
|
}
|
|
7935
8214
|
} catch (err3) {
|
|
8215
|
+
const routing = routingFailure(err3);
|
|
8216
|
+
if (routing) return { ok: false, error: encryptionError(routing) };
|
|
7936
8217
|
}
|
|
7937
8218
|
}
|
|
7938
8219
|
if (input.wellKnown !== void 0) {
|
|
@@ -7950,6 +8231,8 @@ async function discoverNetwork(input) {
|
|
|
7950
8231
|
};
|
|
7951
8232
|
}
|
|
7952
8233
|
} catch (err3) {
|
|
8234
|
+
const routing = routingFailure(err3);
|
|
8235
|
+
if (routing) return { ok: false, error: encryptionError(routing) };
|
|
7953
8236
|
}
|
|
7954
8237
|
}
|
|
7955
8238
|
return {
|
|
@@ -8039,6 +8322,152 @@ function ensureNetworkUsableForDecrypt(descriptor) {
|
|
|
8039
8322
|
};
|
|
8040
8323
|
}
|
|
8041
8324
|
|
|
8325
|
+
// src/encryption/keyWrap.ts
|
|
8326
|
+
var import_aes = require("@noble/ciphers/aes");
|
|
8327
|
+
var import_ed25519 = require("@noble/curves/ed25519");
|
|
8328
|
+
var import_hkdf = require("@noble/hashes/hkdf");
|
|
8329
|
+
var import_sha256 = require("@noble/hashes/sha256");
|
|
8330
|
+
var import_utils = require("@noble/hashes/utils");
|
|
8331
|
+
var KEY_WRAP_V2_ALG = "x25519-hkdf-aes256gcm/v2";
|
|
8332
|
+
var MAGIC = utf8Encode("TCW2");
|
|
8333
|
+
var INFO = utf8Encode("tinycloud.encryption.key-wrap/v2");
|
|
8334
|
+
var MAX_HEADER = 65536;
|
|
8335
|
+
function concat(...parts) {
|
|
8336
|
+
const result = new Uint8Array(parts.reduce((length, part) => length + part.length, 0));
|
|
8337
|
+
let offset = 0;
|
|
8338
|
+
for (const part of parts) {
|
|
8339
|
+
result.set(part, offset);
|
|
8340
|
+
offset += part.length;
|
|
8341
|
+
}
|
|
8342
|
+
return result;
|
|
8343
|
+
}
|
|
8344
|
+
function equal(a, b) {
|
|
8345
|
+
if (a.length !== b.length) return false;
|
|
8346
|
+
let difference = 0;
|
|
8347
|
+
for (let i = 0; i < a.length; i++) difference |= a[i] ^ b[i];
|
|
8348
|
+
return difference === 0;
|
|
8349
|
+
}
|
|
8350
|
+
function decode32(value) {
|
|
8351
|
+
if (typeof value !== "string") throw new Error("Invalid key wrap encoding");
|
|
8352
|
+
const decoded = base64Decode(value);
|
|
8353
|
+
if (decoded.length !== 32 || base64Encode(decoded) !== value) throw new Error("Invalid key wrap encoding");
|
|
8354
|
+
return decoded;
|
|
8355
|
+
}
|
|
8356
|
+
function isWellFormed(value) {
|
|
8357
|
+
for (let i = 0; i < value.length; i++) {
|
|
8358
|
+
const unit = value.charCodeAt(i);
|
|
8359
|
+
if (unit >= 55296 && unit <= 56319) {
|
|
8360
|
+
const next = value.charCodeAt(++i);
|
|
8361
|
+
if (!(next >= 56320 && next <= 57343)) return false;
|
|
8362
|
+
} else if (unit >= 56320 && unit <= 57343) return false;
|
|
8363
|
+
}
|
|
8364
|
+
return true;
|
|
8365
|
+
}
|
|
8366
|
+
function validateContext(context) {
|
|
8367
|
+
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))) {
|
|
8368
|
+
throw new Error("Invalid key wrap context");
|
|
8369
|
+
}
|
|
8370
|
+
if (context.networkId !== "") {
|
|
8371
|
+
const resource = parseEncryptionResource(context.keyPath === "" ? context.networkId : `${context.networkId}/${context.keyPath}`);
|
|
8372
|
+
if (resource.networkId !== context.networkId || (resource.keyPath ?? "") !== context.keyPath || context.keyPath.endsWith("/")) {
|
|
8373
|
+
throw new Error("Invalid key wrap resource");
|
|
8374
|
+
}
|
|
8375
|
+
}
|
|
8376
|
+
}
|
|
8377
|
+
function isKeyWrapV2(wrapped) {
|
|
8378
|
+
return wrapped.length >= 4 && MAGIC.every((byte, index) => wrapped[index] === byte);
|
|
8379
|
+
}
|
|
8380
|
+
function readKeyWrapV2(wrapped) {
|
|
8381
|
+
if (!isKeyWrapV2(wrapped) || wrapped.length < 8) throw new Error("Invalid v2 key wrap frame");
|
|
8382
|
+
const length = new DataView(wrapped.buffer, wrapped.byteOffset, wrapped.byteLength).getUint32(4);
|
|
8383
|
+
if (length === 0 || length > MAX_HEADER || wrapped.length < 8 + length + 12 + 16) throw new Error("Invalid v2 key wrap length");
|
|
8384
|
+
const bytes = wrapped.subarray(8, 8 + length);
|
|
8385
|
+
const header = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
8386
|
+
const fields = ["v", "alg", "kdf", "aead", "salt", "ephemeralPublicKey", "recipientPublicKey", "purpose", "networkId", "keyPath", "keyVersion", "metadata"].sort();
|
|
8387
|
+
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") {
|
|
8388
|
+
throw new Error("Invalid v2 key wrap header");
|
|
8389
|
+
}
|
|
8390
|
+
validateContext(header);
|
|
8391
|
+
decode32(header.salt);
|
|
8392
|
+
decode32(header.ephemeralPublicKey);
|
|
8393
|
+
decode32(header.recipientPublicKey);
|
|
8394
|
+
if (!equal(bytes, utf8Encode(canonicalize({ ...header })))) throw new Error("Non-canonical v2 key wrap header");
|
|
8395
|
+
const end = 8 + length;
|
|
8396
|
+
return { header, aad: wrapped.subarray(0, end), nonce: wrapped.subarray(end, end + 12), ciphertext: wrapped.subarray(end + 12) };
|
|
8397
|
+
}
|
|
8398
|
+
function assertKeyWrapContext(header, expected) {
|
|
8399
|
+
validateContext(expected);
|
|
8400
|
+
for (const field of ["purpose", "networkId", "keyPath", "keyVersion"]) {
|
|
8401
|
+
if (header[field] !== expected[field]) throw new Error("Key wrap context mismatch");
|
|
8402
|
+
}
|
|
8403
|
+
if (canonicalize(header.metadata) !== canonicalize(expected.metadata)) throw new Error("Key wrap metadata mismatch");
|
|
8404
|
+
}
|
|
8405
|
+
function deriveKey(secret, ephemeral, recipient, salt) {
|
|
8406
|
+
const shared = import_ed25519.x25519.getSharedSecret(secret, ephemeral);
|
|
8407
|
+
if (shared.every((byte) => byte === 0)) throw new Error("Invalid key agreement");
|
|
8408
|
+
try {
|
|
8409
|
+
return (0, import_hkdf.hkdf)(import_sha256.sha256, shared, salt, concat(INFO, ephemeral, recipient), 32);
|
|
8410
|
+
} finally {
|
|
8411
|
+
shared.fill(0);
|
|
8412
|
+
}
|
|
8413
|
+
}
|
|
8414
|
+
function sealKeyV2(recipientPublicKey, plaintext, context) {
|
|
8415
|
+
validateContext(context);
|
|
8416
|
+
if (recipientPublicKey.length !== 32) throw new Error("Invalid recipient public key");
|
|
8417
|
+
const secret = (0, import_utils.randomBytes)(32);
|
|
8418
|
+
const ephemeral = import_ed25519.x25519.getPublicKey(secret);
|
|
8419
|
+
const salt = (0, import_utils.randomBytes)(32);
|
|
8420
|
+
const header = {
|
|
8421
|
+
...context,
|
|
8422
|
+
v: 2,
|
|
8423
|
+
alg: KEY_WRAP_V2_ALG,
|
|
8424
|
+
kdf: "hkdf-sha256",
|
|
8425
|
+
aead: "aes-256-gcm",
|
|
8426
|
+
salt: base64Encode(salt),
|
|
8427
|
+
ephemeralPublicKey: base64Encode(ephemeral),
|
|
8428
|
+
recipientPublicKey: base64Encode(recipientPublicKey)
|
|
8429
|
+
};
|
|
8430
|
+
const bytes = utf8Encode(canonicalize({ ...header }));
|
|
8431
|
+
if (bytes.length > MAX_HEADER) throw new Error("Key wrap header too large");
|
|
8432
|
+
const prefix = new Uint8Array(8);
|
|
8433
|
+
prefix.set(MAGIC);
|
|
8434
|
+
new DataView(prefix.buffer).setUint32(4, bytes.length);
|
|
8435
|
+
const aad = concat(prefix, bytes);
|
|
8436
|
+
const nonce = (0, import_utils.randomBytes)(12);
|
|
8437
|
+
let key;
|
|
8438
|
+
try {
|
|
8439
|
+
const shared = import_ed25519.x25519.getSharedSecret(secret, recipientPublicKey);
|
|
8440
|
+
try {
|
|
8441
|
+
if (shared.every((byte) => byte === 0)) throw new Error("Invalid key agreement");
|
|
8442
|
+
key = (0, import_hkdf.hkdf)(import_sha256.sha256, shared, salt, concat(INFO, ephemeral, recipientPublicKey), 32);
|
|
8443
|
+
} finally {
|
|
8444
|
+
shared.fill(0);
|
|
8445
|
+
}
|
|
8446
|
+
return concat(aad, nonce, (0, import_aes.gcm)(key, nonce, aad).encrypt(plaintext));
|
|
8447
|
+
} finally {
|
|
8448
|
+
secret.fill(0);
|
|
8449
|
+
key?.fill(0);
|
|
8450
|
+
}
|
|
8451
|
+
}
|
|
8452
|
+
function openKeyV2(recipientPrivateKey, wrapped, expected) {
|
|
8453
|
+
const { header, aad, nonce, ciphertext } = readKeyWrapV2(wrapped);
|
|
8454
|
+
assertKeyWrapContext(header, expected);
|
|
8455
|
+
const recipient = import_ed25519.x25519.getPublicKey(recipientPrivateKey);
|
|
8456
|
+
if (!equal(recipient, decode32(header.recipientPublicKey))) throw new Error("Key wrap recipient mismatch");
|
|
8457
|
+
const key = deriveKey(recipientPrivateKey, decode32(header.ephemeralPublicKey), recipient, decode32(header.salt));
|
|
8458
|
+
try {
|
|
8459
|
+
return (0, import_aes.gcm)(key, nonce, aad).decrypt(ciphertext);
|
|
8460
|
+
} finally {
|
|
8461
|
+
key.fill(0);
|
|
8462
|
+
}
|
|
8463
|
+
}
|
|
8464
|
+
function assertKeyWrapAlgorithm(wrapped, alg, requireV2 = false) {
|
|
8465
|
+
if (alg !== "x25519-aes256gcm/v1" && alg !== KEY_WRAP_V2_ALG) throw new Error("Unsupported key wrap algorithm");
|
|
8466
|
+
if (requireV2 && alg !== KEY_WRAP_V2_ALG) throw new Error("V2 network key wrap required");
|
|
8467
|
+
if (isKeyWrapV2(wrapped) !== (alg === KEY_WRAP_V2_ALG)) throw new Error("Key wrap algorithm mismatch");
|
|
8468
|
+
if (alg === KEY_WRAP_V2_ALG) readKeyWrapV2(wrapped);
|
|
8469
|
+
}
|
|
8470
|
+
|
|
8042
8471
|
// src/encryption/envelope.ts
|
|
8043
8472
|
function encryptToNetwork(crypto2, input) {
|
|
8044
8473
|
const parsedResource = parseEncryptionResource(
|
|
@@ -8047,11 +8476,20 @@ function encryptToNetwork(crypto2, input) {
|
|
|
8047
8476
|
if (parsedResource.keyPath !== input.keyPath || input.keyPath?.endsWith("/")) {
|
|
8048
8477
|
throw new NetworkIdError("keyPath must name an exact encryption key");
|
|
8049
8478
|
}
|
|
8050
|
-
const alg = input.
|
|
8479
|
+
const alg = input.keyWrapV2 ? KEY_WRAP_V2_ALG : DEFAULT_ENCRYPTION_ALG;
|
|
8480
|
+
if (input.alg !== void 0 && input.alg !== alg) {
|
|
8481
|
+
throw new Error("Algorithm does not match keyWrapV2");
|
|
8482
|
+
}
|
|
8051
8483
|
const keyVersion = input.keyVersion ?? DEFAULT_KEY_VERSION;
|
|
8052
8484
|
const symmetricKey = crypto2.randomBytes(32);
|
|
8053
8485
|
const ciphertext = crypto2.authEncrypt(symmetricKey, input.plaintext, input.aad);
|
|
8054
|
-
const wrapped =
|
|
8486
|
+
const wrapped = input.keyWrapV2 ? sealKeyV2(input.networkPublicKey, symmetricKey, {
|
|
8487
|
+
purpose: "network-key",
|
|
8488
|
+
networkId: input.networkId,
|
|
8489
|
+
keyPath: input.keyPath ?? "",
|
|
8490
|
+
keyVersion,
|
|
8491
|
+
metadata: input.metadata ?? {}
|
|
8492
|
+
}) : crypto2.sealToNetworkKey(input.networkPublicKey, symmetricKey);
|
|
8055
8493
|
const encryptedSymmetricKey = base64Encode(wrapped);
|
|
8056
8494
|
const encryptedSymmetricKeyHash = canonicalHashHex(
|
|
8057
8495
|
crypto2.sha256,
|
|
@@ -8071,7 +8509,7 @@ function encryptToNetwork(crypto2, input) {
|
|
|
8071
8509
|
};
|
|
8072
8510
|
return { envelope, symmetricKey };
|
|
8073
8511
|
}
|
|
8074
|
-
function validateEnvelope(crypto2, envelope) {
|
|
8512
|
+
function validateEnvelope(crypto2, envelope, requireKeyWrapV2 = false) {
|
|
8075
8513
|
if (envelope === null || typeof envelope !== "object") {
|
|
8076
8514
|
return {
|
|
8077
8515
|
ok: false,
|
|
@@ -8148,9 +8586,28 @@ function validateEnvelope(crypto2, envelope) {
|
|
|
8148
8586
|
})
|
|
8149
8587
|
};
|
|
8150
8588
|
}
|
|
8589
|
+
try {
|
|
8590
|
+
validateEnvelopeKeyWrap(e, requireKeyWrapV2);
|
|
8591
|
+
} catch {
|
|
8592
|
+
return { ok: false, error: encryptionError({ code: "INVALID_ENVELOPE", message: "Envelope key wrap context is invalid" }) };
|
|
8593
|
+
}
|
|
8151
8594
|
return { ok: true, data: e };
|
|
8152
8595
|
}
|
|
8596
|
+
function validateEnvelopeKeyWrap(envelope, requireKeyWrapV2 = false) {
|
|
8597
|
+
const wrapped = base64Decode(envelope.encryptedSymmetricKey);
|
|
8598
|
+
assertKeyWrapAlgorithm(wrapped, envelope.alg, requireKeyWrapV2);
|
|
8599
|
+
if (envelope.alg === KEY_WRAP_V2_ALG) {
|
|
8600
|
+
assertKeyWrapContext(readKeyWrapV2(wrapped).header, {
|
|
8601
|
+
purpose: "network-key",
|
|
8602
|
+
networkId: envelope.networkId,
|
|
8603
|
+
keyPath: envelope.keyPath ?? "",
|
|
8604
|
+
keyVersion: envelope.keyVersion,
|
|
8605
|
+
metadata: envelope.metadata ?? {}
|
|
8606
|
+
});
|
|
8607
|
+
}
|
|
8608
|
+
}
|
|
8153
8609
|
function decryptEnvelopeWithKey(crypto2, envelope, symmetricKey) {
|
|
8610
|
+
validateEnvelopeKeyWrap(envelope);
|
|
8154
8611
|
const ciphertext = base64Decode(envelope.ciphertext);
|
|
8155
8612
|
const aad = envelope.aad !== void 0 ? base64Decode(envelope.aad) : void 0;
|
|
8156
8613
|
return crypto2.authDecrypt(symmetricKey, ciphertext, aad);
|
|
@@ -8440,7 +8897,7 @@ function verifyDecryptResponse(input) {
|
|
|
8440
8897
|
})
|
|
8441
8898
|
};
|
|
8442
8899
|
}
|
|
8443
|
-
if (response.alg !==
|
|
8900
|
+
if (response.alg !== "x25519-aes256gcm/v1" && response.alg !== KEY_WRAP_V2_ALG) {
|
|
8444
8901
|
return {
|
|
8445
8902
|
ok: false,
|
|
8446
8903
|
error: encryptionError({
|
|
@@ -8497,7 +8954,7 @@ function verifyDecryptResponse(input) {
|
|
|
8497
8954
|
})
|
|
8498
8955
|
};
|
|
8499
8956
|
}
|
|
8500
|
-
if (facts.encryptedSymmetricKeyHash !== response.encryptedSymmetricKeyHash || facts.receiverPublicKeyHash !== response.receiverPublicKeyHash || facts.networkId !== response.networkId || facts.targetNode !== response.targetNode || facts.alg !==
|
|
8957
|
+
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) {
|
|
8501
8958
|
return {
|
|
8502
8959
|
ok: false,
|
|
8503
8960
|
error: encryptionError({
|
|
@@ -8520,8 +8977,13 @@ function verifyDecryptResponse(input) {
|
|
|
8520
8977
|
}
|
|
8521
8978
|
return { ok: true, data: response };
|
|
8522
8979
|
}
|
|
8523
|
-
function openWrappedKey(crypto2, receiverPrivateKey, response) {
|
|
8980
|
+
function openWrappedKey(crypto2, receiverPrivateKey, response, expected) {
|
|
8524
8981
|
const wrapped = base64Decode(response.wrappedKey);
|
|
8982
|
+
assertKeyWrapAlgorithm(wrapped, response.alg);
|
|
8983
|
+
if (response.alg === KEY_WRAP_V2_ALG) {
|
|
8984
|
+
if (!expected) throw new Error("V2 response requires expected key context");
|
|
8985
|
+
return openKeyV2(receiverPrivateKey, wrapped, expected);
|
|
8986
|
+
}
|
|
8525
8987
|
return crypto2.openWithReceiverKey(receiverPrivateKey, wrapped);
|
|
8526
8988
|
}
|
|
8527
8989
|
|
|
@@ -8534,6 +8996,14 @@ function encOk(data) {
|
|
|
8534
8996
|
function encErr(error) {
|
|
8535
8997
|
return { ok: false, error };
|
|
8536
8998
|
}
|
|
8999
|
+
function decryptionError(input) {
|
|
9000
|
+
const unavailable = input.code === "NODE_NOT_RESPONDING" || input.code === "TRANSPORT_ERROR" && input.cause.name !== "AbortError";
|
|
9001
|
+
return encryptionError(unavailable ? {
|
|
9002
|
+
...input,
|
|
9003
|
+
code: "DECRYPTION_DEPENDENCY_UNAVAILABLE",
|
|
9004
|
+
message: "The decryption service isn't responding right now. Please try again."
|
|
9005
|
+
} : input);
|
|
9006
|
+
}
|
|
8537
9007
|
var EncryptionService = class extends BaseService {
|
|
8538
9008
|
constructor(config) {
|
|
8539
9009
|
super();
|
|
@@ -8575,12 +9045,15 @@ var EncryptionService = class extends BaseService {
|
|
|
8575
9045
|
networkPublicKey,
|
|
8576
9046
|
plaintext,
|
|
8577
9047
|
...options?.aad !== void 0 ? { aad: options.aad } : {},
|
|
8578
|
-
|
|
9048
|
+
keyWrapV2: options?.keyWrapV2 ?? this._config.keyWrapV2 ?? false,
|
|
9049
|
+
...options?.alg !== void 0 ? { alg: options.alg } : {},
|
|
8579
9050
|
keyVersion: options?.keyVersion ?? descriptor.keyVersion,
|
|
8580
9051
|
...options?.metadata !== void 0 ? { metadata: options.metadata } : {}
|
|
8581
9052
|
});
|
|
8582
9053
|
return encOk(result.envelope);
|
|
8583
9054
|
} catch (error) {
|
|
9055
|
+
const routing = routingFailure(error);
|
|
9056
|
+
if (routing) return encErr(encryptionError(routing));
|
|
8584
9057
|
return encErr(
|
|
8585
9058
|
encryptionError({
|
|
8586
9059
|
code: "TRANSPORT_ERROR",
|
|
@@ -8592,7 +9065,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8592
9065
|
async decryptEnvelope(envelope, capabilityProof, options) {
|
|
8593
9066
|
try {
|
|
8594
9067
|
this.assertActive();
|
|
8595
|
-
const validated = validateEnvelope(this.crypto, envelope);
|
|
9068
|
+
const validated = validateEnvelope(this.crypto, envelope, this._config.requireKeyWrapV2 ?? false);
|
|
8596
9069
|
if (!validated.ok) return validated;
|
|
8597
9070
|
if (options?.aad !== void 0 && validated.data.aad !== base64Encode(options.aad)) {
|
|
8598
9071
|
return encErr(
|
|
@@ -8607,12 +9080,12 @@ var EncryptionService = class extends BaseService {
|
|
|
8607
9080
|
descriptor = options.descriptor;
|
|
8608
9081
|
} else {
|
|
8609
9082
|
const discovered = await this.discoverNetwork(envelope.networkId);
|
|
8610
|
-
if (!discovered.ok) return discovered;
|
|
9083
|
+
if (!discovered.ok) return encErr(decryptionError(discovered.error));
|
|
8611
9084
|
descriptor = discovered.data;
|
|
8612
9085
|
}
|
|
8613
9086
|
const usable = ensureNetworkUsableForDecrypt(descriptor);
|
|
8614
9087
|
if (!usable.ok) return usable;
|
|
8615
|
-
const targetNode = options?.targetNode ?? descriptor.members[0]?.nodeId;
|
|
9088
|
+
const targetNode = this._config.resolveTargetNode ? await this._config.resolveTargetNode(envelope.networkId) : options?.targetNode ?? descriptor.members[0]?.nodeId;
|
|
8616
9089
|
if (targetNode === void 0) {
|
|
8617
9090
|
return encErr(
|
|
8618
9091
|
encryptionError({
|
|
@@ -8621,6 +9094,9 @@ var EncryptionService = class extends BaseService {
|
|
|
8621
9094
|
})
|
|
8622
9095
|
);
|
|
8623
9096
|
}
|
|
9097
|
+
if (this._config.resolveTargetNode && (options?.targetNode !== void 0 && options.targetNode !== targetNode || !descriptor.members.some((member) => member.nodeId === targetNode))) {
|
|
9098
|
+
return encErr(encryptionError({ code: "INVALID_INPUT", message: "Encryption destination does not match the registry" }));
|
|
9099
|
+
}
|
|
8624
9100
|
const receiverKey = generateRandomReceiverKey({ crypto: this.crypto });
|
|
8625
9101
|
const receiverPublicKey = base64Encode(receiverKey.publicKey);
|
|
8626
9102
|
const receiverPublicKeyHash = canonicalHashHex(
|
|
@@ -8679,6 +9155,8 @@ var EncryptionService = class extends BaseService {
|
|
|
8679
9155
|
});
|
|
8680
9156
|
this.assertActive();
|
|
8681
9157
|
} catch (error) {
|
|
9158
|
+
const routing = routingFailure(error);
|
|
9159
|
+
if (routing) return encErr(decryptionError(routing));
|
|
8682
9160
|
if (error instanceof import_decrypt_transport_response_error.DecryptTransportResponseError) {
|
|
8683
9161
|
return encErr(
|
|
8684
9162
|
encryptionError(
|
|
@@ -8695,7 +9173,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8695
9173
|
);
|
|
8696
9174
|
}
|
|
8697
9175
|
return encErr(
|
|
8698
|
-
|
|
9176
|
+
decryptionError({
|
|
8699
9177
|
code: "TRANSPORT_ERROR",
|
|
8700
9178
|
cause: toError2(error)
|
|
8701
9179
|
})
|
|
@@ -8713,7 +9191,8 @@ var EncryptionService = class extends BaseService {
|
|
|
8713
9191
|
const symmetricKey = openWrappedKey(
|
|
8714
9192
|
this.crypto,
|
|
8715
9193
|
receiverKey.privateKey,
|
|
8716
|
-
verified.data
|
|
9194
|
+
verified.data,
|
|
9195
|
+
{ purpose: "receiver-key", networkId: body.networkId, keyPath: body.keyPath ?? "", keyVersion: body.keyVersion, metadata: {} }
|
|
8717
9196
|
);
|
|
8718
9197
|
const plaintext = decryptEnvelopeWithKey(
|
|
8719
9198
|
this.crypto,
|
|
@@ -8722,6 +9201,8 @@ var EncryptionService = class extends BaseService {
|
|
|
8722
9201
|
);
|
|
8723
9202
|
return encOk(plaintext);
|
|
8724
9203
|
} catch (error) {
|
|
9204
|
+
const routing = routingFailure(error);
|
|
9205
|
+
if (routing) return encErr(decryptionError(routing));
|
|
8725
9206
|
return encErr(
|
|
8726
9207
|
encryptionError({
|
|
8727
9208
|
code: "INVALID_RESPONSE",
|
|
@@ -8771,7 +9252,11 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
|
|
|
8771
9252
|
MAX_SECRET_SCOPE_LENGTH,
|
|
8772
9253
|
NETWORK_NAME_PATTERN,
|
|
8773
9254
|
NetworkIdError,
|
|
9255
|
+
NodeIdentityMismatchError,
|
|
9256
|
+
NodeNotRespondingError,
|
|
9257
|
+
PartialNodeWriteError,
|
|
8774
9258
|
PrefixedKVService,
|
|
9259
|
+
ResourceNotRoutedError,
|
|
8775
9260
|
RetryPolicySchema,
|
|
8776
9261
|
SECRET_NAME_RE,
|
|
8777
9262
|
SECRET_SCOPE_RE,
|
|
@@ -8819,6 +9304,8 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
|
|
|
8819
9304
|
checkDecryptInvocationInput,
|
|
8820
9305
|
clearTinyCloudDebugLogs,
|
|
8821
9306
|
createKVResponseSchema,
|
|
9307
|
+
createResourceRoutingFetch,
|
|
9308
|
+
createResourceTransportInvocationFetch,
|
|
8822
9309
|
createResultSchema,
|
|
8823
9310
|
createVaultCrypto,
|
|
8824
9311
|
decryptEnvelopeWithKey,
|
|
@@ -8861,6 +9348,9 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
|
|
|
8861
9348
|
requiresSecretsOptIn,
|
|
8862
9349
|
resolveSecretListPrefix,
|
|
8863
9350
|
resolveSecretPath,
|
|
9351
|
+
routeResourceRequest,
|
|
9352
|
+
routingFailure,
|
|
9353
|
+
routingResourceOf,
|
|
8864
9354
|
serviceError,
|
|
8865
9355
|
storageLimitReachedError,
|
|
8866
9356
|
storageQuotaExceededError,
|
|
@@ -8881,6 +9371,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
|
|
|
8881
9371
|
validateTelemetrySpanEvent,
|
|
8882
9372
|
validatedCapabilityOf,
|
|
8883
9373
|
verifyDecryptResponse,
|
|
9374
|
+
verifyResourceDestination,
|
|
8884
9375
|
wrapError
|
|
8885
9376
|
});
|
|
8886
9377
|
//# sourceMappingURL=index.cjs.map
|