@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/runtime.mjs +30 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomato414941/foundation",
3
- "version": "0.18.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/credentials?as=' + encodeURIComponent(me.principal.id), { kind: 'webauthn' });
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/credentials?as=' + encodeURIComponent(me.principal.id), { kind: 'webauthn', name: label, credential: made.response }, { method: 'PUT' });
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/credentials', { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload), redirect: 'error', signal: AbortSignal.timeout(30_000) });
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(forHolder('/v1/recipients'), undefined, { method: 'GET' });
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 (names.length) ({ injection } = await send(forHolder('/v1/injections'), { names }));
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
  }