@tinycloud/sdk-services 2.2.0-beta.7 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BaseService-BiS6HRwE.d.cts → BaseService-C_iXlTeN.d.cts} +6 -1
- package/dist/{BaseService-BiS6HRwE.d.ts → BaseService-C_iXlTeN.d.ts} +6 -1
- package/dist/encryption/index.cjs +1340 -0
- package/dist/encryption/index.cjs.map +1 -0
- package/dist/encryption/index.d.cts +802 -0
- package/dist/encryption/index.d.ts +802 -0
- package/dist/encryption/index.js +1274 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/index.cjs +1555 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -12
- package/dist/index.d.ts +55 -12
- package/dist/index.js +1510 -46
- package/dist/index.js.map +1 -1
- package/dist/kv/index.cjs +116 -0
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +100 -2
- package/dist/kv/index.d.ts +100 -2
- package/dist/kv/index.js +115 -0
- package/dist/kv/index.js.map +1 -1
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +1 -1
- package/dist/sql/index.d.ts +1 -1
- package/dist/sql/index.js.map +1 -1
- package/package.json +7 -2
|
@@ -0,0 +1,1274 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var ErrorCodes = {
|
|
3
|
+
// Common errors
|
|
4
|
+
NOT_FOUND: "NOT_FOUND",
|
|
5
|
+
AUTH_EXPIRED: "AUTH_EXPIRED",
|
|
6
|
+
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
7
|
+
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
|
|
8
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
9
|
+
TIMEOUT: "TIMEOUT",
|
|
10
|
+
ABORTED: "ABORTED",
|
|
11
|
+
INVALID_INPUT: "INVALID_INPUT",
|
|
12
|
+
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
13
|
+
// KV-specific errors
|
|
14
|
+
KV_NOT_FOUND: "KV_NOT_FOUND",
|
|
15
|
+
KV_WRITE_FAILED: "KV_WRITE_FAILED",
|
|
16
|
+
// SQL-specific errors
|
|
17
|
+
SQL_ERROR: "SQL_ERROR",
|
|
18
|
+
SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
|
|
19
|
+
SQL_DATABASE_NOT_FOUND: "SQL_DATABASE_NOT_FOUND",
|
|
20
|
+
SQL_RESPONSE_TOO_LARGE: "SQL_RESPONSE_TOO_LARGE",
|
|
21
|
+
SQL_QUOTA_EXCEEDED: "SQL_QUOTA_EXCEEDED",
|
|
22
|
+
SQL_INVALID_STATEMENT: "SQL_INVALID_STATEMENT",
|
|
23
|
+
SQL_SCHEMA_ERROR: "SQL_SCHEMA_ERROR",
|
|
24
|
+
SQL_READONLY_VIOLATION: "SQL_READONLY_VIOLATION",
|
|
25
|
+
// Storage quota errors
|
|
26
|
+
STORAGE_QUOTA_EXCEEDED: "STORAGE_QUOTA_EXCEEDED",
|
|
27
|
+
STORAGE_LIMIT_REACHED: "STORAGE_LIMIT_REACHED",
|
|
28
|
+
// DuckDB-specific errors
|
|
29
|
+
DUCKDB_ERROR: "DUCKDB_ERROR",
|
|
30
|
+
DUCKDB_PERMISSION_DENIED: "DUCKDB_PERMISSION_DENIED",
|
|
31
|
+
DUCKDB_DATABASE_NOT_FOUND: "DUCKDB_DATABASE_NOT_FOUND",
|
|
32
|
+
DUCKDB_RESPONSE_TOO_LARGE: "DUCKDB_RESPONSE_TOO_LARGE",
|
|
33
|
+
DUCKDB_QUOTA_EXCEEDED: "DUCKDB_QUOTA_EXCEEDED",
|
|
34
|
+
DUCKDB_INVALID_STATEMENT: "DUCKDB_INVALID_STATEMENT",
|
|
35
|
+
DUCKDB_SCHEMA_ERROR: "DUCKDB_SCHEMA_ERROR",
|
|
36
|
+
DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
|
|
37
|
+
};
|
|
38
|
+
var defaultRetryPolicy = {
|
|
39
|
+
maxAttempts: 3,
|
|
40
|
+
backoff: "exponential",
|
|
41
|
+
baseDelayMs: 1e3,
|
|
42
|
+
maxDelayMs: 1e4,
|
|
43
|
+
retryableErrors: [ErrorCodes.NETWORK_ERROR, ErrorCodes.TIMEOUT]
|
|
44
|
+
};
|
|
45
|
+
var TelemetryEvents = {
|
|
46
|
+
SERVICE_REQUEST: "service.request",
|
|
47
|
+
SERVICE_RESPONSE: "service.response",
|
|
48
|
+
SERVICE_ERROR: "service.error",
|
|
49
|
+
SERVICE_RETRY: "service.retry",
|
|
50
|
+
SESSION_CHANGED: "session.changed",
|
|
51
|
+
SESSION_EXPIRED: "session.expired"
|
|
52
|
+
};
|
|
53
|
+
function err(error) {
|
|
54
|
+
return { ok: false, error };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/errors.ts
|
|
58
|
+
function timeoutError(service) {
|
|
59
|
+
return {
|
|
60
|
+
code: ErrorCodes.TIMEOUT,
|
|
61
|
+
message: "Request timed out.",
|
|
62
|
+
service
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function abortedError(service) {
|
|
66
|
+
return {
|
|
67
|
+
code: ErrorCodes.ABORTED,
|
|
68
|
+
message: "Request was aborted.",
|
|
69
|
+
service
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
|
|
73
|
+
if (error instanceof Error) {
|
|
74
|
+
if (error.name === "AbortError") {
|
|
75
|
+
return abortedError(service);
|
|
76
|
+
}
|
|
77
|
+
if (error.name === "TimeoutError" || error.message.toLowerCase().includes("timeout")) {
|
|
78
|
+
return timeoutError(service);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
code: defaultCode,
|
|
82
|
+
message: error.message,
|
|
83
|
+
service,
|
|
84
|
+
cause: error
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
code: defaultCode,
|
|
89
|
+
message: String(error),
|
|
90
|
+
service
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/base/BaseService.ts
|
|
95
|
+
var BaseService = class {
|
|
96
|
+
constructor() {
|
|
97
|
+
/**
|
|
98
|
+
* Abort controller for this service's operations.
|
|
99
|
+
* Reset on sign-out.
|
|
100
|
+
*/
|
|
101
|
+
this.abortController = new AbortController();
|
|
102
|
+
/**
|
|
103
|
+
* Service-specific configuration.
|
|
104
|
+
*/
|
|
105
|
+
this._config = {};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get the service configuration.
|
|
109
|
+
*/
|
|
110
|
+
get config() {
|
|
111
|
+
return this._config;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Initialize the service with context.
|
|
115
|
+
* Called by the SDK after instantiation.
|
|
116
|
+
*
|
|
117
|
+
* @param context - The service context
|
|
118
|
+
*/
|
|
119
|
+
initialize(context) {
|
|
120
|
+
this.context = context;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Called when session changes (sign-in, sign-out, refresh).
|
|
124
|
+
* Override in subclasses to handle session changes.
|
|
125
|
+
*
|
|
126
|
+
* @param session - The new session, or null if signed out
|
|
127
|
+
*/
|
|
128
|
+
onSessionChange(session) {
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Called when SDK signs out.
|
|
132
|
+
* Aborts all pending operations.
|
|
133
|
+
*/
|
|
134
|
+
onSignOut() {
|
|
135
|
+
this.abortController.abort();
|
|
136
|
+
this.abortController = new AbortController();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get the abort signal for this service.
|
|
140
|
+
* Combines the service-level abort with context-level abort.
|
|
141
|
+
*/
|
|
142
|
+
get abortSignal() {
|
|
143
|
+
return this.abortController.signal;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if the service is authenticated.
|
|
147
|
+
*/
|
|
148
|
+
get isAuthenticated() {
|
|
149
|
+
return this.context?.isAuthenticated ?? false;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get the current session.
|
|
153
|
+
* Throws if not authenticated.
|
|
154
|
+
*/
|
|
155
|
+
get session() {
|
|
156
|
+
if (!this.context?.session) {
|
|
157
|
+
throw new Error("Not authenticated");
|
|
158
|
+
}
|
|
159
|
+
return this.context.session;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Check authentication and return error result if not authenticated.
|
|
163
|
+
* Use this at the start of methods that require authentication.
|
|
164
|
+
*
|
|
165
|
+
* @returns true if authenticated, false otherwise
|
|
166
|
+
*/
|
|
167
|
+
requireAuth() {
|
|
168
|
+
return this.isAuthenticated;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Emit a telemetry event.
|
|
172
|
+
*
|
|
173
|
+
* @param event - Event name
|
|
174
|
+
* @param data - Event data
|
|
175
|
+
*/
|
|
176
|
+
emit(event, data) {
|
|
177
|
+
this.context?.emit(event, data);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Emit a service request event.
|
|
181
|
+
*
|
|
182
|
+
* @param action - The action being performed
|
|
183
|
+
* @param key - Optional key/path being accessed
|
|
184
|
+
*/
|
|
185
|
+
emitRequest(action, key) {
|
|
186
|
+
this.emit(TelemetryEvents.SERVICE_REQUEST, {
|
|
187
|
+
service: this.getServiceName(),
|
|
188
|
+
action,
|
|
189
|
+
key,
|
|
190
|
+
timestamp: Date.now()
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Emit a service response event.
|
|
195
|
+
*
|
|
196
|
+
* @param action - The action that was performed
|
|
197
|
+
* @param ok - Whether the request was successful
|
|
198
|
+
* @param startTime - Start time for duration calculation
|
|
199
|
+
* @param status - Optional HTTP status code
|
|
200
|
+
*/
|
|
201
|
+
emitResponse(action, ok, startTime, status) {
|
|
202
|
+
this.emit(TelemetryEvents.SERVICE_RESPONSE, {
|
|
203
|
+
service: this.getServiceName(),
|
|
204
|
+
action,
|
|
205
|
+
ok,
|
|
206
|
+
duration: Date.now() - startTime,
|
|
207
|
+
status
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Emit a service error event.
|
|
212
|
+
*
|
|
213
|
+
* @param error - The service error
|
|
214
|
+
*/
|
|
215
|
+
emitError(error) {
|
|
216
|
+
this.emit(TelemetryEvents.SERVICE_ERROR, {
|
|
217
|
+
service: this.getServiceName(),
|
|
218
|
+
error
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get the service name from the static property.
|
|
223
|
+
* Subclasses must define static serviceName.
|
|
224
|
+
*/
|
|
225
|
+
getServiceName() {
|
|
226
|
+
return this.constructor.serviceName;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Create a combined abort signal from multiple sources.
|
|
230
|
+
*
|
|
231
|
+
* @param signals - Additional abort signals to combine
|
|
232
|
+
* @returns A combined abort signal
|
|
233
|
+
*/
|
|
234
|
+
combineSignals(...signals) {
|
|
235
|
+
const controller = new AbortController();
|
|
236
|
+
const allSignals = [this.abortSignal, ...signals.filter(Boolean)];
|
|
237
|
+
for (const signal of allSignals) {
|
|
238
|
+
if (signal.aborted) {
|
|
239
|
+
controller.abort(signal.reason);
|
|
240
|
+
return controller.signal;
|
|
241
|
+
}
|
|
242
|
+
signal.addEventListener("abort", () => controller.abort(signal.reason), {
|
|
243
|
+
once: true
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return controller.signal;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Wrap an operation with error handling and telemetry.
|
|
250
|
+
*
|
|
251
|
+
* @param action - The action name for telemetry
|
|
252
|
+
* @param key - Optional key for telemetry
|
|
253
|
+
* @param operation - The operation to execute
|
|
254
|
+
* @returns Result of the operation
|
|
255
|
+
*/
|
|
256
|
+
async withTelemetry(action, key, operation) {
|
|
257
|
+
const startTime = Date.now();
|
|
258
|
+
this.emitRequest(action, key);
|
|
259
|
+
try {
|
|
260
|
+
const result = await operation();
|
|
261
|
+
if (result.ok) {
|
|
262
|
+
this.emitResponse(action, true, startTime);
|
|
263
|
+
} else {
|
|
264
|
+
this.emitResponse(action, false, startTime);
|
|
265
|
+
this.emitError(result.error);
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const serviceError2 = wrapError(this.getServiceName(), error);
|
|
270
|
+
this.emitResponse(action, false, startTime);
|
|
271
|
+
this.emitError(serviceError2);
|
|
272
|
+
return err(serviceError2);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/encryption/canonical.ts
|
|
278
|
+
function canonicalize(value) {
|
|
279
|
+
if (value === void 0) {
|
|
280
|
+
return "";
|
|
281
|
+
}
|
|
282
|
+
return stringify(value);
|
|
283
|
+
}
|
|
284
|
+
function stringify(value) {
|
|
285
|
+
if (value === null) return "null";
|
|
286
|
+
switch (typeof value) {
|
|
287
|
+
case "boolean":
|
|
288
|
+
case "number":
|
|
289
|
+
return JSON.stringify(value);
|
|
290
|
+
case "string":
|
|
291
|
+
return JSON.stringify(value);
|
|
292
|
+
case "object": {
|
|
293
|
+
if (Array.isArray(value)) {
|
|
294
|
+
return `[${value.map(stringify).join(",")}]`;
|
|
295
|
+
}
|
|
296
|
+
const keys = Object.keys(value).sort();
|
|
297
|
+
const parts = [];
|
|
298
|
+
for (const k of keys) {
|
|
299
|
+
const v = value[k];
|
|
300
|
+
if (v === void 0) continue;
|
|
301
|
+
parts.push(`${JSON.stringify(k)}:${stringify(v)}`);
|
|
302
|
+
}
|
|
303
|
+
return `{${parts.join(",")}}`;
|
|
304
|
+
}
|
|
305
|
+
default:
|
|
306
|
+
throw new TypeError(
|
|
307
|
+
`canonicalize: unsupported value type ${typeof value}`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
var HEX = "0123456789abcdef";
|
|
312
|
+
function hexEncode(bytes) {
|
|
313
|
+
let out = "";
|
|
314
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
315
|
+
const b = bytes[i];
|
|
316
|
+
out += HEX[b >> 4 & 15] + HEX[b & 15];
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
function hexDecode(hex) {
|
|
321
|
+
if (hex.length % 2 !== 0) {
|
|
322
|
+
throw new Error("hex string must have even length");
|
|
323
|
+
}
|
|
324
|
+
const out = new Uint8Array(hex.length / 2);
|
|
325
|
+
for (let i = 0; i < out.length; i++) {
|
|
326
|
+
const hi = parseInt(hex[i * 2], 16);
|
|
327
|
+
const lo = parseInt(hex[i * 2 + 1], 16);
|
|
328
|
+
if (Number.isNaN(hi) || Number.isNaN(lo)) {
|
|
329
|
+
throw new Error("invalid hex character");
|
|
330
|
+
}
|
|
331
|
+
out[i] = hi << 4 | lo;
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
function base64Encode(bytes) {
|
|
336
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
337
|
+
let out = "";
|
|
338
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
339
|
+
const b0 = bytes[i];
|
|
340
|
+
const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
341
|
+
const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
342
|
+
out += chars[b0 >> 2 & 63];
|
|
343
|
+
out += chars[(b0 << 4 | b1 >> 4) & 63];
|
|
344
|
+
out += i + 1 < bytes.length ? chars[(b1 << 2 | b2 >> 6) & 63] : "=";
|
|
345
|
+
out += i + 2 < bytes.length ? chars[b2 & 63] : "=";
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
function base64Decode(s) {
|
|
350
|
+
const clean = s.replace(/[^A-Za-z0-9+/=]/g, "");
|
|
351
|
+
const len = clean.length;
|
|
352
|
+
if (len % 4 !== 0) {
|
|
353
|
+
throw new Error("invalid base64 input");
|
|
354
|
+
}
|
|
355
|
+
const padding = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
|
|
356
|
+
const outLen = len / 4 * 3 - padding;
|
|
357
|
+
const out = new Uint8Array(outLen);
|
|
358
|
+
const lookup = {};
|
|
359
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
360
|
+
for (let i = 0; i < chars.length; i++) lookup[chars[i]] = i;
|
|
361
|
+
let outIdx = 0;
|
|
362
|
+
for (let i = 0; i < len; i += 4) {
|
|
363
|
+
const v0 = lookup[clean[i]] ?? 0;
|
|
364
|
+
const v1 = lookup[clean[i + 1]] ?? 0;
|
|
365
|
+
const v2 = clean[i + 2] === "=" ? 0 : lookup[clean[i + 2]] ?? 0;
|
|
366
|
+
const v3 = clean[i + 3] === "=" ? 0 : lookup[clean[i + 3]] ?? 0;
|
|
367
|
+
const b0 = v0 << 2 | v1 >> 4;
|
|
368
|
+
const b1 = (v1 & 15) << 4 | v2 >> 2;
|
|
369
|
+
const b2 = (v2 & 3) << 6 | v3;
|
|
370
|
+
if (outIdx < outLen) out[outIdx++] = b0;
|
|
371
|
+
if (outIdx < outLen) out[outIdx++] = b1;
|
|
372
|
+
if (outIdx < outLen) out[outIdx++] = b2;
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
function utf8Encode(s) {
|
|
377
|
+
return new TextEncoder().encode(s);
|
|
378
|
+
}
|
|
379
|
+
function utf8Decode(b) {
|
|
380
|
+
return new TextDecoder().decode(b);
|
|
381
|
+
}
|
|
382
|
+
function canonicalHashHex(sha256, value) {
|
|
383
|
+
const canonical = canonicalize(value);
|
|
384
|
+
return hexEncode(sha256(utf8Encode(canonical)));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/encryption/networkId.ts
|
|
388
|
+
var URN_PREFIX = "urn:tinycloud:encryption:";
|
|
389
|
+
var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
390
|
+
var NetworkIdError = class extends Error {
|
|
391
|
+
constructor(message) {
|
|
392
|
+
super(message);
|
|
393
|
+
this.name = "NetworkIdError";
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
function parseNetworkId(networkId) {
|
|
397
|
+
if (typeof networkId !== "string" || networkId.length === 0) {
|
|
398
|
+
throw new NetworkIdError("networkId must be a non-empty string");
|
|
399
|
+
}
|
|
400
|
+
if (!networkId.startsWith(URN_PREFIX)) {
|
|
401
|
+
throw new NetworkIdError(
|
|
402
|
+
`networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const body = networkId.slice(URN_PREFIX.length);
|
|
406
|
+
const lastColon = body.lastIndexOf(":");
|
|
407
|
+
if (lastColon <= 0 || lastColon === body.length - 1) {
|
|
408
|
+
throw new NetworkIdError(
|
|
409
|
+
`networkId missing principal or name segment (got ${JSON.stringify(networkId)})`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const principal = body.slice(0, lastColon);
|
|
413
|
+
const name = body.slice(lastColon + 1);
|
|
414
|
+
if (!principal.startsWith("did:")) {
|
|
415
|
+
throw new NetworkIdError(
|
|
416
|
+
`networkId principal must be a DID (got ${JSON.stringify(principal)})`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const didParts = principal.split(":");
|
|
420
|
+
if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
|
|
421
|
+
throw new NetworkIdError(
|
|
422
|
+
`networkId principal is not a well-formed DID (got ${JSON.stringify(principal)})`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
if (!NETWORK_NAME_RE.test(name)) {
|
|
426
|
+
throw new NetworkIdError(
|
|
427
|
+
`networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
return { networkId, principal, name };
|
|
431
|
+
}
|
|
432
|
+
function buildNetworkId(principal, name) {
|
|
433
|
+
if (typeof principal !== "string" || !principal.startsWith("did:")) {
|
|
434
|
+
throw new NetworkIdError("principal must be a DID");
|
|
435
|
+
}
|
|
436
|
+
if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
|
|
437
|
+
throw new NetworkIdError(
|
|
438
|
+
`network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const networkId = `${URN_PREFIX}${principal}:${name}`;
|
|
442
|
+
parseNetworkId(networkId);
|
|
443
|
+
return networkId;
|
|
444
|
+
}
|
|
445
|
+
function isNetworkId(networkId) {
|
|
446
|
+
if (typeof networkId !== "string") {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
parseNetworkId(networkId);
|
|
451
|
+
return true;
|
|
452
|
+
} catch {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function networkDiscoveryKey(name) {
|
|
457
|
+
if (!NETWORK_NAME_RE.test(name)) {
|
|
458
|
+
throw new NetworkIdError(
|
|
459
|
+
`network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return `.well-known/encryption/network/${name}`;
|
|
463
|
+
}
|
|
464
|
+
var ENCRYPTION_NETWORK_URN_PREFIX = URN_PREFIX;
|
|
465
|
+
var NETWORK_NAME_PATTERN = NETWORK_NAME_RE;
|
|
466
|
+
|
|
467
|
+
// src/encryption/types.ts
|
|
468
|
+
var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
|
|
469
|
+
var ENVELOPE_VERSION = 1;
|
|
470
|
+
var DEFAULT_KEY_VERSION = 1;
|
|
471
|
+
var DECRYPT_FACT_TYPE = "tinycloud.encryption.decrypt/v1";
|
|
472
|
+
var DECRYPT_RESULT_TYPE = "tinycloud.encryption.decrypt-result/v1";
|
|
473
|
+
var ENCRYPTION_SERVICE = "tinycloud.encryption";
|
|
474
|
+
var ENCRYPTION_SERVICE_SHORT = "encryption";
|
|
475
|
+
var DECRYPT_ACTION = "tinycloud.encryption/decrypt";
|
|
476
|
+
function defaultEncryptionMessage(input) {
|
|
477
|
+
switch (input.code) {
|
|
478
|
+
case "NETWORK_NOT_FOUND":
|
|
479
|
+
return input.message ?? `Network not found: ${input.networkId ?? input.name ?? "<unknown>"}`;
|
|
480
|
+
case "NETWORK_NOT_ACTIVE":
|
|
481
|
+
return input.message ?? `Network not active (state=${input.state})`;
|
|
482
|
+
case "INVALID_NETWORK_ID":
|
|
483
|
+
return input.message;
|
|
484
|
+
case "INVALID_ENVELOPE":
|
|
485
|
+
return input.message;
|
|
486
|
+
case "DECRYPT_DENIED":
|
|
487
|
+
return input.message;
|
|
488
|
+
case "INVALID_RESPONSE":
|
|
489
|
+
return input.message;
|
|
490
|
+
case "RESPONSE_SIGNATURE_INVALID":
|
|
491
|
+
return input.message ?? "Node response signature failed to verify";
|
|
492
|
+
case "RESPONSE_BINDING_MISMATCH":
|
|
493
|
+
return input.message ?? `Node response binding mismatch on field ${JSON.stringify(input.field)}`;
|
|
494
|
+
case "TRANSPORT_ERROR":
|
|
495
|
+
return input.message ?? input.cause.message;
|
|
496
|
+
case "INVALID_INPUT":
|
|
497
|
+
return input.message;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function encryptionError(input) {
|
|
501
|
+
return {
|
|
502
|
+
...input,
|
|
503
|
+
service: "encryption",
|
|
504
|
+
message: defaultEncryptionMessage(input)
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function toError(error) {
|
|
508
|
+
if (error instanceof Error) return error;
|
|
509
|
+
if (typeof error === "object" && error !== null) {
|
|
510
|
+
return new Error(JSON.stringify(error));
|
|
511
|
+
}
|
|
512
|
+
return new Error(String(error));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/encryption/discovery.ts
|
|
516
|
+
async function discoverNetwork(input) {
|
|
517
|
+
let networkId;
|
|
518
|
+
let principal;
|
|
519
|
+
let name;
|
|
520
|
+
try {
|
|
521
|
+
if (input.identifier.startsWith("urn:tinycloud:encryption:")) {
|
|
522
|
+
const parsed = parseNetworkId(input.identifier);
|
|
523
|
+
networkId = parsed.networkId;
|
|
524
|
+
principal = parsed.principal;
|
|
525
|
+
name = parsed.name;
|
|
526
|
+
} else {
|
|
527
|
+
if (input.principal === void 0) {
|
|
528
|
+
return {
|
|
529
|
+
ok: false,
|
|
530
|
+
error: encryptionError({
|
|
531
|
+
code: "INVALID_INPUT",
|
|
532
|
+
message: "discoverNetwork requires `principal` when identifier is a bare network name"
|
|
533
|
+
})
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
networkId = `urn:tinycloud:encryption:${input.principal}:${input.identifier}`;
|
|
537
|
+
const parsed = parseNetworkId(networkId);
|
|
538
|
+
principal = parsed.principal;
|
|
539
|
+
name = parsed.name;
|
|
540
|
+
}
|
|
541
|
+
} catch (err2) {
|
|
542
|
+
if (err2 instanceof NetworkIdError) {
|
|
543
|
+
return {
|
|
544
|
+
ok: false,
|
|
545
|
+
error: encryptionError({
|
|
546
|
+
code: "INVALID_NETWORK_ID",
|
|
547
|
+
message: err2.message
|
|
548
|
+
})
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
throw err2;
|
|
552
|
+
}
|
|
553
|
+
if (input.node !== void 0) {
|
|
554
|
+
try {
|
|
555
|
+
const descriptor = await input.node.fetchByNetworkId(networkId);
|
|
556
|
+
if (descriptor !== null) {
|
|
557
|
+
const validated = validateDescriptor(descriptor, networkId, principal, name);
|
|
558
|
+
if (!validated.ok) return validated;
|
|
559
|
+
return { ok: true, data: { descriptor: validated.data, source: "node" } };
|
|
560
|
+
}
|
|
561
|
+
} catch (err2) {
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (input.wellKnown !== void 0) {
|
|
565
|
+
try {
|
|
566
|
+
const descriptor = await input.wellKnown.fetchWellKnown(
|
|
567
|
+
principal,
|
|
568
|
+
networkDiscoveryKey(name)
|
|
569
|
+
);
|
|
570
|
+
if (descriptor !== null) {
|
|
571
|
+
const validated = validateDescriptor(descriptor, networkId, principal, name);
|
|
572
|
+
if (!validated.ok) return validated;
|
|
573
|
+
return {
|
|
574
|
+
ok: true,
|
|
575
|
+
data: { descriptor: validated.data, source: "well-known" }
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
} catch (err2) {
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
ok: false,
|
|
583
|
+
error: encryptionError({
|
|
584
|
+
code: "NETWORK_NOT_FOUND",
|
|
585
|
+
networkId,
|
|
586
|
+
name
|
|
587
|
+
})
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function validateDescriptor(descriptor, networkId, principal, name) {
|
|
591
|
+
if (descriptor.networkId !== networkId) {
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
error: encryptionError({
|
|
595
|
+
code: "INVALID_NETWORK_ID",
|
|
596
|
+
message: `descriptor networkId ${JSON.stringify(descriptor.networkId)} does not match expected ${JSON.stringify(networkId)}`
|
|
597
|
+
})
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
if (descriptor.principal !== principal) {
|
|
601
|
+
return {
|
|
602
|
+
ok: false,
|
|
603
|
+
error: encryptionError({
|
|
604
|
+
code: "INVALID_NETWORK_ID",
|
|
605
|
+
message: "descriptor principal does not match networkId principal"
|
|
606
|
+
})
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
if (descriptor.name !== name) {
|
|
610
|
+
return {
|
|
611
|
+
ok: false,
|
|
612
|
+
error: encryptionError({
|
|
613
|
+
code: "INVALID_NETWORK_ID",
|
|
614
|
+
message: "descriptor name does not match networkId name"
|
|
615
|
+
})
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
if (typeof descriptor.publicEncryptionKey !== "string" || descriptor.publicEncryptionKey.length === 0) {
|
|
619
|
+
return {
|
|
620
|
+
ok: false,
|
|
621
|
+
error: encryptionError({
|
|
622
|
+
code: "INVALID_NETWORK_ID",
|
|
623
|
+
message: "descriptor publicEncryptionKey must be a non-empty string"
|
|
624
|
+
})
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return { ok: true, data: descriptor };
|
|
628
|
+
}
|
|
629
|
+
function ensureNetworkUsableForDecrypt(descriptor) {
|
|
630
|
+
if (descriptor.state === "active" || descriptor.state === "rotating") {
|
|
631
|
+
return { ok: true, data: descriptor };
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
ok: false,
|
|
635
|
+
error: encryptionError({
|
|
636
|
+
code: "NETWORK_NOT_ACTIVE",
|
|
637
|
+
state: descriptor.state
|
|
638
|
+
})
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/encryption/envelope.ts
|
|
643
|
+
function encryptToNetwork(crypto, input) {
|
|
644
|
+
parseNetworkId(input.networkId);
|
|
645
|
+
const alg = input.alg ?? DEFAULT_ENCRYPTION_ALG;
|
|
646
|
+
const keyVersion = input.keyVersion ?? DEFAULT_KEY_VERSION;
|
|
647
|
+
const symmetricKey = crypto.randomBytes(32);
|
|
648
|
+
const ciphertext = crypto.authEncrypt(symmetricKey, input.plaintext, input.aad);
|
|
649
|
+
const wrapped = crypto.sealToNetworkKey(input.networkPublicKey, symmetricKey);
|
|
650
|
+
const encryptedSymmetricKey = base64Encode(wrapped);
|
|
651
|
+
const encryptedSymmetricKeyHash = canonicalHashHex(
|
|
652
|
+
crypto.sha256,
|
|
653
|
+
encryptedSymmetricKey
|
|
654
|
+
);
|
|
655
|
+
const envelope = {
|
|
656
|
+
v: ENVELOPE_VERSION,
|
|
657
|
+
networkId: input.networkId,
|
|
658
|
+
alg,
|
|
659
|
+
keyVersion,
|
|
660
|
+
encryptedSymmetricKey,
|
|
661
|
+
encryptedSymmetricKeyHash,
|
|
662
|
+
ciphertext: base64Encode(ciphertext),
|
|
663
|
+
...input.aad !== void 0 ? { aad: base64Encode(input.aad) } : {},
|
|
664
|
+
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
665
|
+
};
|
|
666
|
+
return { envelope, symmetricKey };
|
|
667
|
+
}
|
|
668
|
+
function validateEnvelope(crypto, envelope) {
|
|
669
|
+
if (envelope === null || typeof envelope !== "object") {
|
|
670
|
+
return {
|
|
671
|
+
ok: false,
|
|
672
|
+
error: encryptionError({
|
|
673
|
+
code: "INVALID_ENVELOPE",
|
|
674
|
+
message: "envelope must be an object"
|
|
675
|
+
})
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
const e = envelope;
|
|
679
|
+
if (e.v !== ENVELOPE_VERSION) {
|
|
680
|
+
return {
|
|
681
|
+
ok: false,
|
|
682
|
+
error: encryptionError({
|
|
683
|
+
code: "INVALID_ENVELOPE",
|
|
684
|
+
message: `envelope.v must be ${ENVELOPE_VERSION} (got ${e.v})`
|
|
685
|
+
})
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
parseNetworkId(e.networkId);
|
|
690
|
+
} catch (err2) {
|
|
691
|
+
return {
|
|
692
|
+
ok: false,
|
|
693
|
+
error: encryptionError({
|
|
694
|
+
code: "INVALID_ENVELOPE",
|
|
695
|
+
message: `envelope.networkId is malformed: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
696
|
+
})
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
for (const field of [
|
|
700
|
+
"alg",
|
|
701
|
+
"encryptedSymmetricKey",
|
|
702
|
+
"encryptedSymmetricKeyHash",
|
|
703
|
+
"ciphertext"
|
|
704
|
+
]) {
|
|
705
|
+
if (typeof e[field] !== "string" || e[field].length === 0) {
|
|
706
|
+
return {
|
|
707
|
+
ok: false,
|
|
708
|
+
error: encryptionError({
|
|
709
|
+
code: "INVALID_ENVELOPE",
|
|
710
|
+
message: `envelope.${field} must be a non-empty string`
|
|
711
|
+
})
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (typeof e.keyVersion !== "number" || !Number.isInteger(e.keyVersion)) {
|
|
716
|
+
return {
|
|
717
|
+
ok: false,
|
|
718
|
+
error: encryptionError({
|
|
719
|
+
code: "INVALID_ENVELOPE",
|
|
720
|
+
message: "envelope.keyVersion must be an integer"
|
|
721
|
+
})
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
const expectedHash = canonicalHashHex(crypto.sha256, e.encryptedSymmetricKey);
|
|
725
|
+
if (expectedHash !== e.encryptedSymmetricKeyHash) {
|
|
726
|
+
return {
|
|
727
|
+
ok: false,
|
|
728
|
+
error: encryptionError({
|
|
729
|
+
code: "INVALID_ENVELOPE",
|
|
730
|
+
message: "envelope.encryptedSymmetricKeyHash does not match canonical hash of encryptedSymmetricKey"
|
|
731
|
+
})
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
return { ok: true, data: e };
|
|
735
|
+
}
|
|
736
|
+
function decryptEnvelopeWithKey(crypto, envelope, symmetricKey) {
|
|
737
|
+
const ciphertext = base64Decode(envelope.ciphertext);
|
|
738
|
+
const aad = envelope.aad !== void 0 ? base64Decode(envelope.aad) : void 0;
|
|
739
|
+
return crypto.authDecrypt(symmetricKey, ciphertext, aad);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/encryption/invocation.ts
|
|
743
|
+
function buildCanonicalDecryptRequest(input) {
|
|
744
|
+
const canonicalBody = canonicalize(input.body);
|
|
745
|
+
const bodyHash = canonicalHashHex(
|
|
746
|
+
input.crypto.sha256,
|
|
747
|
+
input.body
|
|
748
|
+
);
|
|
749
|
+
const receiverPublicKeyHash = canonicalHashHex(
|
|
750
|
+
input.crypto.sha256,
|
|
751
|
+
input.body.receiverPublicKey
|
|
752
|
+
);
|
|
753
|
+
return { canonicalBody, bodyHash, receiverPublicKeyHash };
|
|
754
|
+
}
|
|
755
|
+
function buildDecryptFacts(input) {
|
|
756
|
+
const bodyHash = input.canonicalBody !== void 0 ? hexEncode(input.crypto.sha256(utf8Encode(input.canonicalBody))) : canonicalHashHex(
|
|
757
|
+
input.crypto.sha256,
|
|
758
|
+
input.body
|
|
759
|
+
);
|
|
760
|
+
const receiverPublicKeyHash = canonicalHashHex(
|
|
761
|
+
input.crypto.sha256,
|
|
762
|
+
input.body.receiverPublicKey
|
|
763
|
+
);
|
|
764
|
+
return {
|
|
765
|
+
type: DECRYPT_FACT_TYPE,
|
|
766
|
+
targetNode: input.body.targetNode,
|
|
767
|
+
networkId: input.body.networkId,
|
|
768
|
+
bodyHash,
|
|
769
|
+
encryptedSymmetricKeyHash: input.encryptedSymmetricKeyHash,
|
|
770
|
+
receiverPublicKeyHash,
|
|
771
|
+
alg: input.body.alg,
|
|
772
|
+
keyVersion: input.body.keyVersion
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function buildDecryptAttenuation(networkId) {
|
|
776
|
+
parseNetworkId(networkId);
|
|
777
|
+
return {
|
|
778
|
+
[networkId]: {
|
|
779
|
+
[DECRYPT_ACTION]: {}
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function checkDecryptInvocationInput(crypto, input) {
|
|
784
|
+
if (input.body.type !== DECRYPT_FACT_TYPE) {
|
|
785
|
+
return {
|
|
786
|
+
ok: false,
|
|
787
|
+
error: encryptionError({
|
|
788
|
+
code: "INVALID_INPUT",
|
|
789
|
+
message: `body.type must be ${DECRYPT_FACT_TYPE}`
|
|
790
|
+
})
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
if (input.facts.type !== DECRYPT_FACT_TYPE) {
|
|
794
|
+
return {
|
|
795
|
+
ok: false,
|
|
796
|
+
error: encryptionError({
|
|
797
|
+
code: "INVALID_INPUT",
|
|
798
|
+
message: `facts.type must be ${DECRYPT_FACT_TYPE}`
|
|
799
|
+
})
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
if (input.facts.targetNode !== input.targetNode) {
|
|
803
|
+
return {
|
|
804
|
+
ok: false,
|
|
805
|
+
error: encryptionError({
|
|
806
|
+
code: "INVALID_INPUT",
|
|
807
|
+
message: "facts.targetNode must equal targetNode \u2014 the UCAN audience binds the request to a single node"
|
|
808
|
+
})
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
if (input.body.targetNode !== input.targetNode) {
|
|
812
|
+
return {
|
|
813
|
+
ok: false,
|
|
814
|
+
error: encryptionError({
|
|
815
|
+
code: "INVALID_INPUT",
|
|
816
|
+
message: "body.targetNode must equal targetNode"
|
|
817
|
+
})
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
if (input.facts.networkId !== input.networkId) {
|
|
821
|
+
return {
|
|
822
|
+
ok: false,
|
|
823
|
+
error: encryptionError({
|
|
824
|
+
code: "INVALID_INPUT",
|
|
825
|
+
message: "facts.networkId must equal networkId"
|
|
826
|
+
})
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
if (input.body.networkId !== input.networkId) {
|
|
830
|
+
return {
|
|
831
|
+
ok: false,
|
|
832
|
+
error: encryptionError({
|
|
833
|
+
code: "INVALID_INPUT",
|
|
834
|
+
message: "body.networkId must equal networkId"
|
|
835
|
+
})
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
if (input.facts.alg !== input.body.alg) {
|
|
839
|
+
return {
|
|
840
|
+
ok: false,
|
|
841
|
+
error: encryptionError({
|
|
842
|
+
code: "INVALID_INPUT",
|
|
843
|
+
message: "facts.alg must equal body.alg"
|
|
844
|
+
})
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
if (input.facts.keyVersion !== input.body.keyVersion) {
|
|
848
|
+
return {
|
|
849
|
+
ok: false,
|
|
850
|
+
error: encryptionError({
|
|
851
|
+
code: "INVALID_INPUT",
|
|
852
|
+
message: "facts.keyVersion must equal body.keyVersion"
|
|
853
|
+
})
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
if (input.facts.encryptedSymmetricKeyHash !== input.body.encryptedSymmetricKeyHash) {
|
|
857
|
+
return {
|
|
858
|
+
ok: false,
|
|
859
|
+
error: encryptionError({
|
|
860
|
+
code: "INVALID_INPUT",
|
|
861
|
+
message: "facts.encryptedSymmetricKeyHash must equal body.encryptedSymmetricKeyHash"
|
|
862
|
+
})
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
if (input.facts.receiverPublicKeyHash !== input.body.receiverPublicKeyHash) {
|
|
866
|
+
return {
|
|
867
|
+
ok: false,
|
|
868
|
+
error: encryptionError({
|
|
869
|
+
code: "INVALID_INPUT",
|
|
870
|
+
message: "facts.receiverPublicKeyHash must equal body.receiverPublicKeyHash"
|
|
871
|
+
})
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
try {
|
|
875
|
+
parseNetworkId(input.networkId);
|
|
876
|
+
} catch (err2) {
|
|
877
|
+
return {
|
|
878
|
+
ok: false,
|
|
879
|
+
error: encryptionError({
|
|
880
|
+
code: "INVALID_NETWORK_ID",
|
|
881
|
+
message: err2 instanceof Error ? err2.message : String(err2)
|
|
882
|
+
})
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
const canonicalBody = canonicalize(
|
|
886
|
+
input.body
|
|
887
|
+
);
|
|
888
|
+
const expectedBodyHash = canonicalHashHex(crypto.sha256, input.body);
|
|
889
|
+
if (expectedBodyHash !== input.facts.bodyHash) {
|
|
890
|
+
return {
|
|
891
|
+
ok: false,
|
|
892
|
+
error: encryptionError({
|
|
893
|
+
code: "INVALID_INPUT",
|
|
894
|
+
message: "facts.bodyHash does not match the canonical body hash"
|
|
895
|
+
})
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
return { ok: true, data: input, canonicalBody };
|
|
899
|
+
}
|
|
900
|
+
async function buildDecryptInvocation(crypto, signer, input) {
|
|
901
|
+
const checked = checkDecryptInvocationInput(crypto, input);
|
|
902
|
+
if (!checked.ok) {
|
|
903
|
+
return checked;
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
const built = await signer.signDecryptInvocation(checked.data);
|
|
907
|
+
if (!built.authorization || !built.invocationCid) {
|
|
908
|
+
return {
|
|
909
|
+
ok: false,
|
|
910
|
+
error: encryptionError({
|
|
911
|
+
code: "INVALID_INPUT",
|
|
912
|
+
message: "decrypt-invocation signer returned an empty authorization or invocationCid"
|
|
913
|
+
})
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
if (built.canonicalBody !== checked.canonicalBody) {
|
|
917
|
+
return {
|
|
918
|
+
ok: false,
|
|
919
|
+
error: encryptionError({
|
|
920
|
+
code: "INVALID_INPUT",
|
|
921
|
+
message: "decrypt-invocation signer returned a canonicalBody that does not match the SDK's canonicalization \u2014 signer must use the SDK-provided body"
|
|
922
|
+
})
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
return { ok: true, data: built };
|
|
926
|
+
} catch (err2) {
|
|
927
|
+
return {
|
|
928
|
+
ok: false,
|
|
929
|
+
error: encryptionError({
|
|
930
|
+
code: "TRANSPORT_ERROR",
|
|
931
|
+
cause: err2 instanceof Error ? err2 : new Error(String(err2)),
|
|
932
|
+
message: `failed to sign decrypt invocation: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
933
|
+
})
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/encryption/receiverKey.ts
|
|
939
|
+
function generateRandomReceiverKey(input) {
|
|
940
|
+
const seed = input.crypto.randomBytes(32);
|
|
941
|
+
return input.crypto.x25519FromSeed(seed);
|
|
942
|
+
}
|
|
943
|
+
async function deriveSignedReceiverKey(input) {
|
|
944
|
+
const message = `tinycloud.encryption.receiver-key/v1:${input.networkId}:${input.context ?? ""}`;
|
|
945
|
+
const sig = await input.signer.signMessage(message);
|
|
946
|
+
const sigBytes = utf8Encode(sig);
|
|
947
|
+
const seed = input.crypto.sha256(sigBytes);
|
|
948
|
+
return input.crypto.x25519FromSeed(seed);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/encryption/response.ts
|
|
952
|
+
function canonicalSignedResponse(response) {
|
|
953
|
+
const { nodeSignature: _drop, ...rest } = response;
|
|
954
|
+
return canonicalize(rest);
|
|
955
|
+
}
|
|
956
|
+
function verifyDecryptResponse(input) {
|
|
957
|
+
const { crypto, request, facts, invocationCid, requestBodyHash, response } = input;
|
|
958
|
+
if (response.type !== DECRYPT_RESULT_TYPE) {
|
|
959
|
+
return {
|
|
960
|
+
ok: false,
|
|
961
|
+
error: encryptionError({
|
|
962
|
+
code: "INVALID_RESPONSE",
|
|
963
|
+
message: `response.type must be ${DECRYPT_RESULT_TYPE}`
|
|
964
|
+
})
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
if (response.targetNode !== request.targetNode) {
|
|
968
|
+
return {
|
|
969
|
+
ok: false,
|
|
970
|
+
error: encryptionError({
|
|
971
|
+
code: "RESPONSE_BINDING_MISMATCH",
|
|
972
|
+
field: "targetNode"
|
|
973
|
+
})
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
if (response.networkId !== request.networkId) {
|
|
977
|
+
return {
|
|
978
|
+
ok: false,
|
|
979
|
+
error: encryptionError({
|
|
980
|
+
code: "RESPONSE_BINDING_MISMATCH",
|
|
981
|
+
field: "networkId"
|
|
982
|
+
})
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
if (response.alg !== request.alg) {
|
|
986
|
+
return {
|
|
987
|
+
ok: false,
|
|
988
|
+
error: encryptionError({
|
|
989
|
+
code: "RESPONSE_BINDING_MISMATCH",
|
|
990
|
+
field: "alg"
|
|
991
|
+
})
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
if (response.keyVersion !== request.keyVersion) {
|
|
995
|
+
return {
|
|
996
|
+
ok: false,
|
|
997
|
+
error: encryptionError({
|
|
998
|
+
code: "RESPONSE_BINDING_MISMATCH",
|
|
999
|
+
field: "keyVersion"
|
|
1000
|
+
})
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
if (response.encryptedSymmetricKeyHash !== request.encryptedSymmetricKeyHash) {
|
|
1004
|
+
return {
|
|
1005
|
+
ok: false,
|
|
1006
|
+
error: encryptionError({
|
|
1007
|
+
code: "RESPONSE_BINDING_MISMATCH",
|
|
1008
|
+
field: "encryptedSymmetricKeyHash"
|
|
1009
|
+
})
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
if (response.receiverPublicKeyHash !== request.receiverPublicKeyHash) {
|
|
1013
|
+
return {
|
|
1014
|
+
ok: false,
|
|
1015
|
+
error: encryptionError({
|
|
1016
|
+
code: "RESPONSE_BINDING_MISMATCH",
|
|
1017
|
+
field: "receiverPublicKeyHash"
|
|
1018
|
+
})
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
if (response.invocationCid !== invocationCid) {
|
|
1022
|
+
return {
|
|
1023
|
+
ok: false,
|
|
1024
|
+
error: encryptionError({
|
|
1025
|
+
code: "RESPONSE_BINDING_MISMATCH",
|
|
1026
|
+
field: "invocationCid"
|
|
1027
|
+
})
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
const expectedRequestHash = hexEncode(
|
|
1031
|
+
crypto.sha256(utf8Encode(`${invocationCid}${requestBodyHash}`))
|
|
1032
|
+
);
|
|
1033
|
+
if (response.requestHash !== expectedRequestHash) {
|
|
1034
|
+
return {
|
|
1035
|
+
ok: false,
|
|
1036
|
+
error: encryptionError({
|
|
1037
|
+
code: "RESPONSE_BINDING_MISMATCH",
|
|
1038
|
+
field: "requestHash"
|
|
1039
|
+
})
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
if (facts.encryptedSymmetricKeyHash !== response.encryptedSymmetricKeyHash || facts.receiverPublicKeyHash !== response.receiverPublicKeyHash || facts.networkId !== response.networkId || facts.targetNode !== response.targetNode || facts.alg !== response.alg || facts.keyVersion !== response.keyVersion) {
|
|
1043
|
+
return {
|
|
1044
|
+
ok: false,
|
|
1045
|
+
error: encryptionError({
|
|
1046
|
+
code: "RESPONSE_BINDING_MISMATCH",
|
|
1047
|
+
field: "facts"
|
|
1048
|
+
})
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
const signedBytes = new TextEncoder().encode(
|
|
1052
|
+
canonicalSignedResponse(response)
|
|
1053
|
+
);
|
|
1054
|
+
const signatureBytes = base64Decode(response.nodeSignature);
|
|
1055
|
+
if (!crypto.verifyNodeSignature(response.nodeId, signedBytes, signatureBytes)) {
|
|
1056
|
+
return {
|
|
1057
|
+
ok: false,
|
|
1058
|
+
error: encryptionError({
|
|
1059
|
+
code: "RESPONSE_SIGNATURE_INVALID"
|
|
1060
|
+
})
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
return { ok: true, data: response };
|
|
1064
|
+
}
|
|
1065
|
+
function openWrappedKey(crypto, receiverPrivateKey, response) {
|
|
1066
|
+
const wrapped = base64Decode(response.wrappedKey);
|
|
1067
|
+
return crypto.openWithReceiverKey(receiverPrivateKey, wrapped);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/encryption/EncryptionService.ts
|
|
1071
|
+
function encOk(data) {
|
|
1072
|
+
return { ok: true, data };
|
|
1073
|
+
}
|
|
1074
|
+
function encErr(error) {
|
|
1075
|
+
return { ok: false, error };
|
|
1076
|
+
}
|
|
1077
|
+
var EncryptionService = class extends BaseService {
|
|
1078
|
+
constructor(config) {
|
|
1079
|
+
super();
|
|
1080
|
+
this._config = config;
|
|
1081
|
+
}
|
|
1082
|
+
get config() {
|
|
1083
|
+
return this._config;
|
|
1084
|
+
}
|
|
1085
|
+
get crypto() {
|
|
1086
|
+
return this._config.crypto;
|
|
1087
|
+
}
|
|
1088
|
+
async discoverNetwork(identifier, principal) {
|
|
1089
|
+
const result = await discoverNetwork({
|
|
1090
|
+
identifier,
|
|
1091
|
+
...principal !== void 0 ? { principal } : {},
|
|
1092
|
+
...this._config.node !== void 0 ? { node: this._config.node } : {},
|
|
1093
|
+
...this._config.wellKnown !== void 0 ? { wellKnown: this._config.wellKnown } : {}
|
|
1094
|
+
});
|
|
1095
|
+
if (!result.ok) return result;
|
|
1096
|
+
return encOk(result.data.descriptor);
|
|
1097
|
+
}
|
|
1098
|
+
async encryptToNetwork(networkId, plaintext, options) {
|
|
1099
|
+
try {
|
|
1100
|
+
const discovered = await this.discoverNetwork(networkId);
|
|
1101
|
+
if (!discovered.ok) return discovered;
|
|
1102
|
+
const usable = ensureNetworkUsableForDecrypt(discovered.data);
|
|
1103
|
+
if (!usable.ok) return usable;
|
|
1104
|
+
const descriptor = usable.data;
|
|
1105
|
+
const networkPublicKey = base64Decode(descriptor.publicEncryptionKey);
|
|
1106
|
+
const result = encryptToNetwork(this.crypto, {
|
|
1107
|
+
networkId,
|
|
1108
|
+
networkPublicKey,
|
|
1109
|
+
plaintext,
|
|
1110
|
+
...options?.aad !== void 0 ? { aad: options.aad } : {},
|
|
1111
|
+
alg: options?.alg ?? descriptor.alg,
|
|
1112
|
+
keyVersion: options?.keyVersion ?? descriptor.keyVersion,
|
|
1113
|
+
...options?.metadata !== void 0 ? { metadata: options.metadata } : {}
|
|
1114
|
+
});
|
|
1115
|
+
return encOk(result.envelope);
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
return encErr(
|
|
1118
|
+
encryptionError({
|
|
1119
|
+
code: "TRANSPORT_ERROR",
|
|
1120
|
+
cause: toError(error)
|
|
1121
|
+
})
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
async decryptEnvelope(envelope, capabilityProof, options) {
|
|
1126
|
+
try {
|
|
1127
|
+
const validated = validateEnvelope(this.crypto, envelope);
|
|
1128
|
+
if (!validated.ok) return validated;
|
|
1129
|
+
let descriptor;
|
|
1130
|
+
if (options?.descriptor !== void 0) {
|
|
1131
|
+
descriptor = options.descriptor;
|
|
1132
|
+
} else {
|
|
1133
|
+
const discovered = await this.discoverNetwork(envelope.networkId);
|
|
1134
|
+
if (!discovered.ok) return discovered;
|
|
1135
|
+
descriptor = discovered.data;
|
|
1136
|
+
}
|
|
1137
|
+
const usable = ensureNetworkUsableForDecrypt(descriptor);
|
|
1138
|
+
if (!usable.ok) return usable;
|
|
1139
|
+
const targetNode = options?.targetNode ?? descriptor.members[0]?.nodeId;
|
|
1140
|
+
if (targetNode === void 0) {
|
|
1141
|
+
return encErr(
|
|
1142
|
+
encryptionError({
|
|
1143
|
+
code: "INVALID_INPUT",
|
|
1144
|
+
message: "no target node available from descriptor"
|
|
1145
|
+
})
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
const receiverKey = generateRandomReceiverKey({ crypto: this.crypto });
|
|
1149
|
+
const receiverPublicKey = base64Encode(receiverKey.publicKey);
|
|
1150
|
+
const receiverPublicKeyHash = canonicalHashHex(
|
|
1151
|
+
this.crypto.sha256,
|
|
1152
|
+
receiverPublicKey
|
|
1153
|
+
);
|
|
1154
|
+
const body = {
|
|
1155
|
+
type: DECRYPT_FACT_TYPE,
|
|
1156
|
+
targetNode,
|
|
1157
|
+
networkId: envelope.networkId,
|
|
1158
|
+
alg: envelope.alg,
|
|
1159
|
+
keyVersion: envelope.keyVersion,
|
|
1160
|
+
encryptedSymmetricKey: envelope.encryptedSymmetricKey,
|
|
1161
|
+
encryptedSymmetricKeyHash: envelope.encryptedSymmetricKeyHash,
|
|
1162
|
+
receiverPublicKey,
|
|
1163
|
+
receiverPublicKeyHash
|
|
1164
|
+
};
|
|
1165
|
+
const canonicalRequest = buildCanonicalDecryptRequest({
|
|
1166
|
+
crypto: this.crypto,
|
|
1167
|
+
body,
|
|
1168
|
+
receiverPublicKey: receiverKey.publicKey
|
|
1169
|
+
});
|
|
1170
|
+
const facts = buildDecryptFacts({
|
|
1171
|
+
crypto: this.crypto,
|
|
1172
|
+
body,
|
|
1173
|
+
encryptedSymmetricKeyHash: envelope.encryptedSymmetricKeyHash,
|
|
1174
|
+
receiverPublicKey: receiverKey.publicKey,
|
|
1175
|
+
canonicalBody: canonicalRequest.canonicalBody
|
|
1176
|
+
});
|
|
1177
|
+
const built = await buildDecryptInvocation(this.crypto, this._config.signer, {
|
|
1178
|
+
targetNode,
|
|
1179
|
+
networkId: envelope.networkId,
|
|
1180
|
+
body,
|
|
1181
|
+
facts,
|
|
1182
|
+
proof: capabilityProof
|
|
1183
|
+
});
|
|
1184
|
+
if (!built.ok) return built;
|
|
1185
|
+
let response;
|
|
1186
|
+
try {
|
|
1187
|
+
response = await this._config.transport.postDecrypt({
|
|
1188
|
+
targetNode,
|
|
1189
|
+
networkId: envelope.networkId,
|
|
1190
|
+
authorization: built.data.authorization,
|
|
1191
|
+
canonicalBody: built.data.canonicalBody
|
|
1192
|
+
});
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
return encErr(
|
|
1195
|
+
encryptionError({
|
|
1196
|
+
code: "TRANSPORT_ERROR",
|
|
1197
|
+
cause: toError(error)
|
|
1198
|
+
})
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
const verified = verifyDecryptResponse({
|
|
1202
|
+
crypto: this.crypto,
|
|
1203
|
+
request: body,
|
|
1204
|
+
facts,
|
|
1205
|
+
invocationCid: built.data.invocationCid,
|
|
1206
|
+
requestBodyHash: facts.bodyHash,
|
|
1207
|
+
response
|
|
1208
|
+
});
|
|
1209
|
+
if (!verified.ok) return verified;
|
|
1210
|
+
const symmetricKey = openWrappedKey(
|
|
1211
|
+
this.crypto,
|
|
1212
|
+
receiverKey.privateKey,
|
|
1213
|
+
verified.data
|
|
1214
|
+
);
|
|
1215
|
+
const plaintext = decryptEnvelopeWithKey(
|
|
1216
|
+
this.crypto,
|
|
1217
|
+
envelope,
|
|
1218
|
+
symmetricKey
|
|
1219
|
+
);
|
|
1220
|
+
return encOk(plaintext);
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
return encErr(
|
|
1223
|
+
encryptionError({
|
|
1224
|
+
code: "TRANSPORT_ERROR",
|
|
1225
|
+
cause: toError(error)
|
|
1226
|
+
})
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
EncryptionService.serviceName = "encryption";
|
|
1232
|
+
export {
|
|
1233
|
+
DECRYPT_ACTION,
|
|
1234
|
+
DECRYPT_FACT_TYPE,
|
|
1235
|
+
DECRYPT_RESULT_TYPE,
|
|
1236
|
+
DEFAULT_ENCRYPTION_ALG,
|
|
1237
|
+
DEFAULT_KEY_VERSION,
|
|
1238
|
+
ENCRYPTION_NETWORK_URN_PREFIX,
|
|
1239
|
+
ENCRYPTION_SERVICE,
|
|
1240
|
+
ENCRYPTION_SERVICE_SHORT,
|
|
1241
|
+
ENVELOPE_VERSION,
|
|
1242
|
+
EncryptionService,
|
|
1243
|
+
NETWORK_NAME_PATTERN,
|
|
1244
|
+
NetworkIdError,
|
|
1245
|
+
base64Decode,
|
|
1246
|
+
base64Encode,
|
|
1247
|
+
buildCanonicalDecryptRequest,
|
|
1248
|
+
buildDecryptAttenuation,
|
|
1249
|
+
buildDecryptFacts,
|
|
1250
|
+
buildDecryptInvocation,
|
|
1251
|
+
buildNetworkId,
|
|
1252
|
+
canonicalHashHex,
|
|
1253
|
+
canonicalSignedResponse,
|
|
1254
|
+
canonicalize,
|
|
1255
|
+
checkDecryptInvocationInput,
|
|
1256
|
+
decryptEnvelopeWithKey,
|
|
1257
|
+
deriveSignedReceiverKey,
|
|
1258
|
+
discoverNetwork,
|
|
1259
|
+
encryptToNetwork,
|
|
1260
|
+
encryptionError,
|
|
1261
|
+
ensureNetworkUsableForDecrypt,
|
|
1262
|
+
generateRandomReceiverKey,
|
|
1263
|
+
hexDecode,
|
|
1264
|
+
hexEncode,
|
|
1265
|
+
isNetworkId,
|
|
1266
|
+
networkDiscoveryKey,
|
|
1267
|
+
openWrappedKey,
|
|
1268
|
+
parseNetworkId,
|
|
1269
|
+
utf8Decode,
|
|
1270
|
+
utf8Encode,
|
|
1271
|
+
validateEnvelope,
|
|
1272
|
+
verifyDecryptResponse
|
|
1273
|
+
};
|
|
1274
|
+
//# sourceMappingURL=index.js.map
|