@tomato414941/foundation 0.18.0 → 0.22.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 +30 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomato414941/foundation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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
|
@@ -231,9 +231,9 @@ async function main() {
|
|
|
231
231
|
async function upgrade(label) {
|
|
232
232
|
const me = await send('/v1/principals/me', undefined, { method: 'GET' });
|
|
233
233
|
if (me.key?.environment) return;
|
|
234
|
-
const { options } = await send('/v1/
|
|
234
|
+
const { options } = await send('/v1/principals/' + encodeURIComponent(me.principal.id) + '/credentials', { kind: 'webauthn' });
|
|
235
235
|
const made = createCredential(options, url.origin);
|
|
236
|
-
await send('/v1/
|
|
236
|
+
await send('/v1/principals/' + encodeURIComponent(me.principal.id) + '/credentials', { kind: 'webauthn', name: label, credential: made.response }, { method: 'PUT' });
|
|
237
237
|
await writeKey(keyPath, JSON.stringify({ webauthn_credential: made.credential }), !process.env.FOUNDATION_RUNTIME_KEY_FILE);
|
|
238
238
|
key = { credential: made.credential };
|
|
239
239
|
}
|
|
@@ -242,7 +242,7 @@ async function main() {
|
|
|
242
242
|
async function publishKey() {
|
|
243
243
|
if (key.own) return;
|
|
244
244
|
const made = generateKey();
|
|
245
|
-
const published = await send('/v1/key', { public_key: made.publicKey.toString('base64url') }, { method: 'PUT', accept: data => data.error?.code === 'key_exists' });
|
|
245
|
+
const published = await send('/v1/principals/me/key', { public_key: made.publicKey.toString('base64url') }, { method: 'PUT', accept: data => data.error?.code === 'key_exists' });
|
|
246
246
|
if (published.error) return;
|
|
247
247
|
key.own = { private_key: made.privateKey.toString('base64url') };
|
|
248
248
|
await writeKey(keyPath, JSON.stringify({ webauthn_credential: key.credential, key: key.own }), !process.env.FOUNDATION_RUNTIME_KEY_FILE);
|
|
@@ -278,7 +278,7 @@ async function main() {
|
|
|
278
278
|
// No key, or one this server does not know: a WebAuthn credential made here, proven by nobody, makes this machine a
|
|
279
279
|
// principal there, of nobody's - the same call a browser's passkey makes.
|
|
280
280
|
key = null; me = null;
|
|
281
|
-
const post = (method, payload) => fetch(url.origin + '/v1/
|
|
281
|
+
const post = (method, payload) => fetch(url.origin + '/v1/principals', { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload), redirect: 'error', signal: AbortSignal.timeout(30_000) });
|
|
282
282
|
const begun = await post('POST', { kind: 'webauthn', name: wanted });
|
|
283
283
|
const { options, error } = await begun.json();
|
|
284
284
|
if (!begun.ok) throw new Error('Foundation did not begin a credential (' + begun.status + ', ' + (error?.code || 'unknown') + ').');
|
|
@@ -331,21 +331,43 @@ async function main() {
|
|
|
331
331
|
// A secret is sealed here, with a key of its own, for each of the owner's recipients: the server keeps what it
|
|
332
332
|
// cannot open. This machine is not among them; it places the bytes and does not read them back.
|
|
333
333
|
const sealedFor = async bytes => {
|
|
334
|
-
const { recipients } = await send(
|
|
334
|
+
const { recipients } = await send('/v1/principals/' + encodeURIComponent(owner || 'me') + '/recipients', undefined, { method: 'GET' });
|
|
335
335
|
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
336
|
const contentKey = newContentKey();
|
|
337
337
|
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
338
|
};
|
|
339
339
|
if (action === 'keep') {
|
|
340
340
|
const saved = await send(forHolder('/v1/resources?kind=secret&name=' + encodeURIComponent(call.name)), await sealedFor(call.body), { method: 'PUT' });
|
|
341
|
-
try { await send('/v1/relations', { relation: 'editor', object_type: 'resource', object_id: saved.resource.id }, { method: 'DELETE' }); } catch {}
|
|
341
|
+
try { await send('/v1/principals/me/relations', { relation: 'editor', object_type: 'resource', object_id: saved.resource.id }, { method: 'DELETE' }); } catch {}
|
|
342
342
|
console.log(JSON.stringify(saved));
|
|
343
343
|
return;
|
|
344
344
|
}
|
|
345
|
+
// A secret this machine was handed an envelope for is opened here, with its own key: the server keeps what it cannot
|
|
346
|
+
// open, and nobody else has to be able to open it for this machine to use it. Everything else - a connection, a secret
|
|
347
|
+
// not handed to this machine - is asked of the server, which hands over only what Foundation may open for the owner.
|
|
348
|
+
const handed = { environment: {}, files: [] };
|
|
349
|
+
let asked = names;
|
|
350
|
+
if (key.own && names.some(item => typeof item.name === 'string')) {
|
|
351
|
+
const shown = owner ? (await send('/v1/resources?shown=me', undefined, { method: 'GET', accept: () => true })).resources ?? [] : [];
|
|
352
|
+
asked = [];
|
|
353
|
+
for (const item of names) {
|
|
354
|
+
let resource = null;
|
|
355
|
+
if (typeof item.name === 'string') {
|
|
356
|
+
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;
|
|
358
|
+
}
|
|
359
|
+
const kept = resource ? await send('/v1/resources/' + resource.id + '/content', undefined, { method: 'GET', accept: () => true }) : null;
|
|
360
|
+
if (!kept?.envelope) { asked.push(item); continue; }
|
|
361
|
+
const bytes = openContent(openEnvelope(Buffer.from(kept.envelope, 'base64url'), Buffer.from(key.own.private_key, 'base64url')), Buffer.from(kept.content, 'base64url'));
|
|
362
|
+
if (item.filename !== undefined) handed.files.push({ env: item.as, filename: item.filename, content: bytes.toString('base64'), encoding: 'base64' });
|
|
363
|
+
else handed.environment[item.as] = bytes.toString('utf8');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
345
366
|
let injection;
|
|
346
|
-
if (
|
|
367
|
+
if (asked.length) ({ injection } = await send(forHolder('/v1/injections'), { names: asked }));
|
|
347
368
|
else injection = { environment: {}, files: [] };
|
|
348
369
|
if (!injection || typeof injection.environment !== 'object' || !Array.isArray(injection.files)) throw new Error('Foundation returned an invalid injection.');
|
|
370
|
+
injection = { environment: { ...injection.environment, ...handed.environment }, files: [...injection.files, ...handed.files] };
|
|
349
371
|
// What each of them sets is the server's to say; this applies it and refuses anything it may not set.
|
|
350
372
|
const environment = { ...process.env };
|
|
351
373
|
delete environment.FOUNDATION_RUNTIME_KEY_FILE;
|
|
@@ -414,7 +436,7 @@ async function main() {
|
|
|
414
436
|
let saved;
|
|
415
437
|
try { saved = await send(forHolder('/v1/resources?kind=secret&name=' + encodeURIComponent(output.name)), await sealedFor(bytes), { method: 'PUT' }); }
|
|
416
438
|
catch { throw new Error(recovery()); }
|
|
417
|
-
try { await send('/v1/relations', { relation: 'editor', object_type: 'resource', object_id: saved.resource.id }, { method: 'DELETE' }); } catch {}
|
|
439
|
+
try { await send('/v1/principals/me/relations', { relation: 'editor', object_type: 'resource', object_id: saved.resource.id }, { method: 'DELETE' }); } catch {}
|
|
418
440
|
retainOutput = false;
|
|
419
441
|
console.error('Saved output as ' + JSON.stringify(output.name) + '.');
|
|
420
442
|
}
|