@tomato414941/foundation 0.22.0 → 0.24.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/package.json +1 -1
- package/runtime.mjs +18 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomato414941/foundation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Foundation CLI: give this machine a WebAuthn credential, and hand what is kept to a command without it passing through the agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
package/runtime.mjs
CHANGED
|
@@ -256,10 +256,6 @@ async function main() {
|
|
|
256
256
|
if (action === 'token') { console.log(token); return; }
|
|
257
257
|
// One request, as this machine, and the answer printed as it came. Nothing here knows the endpoints.
|
|
258
258
|
if (action === 'api') {
|
|
259
|
-
if (!publicSpec && !/[?&]as=/.test(call.target)) {
|
|
260
|
-
const me = await send('/v1/principals/me', undefined, { method: 'GET', accept: () => true });
|
|
261
|
-
if (me.acts_for?.length === 1) call.target += (call.target.includes('?') ? '&' : '?') + 'as=' + encodeURIComponent(me.acts_for[0]);
|
|
262
|
-
}
|
|
263
259
|
const response = await fetch(url.origin + call.target, { method: call.method, headers: { ...(token ? { authorization: 'Bearer ' + token } : {}), ...(call.body === undefined ? {} : { 'content-type': call.type }) },
|
|
264
260
|
...(call.body === undefined ? {} : { body: call.body }), redirect: 'error', signal: AbortSignal.timeout(30_000) });
|
|
265
261
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
@@ -304,11 +300,20 @@ async function main() {
|
|
|
304
300
|
console.log(JSON.stringify(answer, null, 2));
|
|
305
301
|
return;
|
|
306
302
|
}
|
|
303
|
+
// What others hold and this machine has a line onto: the things at the far end of the lines it is at the near end of.
|
|
304
|
+
const shownToMe = async () => {
|
|
305
|
+
const things = [];
|
|
306
|
+
for (let after = null; ;) {
|
|
307
|
+
const page = await send('/v1/principals/me/relations?direction=from&limit=200' + (after ? '&after=' + encodeURIComponent(after) : ''), undefined, { method: 'GET', accept: () => true });
|
|
308
|
+
things.push(...(page.relations ?? []).filter(line => line.resource).map(line => line.resource));
|
|
309
|
+
if (!(after = page.next)) return things;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
307
312
|
// Reading a secret this machine was handed an envelope for: its own, or one shown to it along a line.
|
|
308
313
|
if (action === 'read') {
|
|
309
314
|
if (!key.own) throw new Error('This machine has no key of its own here, so nothing sealed for it can be opened.');
|
|
310
|
-
const own = await send('/v1/resources?kind=secret&name=' + encodeURIComponent(call.name), undefined, { method: 'GET', accept: () => true });
|
|
311
|
-
const resource = own.resource ?? (await
|
|
315
|
+
const own = await send('/v1/principals/me/resources?kind=secret&name=' + encodeURIComponent(call.name), undefined, { method: 'GET', accept: () => true });
|
|
316
|
+
const resource = own.resource ?? (await shownToMe()).find(item => item.kind === 'secret' && item.name === call.name);
|
|
312
317
|
if (!resource) throw new Error('No secret named ' + JSON.stringify(call.name) + ' is kept by this machine or shown to it.');
|
|
313
318
|
const kept = await send('/v1/resources/' + resource.id + '/content', undefined, { method: 'GET' });
|
|
314
319
|
if (!kept.envelope) throw new Error('No envelope was made for this machine: it may read about this secret, but was not handed its key.');
|
|
@@ -327,17 +332,17 @@ async function main() {
|
|
|
327
332
|
const acting = current.acts_for ?? [];
|
|
328
333
|
const owner = process.env.FOUNDATION_AS || (acting.length === 1 ? acting[0] : null);
|
|
329
334
|
if (!owner && acting.length > 1) throw new Error('This key acts for several principals. Set FOUNDATION_AS=<principal id> to say which one this run is for.');
|
|
330
|
-
const
|
|
335
|
+
const holder = '/v1/principals/' + encodeURIComponent(owner || 'me');
|
|
331
336
|
// A secret is sealed here, with a key of its own, for each of the owner's recipients: the server keeps what it
|
|
332
337
|
// cannot open. This machine is not among them; it places the bytes and does not read them back.
|
|
333
338
|
const sealedFor = async bytes => {
|
|
334
|
-
const { recipients } = await send(
|
|
339
|
+
const { recipients } = await send(holder + '/recipients', undefined, { method: 'GET' });
|
|
335
340
|
if (!Array.isArray(recipients) || !recipients.length) throw new Error('Nobody can open a secret kept for this owner yet: the owner needs a key, or Foundation needs to act for them.');
|
|
336
341
|
const contentKey = newContentKey();
|
|
337
342
|
return { content: sealContent(contentKey, bytes).toString('base64url'), envelopes: Object.fromEntries(recipients.map(item => [item.principal_id, seal(contentKey, Buffer.from(item.public_key, 'base64url')).toString('base64url')])) };
|
|
338
343
|
};
|
|
339
344
|
if (action === 'keep') {
|
|
340
|
-
const saved = await send(
|
|
345
|
+
const saved = await send(holder + '/resources?kind=secret&name=' + encodeURIComponent(call.name), await sealedFor(call.body), { method: 'PUT' });
|
|
341
346
|
try { await send('/v1/principals/me/relations', { relation: 'editor', object_type: 'resource', object_id: saved.resource.id }, { method: 'DELETE' }); } catch {}
|
|
342
347
|
console.log(JSON.stringify(saved));
|
|
343
348
|
return;
|
|
@@ -348,13 +353,13 @@ async function main() {
|
|
|
348
353
|
const handed = { environment: {}, files: [] };
|
|
349
354
|
let asked = names;
|
|
350
355
|
if (key.own && names.some(item => typeof item.name === 'string')) {
|
|
351
|
-
const shown = owner ?
|
|
356
|
+
const shown = owner ? await shownToMe() : [];
|
|
352
357
|
asked = [];
|
|
353
358
|
for (const item of names) {
|
|
354
359
|
let resource = null;
|
|
355
360
|
if (typeof item.name === 'string') {
|
|
356
361
|
resource = owner ? shown.find(row => row.kind === 'secret' && row.name === item.name && row.owner_id === owner)
|
|
357
|
-
: (await send('/v1/resources?kind=secret&name=' + encodeURIComponent(item.name), undefined, { method: 'GET', accept: () => true })).resource;
|
|
362
|
+
: (await send('/v1/principals/me/resources?kind=secret&name=' + encodeURIComponent(item.name), undefined, { method: 'GET', accept: () => true })).resource;
|
|
358
363
|
}
|
|
359
364
|
const kept = resource ? await send('/v1/resources/' + resource.id + '/content', undefined, { method: 'GET', accept: () => true }) : null;
|
|
360
365
|
if (!kept?.envelope) { asked.push(item); continue; }
|
|
@@ -364,7 +369,7 @@ async function main() {
|
|
|
364
369
|
}
|
|
365
370
|
}
|
|
366
371
|
let injection;
|
|
367
|
-
if (asked.length) ({ injection } = await send(
|
|
372
|
+
if (asked.length) ({ injection } = await send(holder + '/injections', { names: asked }));
|
|
368
373
|
else injection = { environment: {}, files: [] };
|
|
369
374
|
if (!injection || typeof injection.environment !== 'object' || !Array.isArray(injection.files)) throw new Error('Foundation returned an invalid injection.');
|
|
370
375
|
injection = { environment: { ...injection.environment, ...handed.environment }, files: [...injection.files, ...handed.files] };
|
|
@@ -434,7 +439,7 @@ async function main() {
|
|
|
434
439
|
retainOutput = true;
|
|
435
440
|
// The command wrote it; the agent never saw it, and keeps it that way: the line drawn for the one who kept it is declined.
|
|
436
441
|
let saved;
|
|
437
|
-
try { saved = await send(
|
|
442
|
+
try { saved = await send(holder + '/resources?kind=secret&name=' + encodeURIComponent(output.name), await sealedFor(bytes), { method: 'PUT' }); }
|
|
438
443
|
catch { throw new Error(recovery()); }
|
|
439
444
|
try { await send('/v1/principals/me/relations', { relation: 'editor', object_type: 'resource', object_id: saved.resource.id }, { method: 'DELETE' }); } catch {}
|
|
440
445
|
retainOutput = false;
|