@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.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);
|
|
@@ -2157,11 +2404,15 @@ var KVService = class extends BaseService {
|
|
|
2157
2404
|
}
|
|
2158
2405
|
/** Delay KV routing until an asynchronously loaded read-through is attached. */
|
|
2159
2406
|
setReadThroughReady(ready) {
|
|
2160
|
-
const
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2407
|
+
const start = () => {
|
|
2408
|
+
if (typeof ready === "function") ready = Promise.resolve().then(ready);
|
|
2409
|
+
const finish = () => {
|
|
2410
|
+
if (this.readThroughReady === start) this.readThroughReady = null;
|
|
2411
|
+
};
|
|
2412
|
+
return ready.then(finish, finish);
|
|
2413
|
+
};
|
|
2414
|
+
this.readThroughReady = start;
|
|
2415
|
+
if (typeof ready !== "function") void start();
|
|
2165
2416
|
}
|
|
2166
2417
|
/**
|
|
2167
2418
|
* Get the service configuration.
|
|
@@ -2408,7 +2659,6 @@ var KVService = class extends BaseService {
|
|
|
2408
2659
|
return aligned;
|
|
2409
2660
|
}
|
|
2410
2661
|
async batchRead(keys, action, options) {
|
|
2411
|
-
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
2412
2662
|
if (!this.requireAuth()) return err(authRequiredError("kv"));
|
|
2413
2663
|
if (keys.length === 0) return ok({ results: [], count: 0 });
|
|
2414
2664
|
if (keys.length > MAX_KV_BATCH_READ_ITEMS) {
|
|
@@ -2659,7 +2909,7 @@ var KVService = class extends BaseService {
|
|
|
2659
2909
|
*/
|
|
2660
2910
|
async get(key, options) {
|
|
2661
2911
|
return this.withTelemetry("get", key, async () => {
|
|
2662
|
-
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
2912
|
+
if (options?.source !== "network" && this.readThroughReady !== null) await this.readThroughReady();
|
|
2663
2913
|
if (!this.requireAuth()) {
|
|
2664
2914
|
return err(authRequiredError("kv"));
|
|
2665
2915
|
}
|
|
@@ -2778,7 +3028,7 @@ var KVService = class extends BaseService {
|
|
|
2778
3028
|
*/
|
|
2779
3029
|
async put(key, value, options) {
|
|
2780
3030
|
return this.withTelemetry("put", key, async () => {
|
|
2781
|
-
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
3031
|
+
if (this.readThroughReady !== null) await this.readThroughReady();
|
|
2782
3032
|
if (!this.requireAuth()) {
|
|
2783
3033
|
return err(authRequiredError("kv"));
|
|
2784
3034
|
}
|
|
@@ -2863,7 +3113,7 @@ var KVService = class extends BaseService {
|
|
|
2863
3113
|
*/
|
|
2864
3114
|
async batchPut(items, options) {
|
|
2865
3115
|
return this.withTelemetry("batchPut", String(items.length), async () => {
|
|
2866
|
-
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
3116
|
+
if (this.readThroughReady !== null) await this.readThroughReady();
|
|
2867
3117
|
if (!this.requireAuth()) {
|
|
2868
3118
|
return err(authRequiredError("kv"));
|
|
2869
3119
|
}
|
|
@@ -3062,7 +3312,7 @@ var KVService = class extends BaseService {
|
|
|
3062
3312
|
*/
|
|
3063
3313
|
async list(options) {
|
|
3064
3314
|
return this.withTelemetry("list", options?.prefix, async () => {
|
|
3065
|
-
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
3315
|
+
if (options?.source !== "network" && this.readThroughReady !== null) await this.readThroughReady();
|
|
3066
3316
|
if (!this.requireAuth()) {
|
|
3067
3317
|
return err(authRequiredError("kv"));
|
|
3068
3318
|
}
|
|
@@ -3350,7 +3600,7 @@ var KVService = class extends BaseService {
|
|
|
3350
3600
|
*/
|
|
3351
3601
|
async delete(key, options) {
|
|
3352
3602
|
return this.withTelemetry("delete", key, async () => {
|
|
3353
|
-
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
3603
|
+
if (this.readThroughReady !== null) await this.readThroughReady();
|
|
3354
3604
|
if (!this.requireAuth()) {
|
|
3355
3605
|
return err(authRequiredError("kv"));
|
|
3356
3606
|
}
|
|
@@ -5953,9 +6203,9 @@ function utf8Encode(s) {
|
|
|
5953
6203
|
function utf8Decode(b) {
|
|
5954
6204
|
return new TextDecoder().decode(b);
|
|
5955
6205
|
}
|
|
5956
|
-
function canonicalHashHex(
|
|
6206
|
+
function canonicalHashHex(sha2562, value) {
|
|
5957
6207
|
const canonical = canonicalize(value);
|
|
5958
|
-
return hexEncode(
|
|
6208
|
+
return hexEncode(sha2562(utf8Encode(canonical)));
|
|
5959
6209
|
}
|
|
5960
6210
|
|
|
5961
6211
|
// src/vault/types.ts
|
|
@@ -6197,6 +6447,8 @@ function defaultVaultMessage(input) {
|
|
|
6197
6447
|
case "STORAGE_QUOTA_EXCEEDED":
|
|
6198
6448
|
case "STORAGE_LIMIT_REACHED":
|
|
6199
6449
|
return input.message;
|
|
6450
|
+
default:
|
|
6451
|
+
return input.message ?? input.code;
|
|
6200
6452
|
}
|
|
6201
6453
|
}
|
|
6202
6454
|
function vaultError(input) {
|
|
@@ -6248,6 +6500,8 @@ function ifMatchWriteError(key, error) {
|
|
|
6248
6500
|
});
|
|
6249
6501
|
}
|
|
6250
6502
|
function kvWriteError(context, error) {
|
|
6503
|
+
const routing = routingFailure(error);
|
|
6504
|
+
if (routing) return vaultError(routing);
|
|
6251
6505
|
if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
|
|
6252
6506
|
return vaultError({ code: error.code, message: error.message, meta: error.meta });
|
|
6253
6507
|
}
|
|
@@ -6554,6 +6808,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6554
6808
|
{ aad, metadata }
|
|
6555
6809
|
);
|
|
6556
6810
|
if (!envelopeResult.ok) {
|
|
6811
|
+
const routing = routingFailure(envelopeResult.error);
|
|
6812
|
+
if (routing) return vaultError(routing);
|
|
6557
6813
|
return vaultError({
|
|
6558
6814
|
code: "STORAGE_ERROR",
|
|
6559
6815
|
cause: new Error(envelopeResult.error.message)
|
|
@@ -6580,6 +6836,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6580
6836
|
}
|
|
6581
6837
|
return { ok: true, data: void 0 };
|
|
6582
6838
|
} catch (error) {
|
|
6839
|
+
const routing = routingFailure(error);
|
|
6840
|
+
if (routing) return vaultError(routing);
|
|
6583
6841
|
return vaultError({
|
|
6584
6842
|
code: "STORAGE_ERROR",
|
|
6585
6843
|
cause: toError(error)
|
|
@@ -6606,6 +6864,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6606
6864
|
...options?.source === void 0 ? {} : { source: options.source }
|
|
6607
6865
|
});
|
|
6608
6866
|
if (!valueResult.ok) {
|
|
6867
|
+
const routing = routingFailure(valueResult.error);
|
|
6868
|
+
if (routing) return vaultError(routing);
|
|
6609
6869
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6610
6870
|
}
|
|
6611
6871
|
const rawEnvelope = unwrapKVData(valueResult.data);
|
|
@@ -6626,6 +6886,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6626
6886
|
const proof = await this.decryptCapabilityProof();
|
|
6627
6887
|
const plaintextResult = await config.service.decryptEnvelope(envelope, proof);
|
|
6628
6888
|
if (!plaintextResult.ok) {
|
|
6889
|
+
const routing = routingFailure(plaintextResult.error);
|
|
6890
|
+
if (routing) return vaultError(routing);
|
|
6629
6891
|
return vaultError({
|
|
6630
6892
|
code: "DECRYPTION_FAILED",
|
|
6631
6893
|
message: plaintextResult.error.message
|
|
@@ -6645,6 +6907,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6645
6907
|
data: { value, metadata, keyId, ...etag === void 0 ? {} : { etag } }
|
|
6646
6908
|
};
|
|
6647
6909
|
} catch (error) {
|
|
6910
|
+
const routing = routingFailure(error);
|
|
6911
|
+
if (routing) return vaultError(routing);
|
|
6648
6912
|
return vaultError({
|
|
6649
6913
|
code: "STORAGE_ERROR",
|
|
6650
6914
|
cause: toError(error)
|
|
@@ -6657,6 +6921,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6657
6921
|
* Unlike {@link getNetworkEncrypted}, this is intentionally message-free so
|
|
6658
6922
|
* an operation layer can distinguish an authorized absence from a failed
|
|
6659
6923
|
* read without receiving node, envelope, plaintext, or key material.
|
|
6924
|
+
* Typed routing failures reject unchanged rather than becoming an ordinary
|
|
6925
|
+
* read/decrypt failure; the operation boundary projects their safe code.
|
|
6660
6926
|
*/
|
|
6661
6927
|
async readNetworkEncrypted(key, options) {
|
|
6662
6928
|
const config = this.networkEncryption;
|
|
@@ -6666,10 +6932,12 @@ var DataVaultService = class extends BaseService {
|
|
|
6666
6932
|
let valueResult;
|
|
6667
6933
|
try {
|
|
6668
6934
|
valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
|
|
6669
|
-
} catch {
|
|
6935
|
+
} catch (error) {
|
|
6936
|
+
if (routingFailure(error)) throw error;
|
|
6670
6937
|
return { status: "node_unreachable" };
|
|
6671
6938
|
}
|
|
6672
6939
|
if (!valueResult.ok) {
|
|
6940
|
+
if (routingFailure(valueResult.error)) throw valueResult.error;
|
|
6673
6941
|
const permissionHint = parsePermissionHint(valueResult.error.meta?.permissionHint);
|
|
6674
6942
|
if (valueResult.error.code === "AUTH_UNAUTHORIZED" && permissionHint !== void 0) {
|
|
6675
6943
|
return { status: "permission_required", hint: permissionHint };
|
|
@@ -6677,7 +6945,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6677
6945
|
if ((valueResult.error.code === "NOT_FOUND" || valueResult.error.code === "KV_NOT_FOUND") && !hasHttpResponse(valueResult.error)) {
|
|
6678
6946
|
return { status: "not_found" };
|
|
6679
6947
|
}
|
|
6680
|
-
if (valueResult.error.code === "NETWORK_ERROR" || valueResult.error.code === "TIMEOUT" || valueResult.error.code === "ABORTED") {
|
|
6948
|
+
if (valueResult.error.code === "NODE_NOT_RESPONDING" || valueResult.error.code === "NETWORK_ERROR" || valueResult.error.code === "TIMEOUT" || valueResult.error.code === "ABORTED") {
|
|
6681
6949
|
return hasHttpResponse(valueResult.error) ? { status: "read_failed" } : { status: "node_unreachable" };
|
|
6682
6950
|
}
|
|
6683
6951
|
return { status: "read_failed" };
|
|
@@ -6699,10 +6967,12 @@ var DataVaultService = class extends BaseService {
|
|
|
6699
6967
|
try {
|
|
6700
6968
|
const proof = await this.decryptCapabilityProof();
|
|
6701
6969
|
plaintextResult = await config.service.decryptEnvelope(envelope, proof);
|
|
6702
|
-
} catch {
|
|
6970
|
+
} catch (error) {
|
|
6971
|
+
if (routingFailure(error)) throw error;
|
|
6703
6972
|
return { status: "decrypt_failed" };
|
|
6704
6973
|
}
|
|
6705
6974
|
if (!plaintextResult.ok) {
|
|
6975
|
+
if (routingFailure(plaintextResult.error)) throw plaintextResult.error;
|
|
6706
6976
|
const permissionHint = parsePermissionHint(
|
|
6707
6977
|
plaintextResult.error.permissionHint
|
|
6708
6978
|
);
|
|
@@ -6712,7 +6982,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6712
6982
|
if (plaintextResult.error.code === "INVALID_ENVELOPE") {
|
|
6713
6983
|
return { status: "corrupt_envelope" };
|
|
6714
6984
|
}
|
|
6715
|
-
if (plaintextResult.error.code === "TRANSPORT_ERROR") {
|
|
6985
|
+
if (plaintextResult.error.code === "NODE_NOT_RESPONDING" || plaintextResult.error.code === "TRANSPORT_ERROR") {
|
|
6716
6986
|
return { status: "node_unreachable" };
|
|
6717
6987
|
}
|
|
6718
6988
|
return { status: "decrypt_failed" };
|
|
@@ -6964,6 +7234,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6964
7234
|
if (this.usesNetworkEncryption) {
|
|
6965
7235
|
const valueDelResult2 = await this.tc.kv.delete(`vault/${key}`);
|
|
6966
7236
|
if (!valueDelResult2.ok) {
|
|
7237
|
+
const routing = routingFailure(valueDelResult2.error);
|
|
7238
|
+
if (routing) return vaultError(routing);
|
|
6967
7239
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6968
7240
|
}
|
|
6969
7241
|
return ok(void 0);
|
|
@@ -6973,10 +7245,14 @@ var DataVaultService = class extends BaseService {
|
|
|
6973
7245
|
this.tc.kv.delete(`vault/${key}`)
|
|
6974
7246
|
]);
|
|
6975
7247
|
if (!keyDelResult.ok && !valueDelResult.ok) {
|
|
7248
|
+
const routing = routingFailure(keyDelResult.error) ?? routingFailure(valueDelResult.error);
|
|
7249
|
+
if (routing) return vaultError(routing);
|
|
6976
7250
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6977
7251
|
}
|
|
6978
7252
|
return ok(void 0);
|
|
6979
7253
|
} catch (error) {
|
|
7254
|
+
const routing = routingFailure(error);
|
|
7255
|
+
if (routing) return vaultError(routing);
|
|
6980
7256
|
return vaultError({
|
|
6981
7257
|
code: "STORAGE_ERROR",
|
|
6982
7258
|
cause: toError(error)
|
|
@@ -7018,6 +7294,8 @@ var DataVaultService = class extends BaseService {
|
|
|
7018
7294
|
...options?.cursor === void 0 ? {} : { cursor: options.cursor }
|
|
7019
7295
|
});
|
|
7020
7296
|
if (!listResult.ok) {
|
|
7297
|
+
const routing = routingFailure(listResult.error);
|
|
7298
|
+
if (routing) return vaultError(routing);
|
|
7021
7299
|
return vaultError({
|
|
7022
7300
|
code: "STORAGE_ERROR",
|
|
7023
7301
|
cause: new Error(
|
|
@@ -7038,6 +7316,8 @@ var DataVaultService = class extends BaseService {
|
|
|
7038
7316
|
...listResult.data.nextCursor === void 0 ? {} : { nextCursor: listResult.data.nextCursor }
|
|
7039
7317
|
});
|
|
7040
7318
|
} catch (error) {
|
|
7319
|
+
const routing = routingFailure(error);
|
|
7320
|
+
if (routing) return vaultError(routing);
|
|
7041
7321
|
return vaultError({
|
|
7042
7322
|
code: "STORAGE_ERROR",
|
|
7043
7323
|
cause: toError(error)
|
|
@@ -7869,6 +8149,8 @@ function defaultEncryptionMessage(input) {
|
|
|
7869
8149
|
return input.message ?? input.cause.message;
|
|
7870
8150
|
case "INVALID_INPUT":
|
|
7871
8151
|
return input.message;
|
|
8152
|
+
default:
|
|
8153
|
+
return input.message ?? input.code;
|
|
7872
8154
|
}
|
|
7873
8155
|
}
|
|
7874
8156
|
function encryptionError(input) {
|
|
@@ -7933,6 +8215,8 @@ async function discoverNetwork(input) {
|
|
|
7933
8215
|
return { ok: true, data: { descriptor: validated.data, source: "node" } };
|
|
7934
8216
|
}
|
|
7935
8217
|
} catch (err3) {
|
|
8218
|
+
const routing = routingFailure(err3);
|
|
8219
|
+
if (routing) return { ok: false, error: encryptionError(routing) };
|
|
7936
8220
|
}
|
|
7937
8221
|
}
|
|
7938
8222
|
if (input.wellKnown !== void 0) {
|
|
@@ -7950,6 +8234,8 @@ async function discoverNetwork(input) {
|
|
|
7950
8234
|
};
|
|
7951
8235
|
}
|
|
7952
8236
|
} catch (err3) {
|
|
8237
|
+
const routing = routingFailure(err3);
|
|
8238
|
+
if (routing) return { ok: false, error: encryptionError(routing) };
|
|
7953
8239
|
}
|
|
7954
8240
|
}
|
|
7955
8241
|
return {
|
|
@@ -8039,6 +8325,152 @@ function ensureNetworkUsableForDecrypt(descriptor) {
|
|
|
8039
8325
|
};
|
|
8040
8326
|
}
|
|
8041
8327
|
|
|
8328
|
+
// src/encryption/keyWrap.ts
|
|
8329
|
+
var import_aes = require("@noble/ciphers/aes");
|
|
8330
|
+
var import_ed25519 = require("@noble/curves/ed25519");
|
|
8331
|
+
var import_hkdf = require("@noble/hashes/hkdf");
|
|
8332
|
+
var import_sha256 = require("@noble/hashes/sha256");
|
|
8333
|
+
var import_utils = require("@noble/hashes/utils");
|
|
8334
|
+
var KEY_WRAP_V2_ALG = "x25519-hkdf-aes256gcm/v2";
|
|
8335
|
+
var MAGIC = utf8Encode("TCW2");
|
|
8336
|
+
var INFO = utf8Encode("tinycloud.encryption.key-wrap/v2");
|
|
8337
|
+
var MAX_HEADER = 65536;
|
|
8338
|
+
function concat(...parts) {
|
|
8339
|
+
const result = new Uint8Array(parts.reduce((length, part) => length + part.length, 0));
|
|
8340
|
+
let offset = 0;
|
|
8341
|
+
for (const part of parts) {
|
|
8342
|
+
result.set(part, offset);
|
|
8343
|
+
offset += part.length;
|
|
8344
|
+
}
|
|
8345
|
+
return result;
|
|
8346
|
+
}
|
|
8347
|
+
function equal(a, b) {
|
|
8348
|
+
if (a.length !== b.length) return false;
|
|
8349
|
+
let difference = 0;
|
|
8350
|
+
for (let i = 0; i < a.length; i++) difference |= a[i] ^ b[i];
|
|
8351
|
+
return difference === 0;
|
|
8352
|
+
}
|
|
8353
|
+
function decode32(value) {
|
|
8354
|
+
if (typeof value !== "string") throw new Error("Invalid key wrap encoding");
|
|
8355
|
+
const decoded = base64Decode(value);
|
|
8356
|
+
if (decoded.length !== 32 || base64Encode(decoded) !== value) throw new Error("Invalid key wrap encoding");
|
|
8357
|
+
return decoded;
|
|
8358
|
+
}
|
|
8359
|
+
function isWellFormed(value) {
|
|
8360
|
+
for (let i = 0; i < value.length; i++) {
|
|
8361
|
+
const unit = value.charCodeAt(i);
|
|
8362
|
+
if (unit >= 55296 && unit <= 56319) {
|
|
8363
|
+
const next = value.charCodeAt(++i);
|
|
8364
|
+
if (!(next >= 56320 && next <= 57343)) return false;
|
|
8365
|
+
} else if (unit >= 56320 && unit <= 57343) return false;
|
|
8366
|
+
}
|
|
8367
|
+
return true;
|
|
8368
|
+
}
|
|
8369
|
+
function validateContext(context) {
|
|
8370
|
+
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))) {
|
|
8371
|
+
throw new Error("Invalid key wrap context");
|
|
8372
|
+
}
|
|
8373
|
+
if (context.networkId !== "") {
|
|
8374
|
+
const resource = parseEncryptionResource(context.keyPath === "" ? context.networkId : `${context.networkId}/${context.keyPath}`);
|
|
8375
|
+
if (resource.networkId !== context.networkId || (resource.keyPath ?? "") !== context.keyPath || context.keyPath.endsWith("/")) {
|
|
8376
|
+
throw new Error("Invalid key wrap resource");
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8379
|
+
}
|
|
8380
|
+
function isKeyWrapV2(wrapped) {
|
|
8381
|
+
return wrapped.length >= 4 && MAGIC.every((byte, index) => wrapped[index] === byte);
|
|
8382
|
+
}
|
|
8383
|
+
function readKeyWrapV2(wrapped) {
|
|
8384
|
+
if (!isKeyWrapV2(wrapped) || wrapped.length < 8) throw new Error("Invalid v2 key wrap frame");
|
|
8385
|
+
const length = new DataView(wrapped.buffer, wrapped.byteOffset, wrapped.byteLength).getUint32(4);
|
|
8386
|
+
if (length === 0 || length > MAX_HEADER || wrapped.length < 8 + length + 12 + 16) throw new Error("Invalid v2 key wrap length");
|
|
8387
|
+
const bytes = wrapped.subarray(8, 8 + length);
|
|
8388
|
+
const header = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
8389
|
+
const fields = ["v", "alg", "kdf", "aead", "salt", "ephemeralPublicKey", "recipientPublicKey", "purpose", "networkId", "keyPath", "keyVersion", "metadata"].sort();
|
|
8390
|
+
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") {
|
|
8391
|
+
throw new Error("Invalid v2 key wrap header");
|
|
8392
|
+
}
|
|
8393
|
+
validateContext(header);
|
|
8394
|
+
decode32(header.salt);
|
|
8395
|
+
decode32(header.ephemeralPublicKey);
|
|
8396
|
+
decode32(header.recipientPublicKey);
|
|
8397
|
+
if (!equal(bytes, utf8Encode(canonicalize({ ...header })))) throw new Error("Non-canonical v2 key wrap header");
|
|
8398
|
+
const end = 8 + length;
|
|
8399
|
+
return { header, aad: wrapped.subarray(0, end), nonce: wrapped.subarray(end, end + 12), ciphertext: wrapped.subarray(end + 12) };
|
|
8400
|
+
}
|
|
8401
|
+
function assertKeyWrapContext(header, expected) {
|
|
8402
|
+
validateContext(expected);
|
|
8403
|
+
for (const field of ["purpose", "networkId", "keyPath", "keyVersion"]) {
|
|
8404
|
+
if (header[field] !== expected[field]) throw new Error("Key wrap context mismatch");
|
|
8405
|
+
}
|
|
8406
|
+
if (canonicalize(header.metadata) !== canonicalize(expected.metadata)) throw new Error("Key wrap metadata mismatch");
|
|
8407
|
+
}
|
|
8408
|
+
function deriveKey(secret, ephemeral, recipient, salt) {
|
|
8409
|
+
const shared = import_ed25519.x25519.getSharedSecret(secret, ephemeral);
|
|
8410
|
+
if (shared.every((byte) => byte === 0)) throw new Error("Invalid key agreement");
|
|
8411
|
+
try {
|
|
8412
|
+
return (0, import_hkdf.hkdf)(import_sha256.sha256, shared, salt, concat(INFO, ephemeral, recipient), 32);
|
|
8413
|
+
} finally {
|
|
8414
|
+
shared.fill(0);
|
|
8415
|
+
}
|
|
8416
|
+
}
|
|
8417
|
+
function sealKeyV2(recipientPublicKey, plaintext, context) {
|
|
8418
|
+
validateContext(context);
|
|
8419
|
+
if (recipientPublicKey.length !== 32) throw new Error("Invalid recipient public key");
|
|
8420
|
+
const secret = (0, import_utils.randomBytes)(32);
|
|
8421
|
+
const ephemeral = import_ed25519.x25519.getPublicKey(secret);
|
|
8422
|
+
const salt = (0, import_utils.randomBytes)(32);
|
|
8423
|
+
const header = {
|
|
8424
|
+
...context,
|
|
8425
|
+
v: 2,
|
|
8426
|
+
alg: KEY_WRAP_V2_ALG,
|
|
8427
|
+
kdf: "hkdf-sha256",
|
|
8428
|
+
aead: "aes-256-gcm",
|
|
8429
|
+
salt: base64Encode(salt),
|
|
8430
|
+
ephemeralPublicKey: base64Encode(ephemeral),
|
|
8431
|
+
recipientPublicKey: base64Encode(recipientPublicKey)
|
|
8432
|
+
};
|
|
8433
|
+
const bytes = utf8Encode(canonicalize({ ...header }));
|
|
8434
|
+
if (bytes.length > MAX_HEADER) throw new Error("Key wrap header too large");
|
|
8435
|
+
const prefix = new Uint8Array(8);
|
|
8436
|
+
prefix.set(MAGIC);
|
|
8437
|
+
new DataView(prefix.buffer).setUint32(4, bytes.length);
|
|
8438
|
+
const aad = concat(prefix, bytes);
|
|
8439
|
+
const nonce = (0, import_utils.randomBytes)(12);
|
|
8440
|
+
let key;
|
|
8441
|
+
try {
|
|
8442
|
+
const shared = import_ed25519.x25519.getSharedSecret(secret, recipientPublicKey);
|
|
8443
|
+
try {
|
|
8444
|
+
if (shared.every((byte) => byte === 0)) throw new Error("Invalid key agreement");
|
|
8445
|
+
key = (0, import_hkdf.hkdf)(import_sha256.sha256, shared, salt, concat(INFO, ephemeral, recipientPublicKey), 32);
|
|
8446
|
+
} finally {
|
|
8447
|
+
shared.fill(0);
|
|
8448
|
+
}
|
|
8449
|
+
return concat(aad, nonce, (0, import_aes.gcm)(key, nonce, aad).encrypt(plaintext));
|
|
8450
|
+
} finally {
|
|
8451
|
+
secret.fill(0);
|
|
8452
|
+
key?.fill(0);
|
|
8453
|
+
}
|
|
8454
|
+
}
|
|
8455
|
+
function openKeyV2(recipientPrivateKey, wrapped, expected) {
|
|
8456
|
+
const { header, aad, nonce, ciphertext } = readKeyWrapV2(wrapped);
|
|
8457
|
+
assertKeyWrapContext(header, expected);
|
|
8458
|
+
const recipient = import_ed25519.x25519.getPublicKey(recipientPrivateKey);
|
|
8459
|
+
if (!equal(recipient, decode32(header.recipientPublicKey))) throw new Error("Key wrap recipient mismatch");
|
|
8460
|
+
const key = deriveKey(recipientPrivateKey, decode32(header.ephemeralPublicKey), recipient, decode32(header.salt));
|
|
8461
|
+
try {
|
|
8462
|
+
return (0, import_aes.gcm)(key, nonce, aad).decrypt(ciphertext);
|
|
8463
|
+
} finally {
|
|
8464
|
+
key.fill(0);
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
8467
|
+
function assertKeyWrapAlgorithm(wrapped, alg, requireV2 = false) {
|
|
8468
|
+
if (alg !== "x25519-aes256gcm/v1" && alg !== KEY_WRAP_V2_ALG) throw new Error("Unsupported key wrap algorithm");
|
|
8469
|
+
if (requireV2 && alg !== KEY_WRAP_V2_ALG) throw new Error("V2 network key wrap required");
|
|
8470
|
+
if (isKeyWrapV2(wrapped) !== (alg === KEY_WRAP_V2_ALG)) throw new Error("Key wrap algorithm mismatch");
|
|
8471
|
+
if (alg === KEY_WRAP_V2_ALG) readKeyWrapV2(wrapped);
|
|
8472
|
+
}
|
|
8473
|
+
|
|
8042
8474
|
// src/encryption/envelope.ts
|
|
8043
8475
|
function encryptToNetwork(crypto2, input) {
|
|
8044
8476
|
const parsedResource = parseEncryptionResource(
|
|
@@ -8047,11 +8479,20 @@ function encryptToNetwork(crypto2, input) {
|
|
|
8047
8479
|
if (parsedResource.keyPath !== input.keyPath || input.keyPath?.endsWith("/")) {
|
|
8048
8480
|
throw new NetworkIdError("keyPath must name an exact encryption key");
|
|
8049
8481
|
}
|
|
8050
|
-
const alg = input.
|
|
8482
|
+
const alg = input.keyWrapV2 ? KEY_WRAP_V2_ALG : DEFAULT_ENCRYPTION_ALG;
|
|
8483
|
+
if (input.alg !== void 0 && input.alg !== alg) {
|
|
8484
|
+
throw new Error("Algorithm does not match keyWrapV2");
|
|
8485
|
+
}
|
|
8051
8486
|
const keyVersion = input.keyVersion ?? DEFAULT_KEY_VERSION;
|
|
8052
8487
|
const symmetricKey = crypto2.randomBytes(32);
|
|
8053
8488
|
const ciphertext = crypto2.authEncrypt(symmetricKey, input.plaintext, input.aad);
|
|
8054
|
-
const wrapped =
|
|
8489
|
+
const wrapped = input.keyWrapV2 ? sealKeyV2(input.networkPublicKey, symmetricKey, {
|
|
8490
|
+
purpose: "network-key",
|
|
8491
|
+
networkId: input.networkId,
|
|
8492
|
+
keyPath: input.keyPath ?? "",
|
|
8493
|
+
keyVersion,
|
|
8494
|
+
metadata: input.metadata ?? {}
|
|
8495
|
+
}) : crypto2.sealToNetworkKey(input.networkPublicKey, symmetricKey);
|
|
8055
8496
|
const encryptedSymmetricKey = base64Encode(wrapped);
|
|
8056
8497
|
const encryptedSymmetricKeyHash = canonicalHashHex(
|
|
8057
8498
|
crypto2.sha256,
|
|
@@ -8071,7 +8512,7 @@ function encryptToNetwork(crypto2, input) {
|
|
|
8071
8512
|
};
|
|
8072
8513
|
return { envelope, symmetricKey };
|
|
8073
8514
|
}
|
|
8074
|
-
function validateEnvelope(crypto2, envelope) {
|
|
8515
|
+
function validateEnvelope(crypto2, envelope, requireKeyWrapV2 = false) {
|
|
8075
8516
|
if (envelope === null || typeof envelope !== "object") {
|
|
8076
8517
|
return {
|
|
8077
8518
|
ok: false,
|
|
@@ -8148,9 +8589,28 @@ function validateEnvelope(crypto2, envelope) {
|
|
|
8148
8589
|
})
|
|
8149
8590
|
};
|
|
8150
8591
|
}
|
|
8592
|
+
try {
|
|
8593
|
+
validateEnvelopeKeyWrap(e, requireKeyWrapV2);
|
|
8594
|
+
} catch {
|
|
8595
|
+
return { ok: false, error: encryptionError({ code: "INVALID_ENVELOPE", message: "Envelope key wrap context is invalid" }) };
|
|
8596
|
+
}
|
|
8151
8597
|
return { ok: true, data: e };
|
|
8152
8598
|
}
|
|
8599
|
+
function validateEnvelopeKeyWrap(envelope, requireKeyWrapV2 = false) {
|
|
8600
|
+
const wrapped = base64Decode(envelope.encryptedSymmetricKey);
|
|
8601
|
+
assertKeyWrapAlgorithm(wrapped, envelope.alg, requireKeyWrapV2);
|
|
8602
|
+
if (envelope.alg === KEY_WRAP_V2_ALG) {
|
|
8603
|
+
assertKeyWrapContext(readKeyWrapV2(wrapped).header, {
|
|
8604
|
+
purpose: "network-key",
|
|
8605
|
+
networkId: envelope.networkId,
|
|
8606
|
+
keyPath: envelope.keyPath ?? "",
|
|
8607
|
+
keyVersion: envelope.keyVersion,
|
|
8608
|
+
metadata: envelope.metadata ?? {}
|
|
8609
|
+
});
|
|
8610
|
+
}
|
|
8611
|
+
}
|
|
8153
8612
|
function decryptEnvelopeWithKey(crypto2, envelope, symmetricKey) {
|
|
8613
|
+
validateEnvelopeKeyWrap(envelope);
|
|
8154
8614
|
const ciphertext = base64Decode(envelope.ciphertext);
|
|
8155
8615
|
const aad = envelope.aad !== void 0 ? base64Decode(envelope.aad) : void 0;
|
|
8156
8616
|
return crypto2.authDecrypt(symmetricKey, ciphertext, aad);
|
|
@@ -8440,7 +8900,7 @@ function verifyDecryptResponse(input) {
|
|
|
8440
8900
|
})
|
|
8441
8901
|
};
|
|
8442
8902
|
}
|
|
8443
|
-
if (response.alg !==
|
|
8903
|
+
if (response.alg !== "x25519-aes256gcm/v1" && response.alg !== KEY_WRAP_V2_ALG) {
|
|
8444
8904
|
return {
|
|
8445
8905
|
ok: false,
|
|
8446
8906
|
error: encryptionError({
|
|
@@ -8497,7 +8957,7 @@ function verifyDecryptResponse(input) {
|
|
|
8497
8957
|
})
|
|
8498
8958
|
};
|
|
8499
8959
|
}
|
|
8500
|
-
if (facts.encryptedSymmetricKeyHash !== response.encryptedSymmetricKeyHash || facts.receiverPublicKeyHash !== response.receiverPublicKeyHash || facts.networkId !== response.networkId || facts.targetNode !== response.targetNode || facts.alg !==
|
|
8960
|
+
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
8961
|
return {
|
|
8502
8962
|
ok: false,
|
|
8503
8963
|
error: encryptionError({
|
|
@@ -8520,8 +8980,13 @@ function verifyDecryptResponse(input) {
|
|
|
8520
8980
|
}
|
|
8521
8981
|
return { ok: true, data: response };
|
|
8522
8982
|
}
|
|
8523
|
-
function openWrappedKey(crypto2, receiverPrivateKey, response) {
|
|
8983
|
+
function openWrappedKey(crypto2, receiverPrivateKey, response, expected) {
|
|
8524
8984
|
const wrapped = base64Decode(response.wrappedKey);
|
|
8985
|
+
assertKeyWrapAlgorithm(wrapped, response.alg);
|
|
8986
|
+
if (response.alg === KEY_WRAP_V2_ALG) {
|
|
8987
|
+
if (!expected) throw new Error("V2 response requires expected key context");
|
|
8988
|
+
return openKeyV2(receiverPrivateKey, wrapped, expected);
|
|
8989
|
+
}
|
|
8525
8990
|
return crypto2.openWithReceiverKey(receiverPrivateKey, wrapped);
|
|
8526
8991
|
}
|
|
8527
8992
|
|
|
@@ -8534,6 +8999,14 @@ function encOk(data) {
|
|
|
8534
8999
|
function encErr(error) {
|
|
8535
9000
|
return { ok: false, error };
|
|
8536
9001
|
}
|
|
9002
|
+
function decryptionError(input) {
|
|
9003
|
+
const unavailable = input.code === "NODE_NOT_RESPONDING" || input.code === "TRANSPORT_ERROR" && input.cause.name !== "AbortError";
|
|
9004
|
+
return encryptionError(unavailable ? {
|
|
9005
|
+
...input,
|
|
9006
|
+
code: "DECRYPTION_DEPENDENCY_UNAVAILABLE",
|
|
9007
|
+
message: "The decryption service isn't responding right now. Please try again."
|
|
9008
|
+
} : input);
|
|
9009
|
+
}
|
|
8537
9010
|
var EncryptionService = class extends BaseService {
|
|
8538
9011
|
constructor(config) {
|
|
8539
9012
|
super();
|
|
@@ -8575,12 +9048,15 @@ var EncryptionService = class extends BaseService {
|
|
|
8575
9048
|
networkPublicKey,
|
|
8576
9049
|
plaintext,
|
|
8577
9050
|
...options?.aad !== void 0 ? { aad: options.aad } : {},
|
|
8578
|
-
|
|
9051
|
+
keyWrapV2: options?.keyWrapV2 ?? this._config.keyWrapV2 ?? false,
|
|
9052
|
+
...options?.alg !== void 0 ? { alg: options.alg } : {},
|
|
8579
9053
|
keyVersion: options?.keyVersion ?? descriptor.keyVersion,
|
|
8580
9054
|
...options?.metadata !== void 0 ? { metadata: options.metadata } : {}
|
|
8581
9055
|
});
|
|
8582
9056
|
return encOk(result.envelope);
|
|
8583
9057
|
} catch (error) {
|
|
9058
|
+
const routing = routingFailure(error);
|
|
9059
|
+
if (routing) return encErr(encryptionError(routing));
|
|
8584
9060
|
return encErr(
|
|
8585
9061
|
encryptionError({
|
|
8586
9062
|
code: "TRANSPORT_ERROR",
|
|
@@ -8592,7 +9068,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8592
9068
|
async decryptEnvelope(envelope, capabilityProof, options) {
|
|
8593
9069
|
try {
|
|
8594
9070
|
this.assertActive();
|
|
8595
|
-
const validated = validateEnvelope(this.crypto, envelope);
|
|
9071
|
+
const validated = validateEnvelope(this.crypto, envelope, this._config.requireKeyWrapV2 ?? false);
|
|
8596
9072
|
if (!validated.ok) return validated;
|
|
8597
9073
|
if (options?.aad !== void 0 && validated.data.aad !== base64Encode(options.aad)) {
|
|
8598
9074
|
return encErr(
|
|
@@ -8607,12 +9083,12 @@ var EncryptionService = class extends BaseService {
|
|
|
8607
9083
|
descriptor = options.descriptor;
|
|
8608
9084
|
} else {
|
|
8609
9085
|
const discovered = await this.discoverNetwork(envelope.networkId);
|
|
8610
|
-
if (!discovered.ok) return discovered;
|
|
9086
|
+
if (!discovered.ok) return encErr(decryptionError(discovered.error));
|
|
8611
9087
|
descriptor = discovered.data;
|
|
8612
9088
|
}
|
|
8613
9089
|
const usable = ensureNetworkUsableForDecrypt(descriptor);
|
|
8614
9090
|
if (!usable.ok) return usable;
|
|
8615
|
-
const targetNode = options?.targetNode ?? descriptor.members[0]?.nodeId;
|
|
9091
|
+
const targetNode = this._config.resolveTargetNode ? await this._config.resolveTargetNode(envelope.networkId) : options?.targetNode ?? descriptor.members[0]?.nodeId;
|
|
8616
9092
|
if (targetNode === void 0) {
|
|
8617
9093
|
return encErr(
|
|
8618
9094
|
encryptionError({
|
|
@@ -8621,6 +9097,9 @@ var EncryptionService = class extends BaseService {
|
|
|
8621
9097
|
})
|
|
8622
9098
|
);
|
|
8623
9099
|
}
|
|
9100
|
+
if (this._config.resolveTargetNode && (options?.targetNode !== void 0 && options.targetNode !== targetNode || !descriptor.members.some((member) => member.nodeId === targetNode))) {
|
|
9101
|
+
return encErr(encryptionError({ code: "INVALID_INPUT", message: "Encryption destination does not match the registry" }));
|
|
9102
|
+
}
|
|
8624
9103
|
const receiverKey = generateRandomReceiverKey({ crypto: this.crypto });
|
|
8625
9104
|
const receiverPublicKey = base64Encode(receiverKey.publicKey);
|
|
8626
9105
|
const receiverPublicKeyHash = canonicalHashHex(
|
|
@@ -8679,6 +9158,8 @@ var EncryptionService = class extends BaseService {
|
|
|
8679
9158
|
});
|
|
8680
9159
|
this.assertActive();
|
|
8681
9160
|
} catch (error) {
|
|
9161
|
+
const routing = routingFailure(error);
|
|
9162
|
+
if (routing) return encErr(decryptionError(routing));
|
|
8682
9163
|
if (error instanceof import_decrypt_transport_response_error.DecryptTransportResponseError) {
|
|
8683
9164
|
return encErr(
|
|
8684
9165
|
encryptionError(
|
|
@@ -8695,7 +9176,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8695
9176
|
);
|
|
8696
9177
|
}
|
|
8697
9178
|
return encErr(
|
|
8698
|
-
|
|
9179
|
+
decryptionError({
|
|
8699
9180
|
code: "TRANSPORT_ERROR",
|
|
8700
9181
|
cause: toError2(error)
|
|
8701
9182
|
})
|
|
@@ -8713,7 +9194,8 @@ var EncryptionService = class extends BaseService {
|
|
|
8713
9194
|
const symmetricKey = openWrappedKey(
|
|
8714
9195
|
this.crypto,
|
|
8715
9196
|
receiverKey.privateKey,
|
|
8716
|
-
verified.data
|
|
9197
|
+
verified.data,
|
|
9198
|
+
{ purpose: "receiver-key", networkId: body.networkId, keyPath: body.keyPath ?? "", keyVersion: body.keyVersion, metadata: {} }
|
|
8717
9199
|
);
|
|
8718
9200
|
const plaintext = decryptEnvelopeWithKey(
|
|
8719
9201
|
this.crypto,
|
|
@@ -8722,6 +9204,8 @@ var EncryptionService = class extends BaseService {
|
|
|
8722
9204
|
);
|
|
8723
9205
|
return encOk(plaintext);
|
|
8724
9206
|
} catch (error) {
|
|
9207
|
+
const routing = routingFailure(error);
|
|
9208
|
+
if (routing) return encErr(decryptionError(routing));
|
|
8725
9209
|
return encErr(
|
|
8726
9210
|
encryptionError({
|
|
8727
9211
|
code: "INVALID_RESPONSE",
|
|
@@ -8771,7 +9255,11 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
|
|
|
8771
9255
|
MAX_SECRET_SCOPE_LENGTH,
|
|
8772
9256
|
NETWORK_NAME_PATTERN,
|
|
8773
9257
|
NetworkIdError,
|
|
9258
|
+
NodeIdentityMismatchError,
|
|
9259
|
+
NodeNotRespondingError,
|
|
9260
|
+
PartialNodeWriteError,
|
|
8774
9261
|
PrefixedKVService,
|
|
9262
|
+
ResourceNotRoutedError,
|
|
8775
9263
|
RetryPolicySchema,
|
|
8776
9264
|
SECRET_NAME_RE,
|
|
8777
9265
|
SECRET_SCOPE_RE,
|
|
@@ -8819,6 +9307,8 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
|
|
|
8819
9307
|
checkDecryptInvocationInput,
|
|
8820
9308
|
clearTinyCloudDebugLogs,
|
|
8821
9309
|
createKVResponseSchema,
|
|
9310
|
+
createResourceRoutingFetch,
|
|
9311
|
+
createResourceTransportInvocationFetch,
|
|
8822
9312
|
createResultSchema,
|
|
8823
9313
|
createVaultCrypto,
|
|
8824
9314
|
decryptEnvelopeWithKey,
|
|
@@ -8861,6 +9351,9 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
|
|
|
8861
9351
|
requiresSecretsOptIn,
|
|
8862
9352
|
resolveSecretListPrefix,
|
|
8863
9353
|
resolveSecretPath,
|
|
9354
|
+
routeResourceRequest,
|
|
9355
|
+
routingFailure,
|
|
9356
|
+
routingResourceOf,
|
|
8864
9357
|
serviceError,
|
|
8865
9358
|
storageLimitReachedError,
|
|
8866
9359
|
storageQuotaExceededError,
|
|
@@ -8881,6 +9374,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
|
|
|
8881
9374
|
validateTelemetrySpanEvent,
|
|
8882
9375
|
validatedCapabilityOf,
|
|
8883
9376
|
verifyDecryptResponse,
|
|
9377
|
+
verifyResourceDestination,
|
|
8884
9378
|
wrapError
|
|
8885
9379
|
});
|
|
8886
9380
|
//# sourceMappingURL=index.cjs.map
|