@tomato414941/foundation 0.10.0 → 0.12.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 (3) hide show
  1. package/guide.mjs +8 -3
  2. package/package.json +1 -1
  3. package/runtime.mjs +11 -4
package/guide.mjs CHANGED
@@ -41,10 +41,15 @@ export function guide(services) {
41
41
  ' PATCH /v1/principals/me {"name"} DELETE /v1/principals/me a new name; leaving (your key stops working).',
42
42
  ' GET /v1/requests/<id> one of your requests and what happened at its page (events). DELETE cancels it.',
43
43
  ' GET /v1/relations every line you are on. GET /v1/audit-log what was done in your name or to you.',
44
- ' Every resource has an id, shown in every listing. The holder shows one to another principal with',
45
- ' POST /v1/relations {"subject":"<principal id>","relation":"viewer"|"editor","object_type":"resource","object_id":"<id>"} DELETE takes it back.',
44
+ ' Every resource has an id, shown in every listing. What a principal may do comes from lines drawn to it:',
45
+ ' POST /v1/relations {"subject":"<principal id>","relation":"<role or action>","object_type":"resource"|"principal","object_id":"<id>"}',
46
+ ' A role is viewer or editor onto a resource, or actor onto a principal (acting for it). An action is one the rules',
47
+ ' name, such as credential.disconnect, onto one resource or onto a principal (all it holds). One draws a line where',
48
+ ' one may give lines (share on a resource, relate on a principal) and may do all the line reaches. DELETE takes it',
49
+ ' back; anyone may step off a line drawn to them. Owning a principal is never drawn: it comes from making or approving.',
46
50
  ' GET /v1/resources?shown=me what others have shown you. GET /v1/resources/<id> one resource, whoever holds it.',
47
- ' GET /v1/resources/<id>/content reads it; PUT writes it, as editor. Lines point at the id, so renaming changes nothing.', '',
51
+ ' GET /v1/resources/<id>/content reads it; PUT writes it, as editor. Lines point at the id, so renaming changes nothing.',
52
+ '',
48
53
  'CREDENTIALS',
49
54
  ' GET /v1/resources?kind=credential every credential: id, name, status, and for one made for a service, its',
50
55
  ' service, auth_scheme, facts and the variables it yields. &service=<id> narrows to one service; &secret=true to',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomato414941/foundation",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Foundation CLI: make a key, 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
@@ -199,7 +199,7 @@ async function main() {
199
199
  if (action === 'api') {
200
200
  if (!/[?&]as=/.test(call.target)) {
201
201
  const me = await send('/v1/principals/me', undefined, { method: 'GET', accept: () => true });
202
- if (me.acts_for?.length === 1) call.target += (call.target.includes('?') ? '&' : '?') + 'as=' + encodeURIComponent(me.acts_for[0].id);
202
+ if (me.acts_for?.length === 1) call.target += (call.target.includes('?') ? '&' : '?') + 'as=' + encodeURIComponent(me.acts_for[0]);
203
203
  }
204
204
  const response = await fetch(url.origin + call.target, { method: call.method, headers: { authorization: 'Bearer ' + token, ...(call.body === undefined ? {} : { 'content-type': call.type }) },
205
205
  ...(call.body === undefined ? {} : { body: call.body }), redirect: 'error', signal: AbortSignal.timeout(30_000) });
@@ -239,9 +239,16 @@ async function main() {
239
239
  if (!own && !current.acts_for?.length) throw new Error('Foundation request failed (401, not_approved). This key acts for nobody yet' + (current.requests?.[0] ? '; it is waiting for approval at ' + current.requests[0].verification_uri : '') + '.');
240
240
  // Whose resources a run reaches: the one this key acts for, the one named when it acts for several, or its own.
241
241
  const acting = current.acts_for ?? [];
242
- const holder = process.env.FOUNDATION_AS || (acting.length === 1 ? acting[0].id : null);
242
+ const holder = process.env.FOUNDATION_AS || (acting.length === 1 ? acting[0] : null);
243
243
  if (!holder && 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.');
244
244
  const forHolder = target => holder ? target + (target.includes('?') ? '&' : '?') + 'as=' + encodeURIComponent(holder) : target;
245
+ // A client can be updated before its server. Discover the storage kind before the command creates an output.
246
+ let outputKind = 'secret';
247
+ if (output) {
248
+ const probe = await send(forHolder('/v1/resources?kind=secret&prefix=' + encodeURIComponent(output.name)), undefined,
249
+ { method: 'GET', accept: data => data.error?.code === 'invalid_kind' });
250
+ if (probe.error?.code === 'invalid_kind') outputKind = 'credential';
251
+ }
245
252
  let injection;
246
253
  if (names.length) ({ injection } = await send(forHolder('/v1/injections'), { names }));
247
254
  else injection = { environment: {}, files: [] };
@@ -273,7 +280,7 @@ async function main() {
273
280
  }
274
281
  }
275
282
  };
276
- const recovery = () => 'Foundation could not confirm the output was saved. The private output file is retained for recovery: ' + outputPath + '\nRetry with foundation api PUT "/v1/resources?kind=credential&name=<URL-encoded-name>" --from <file>, then remove that recovery file.';
283
+ const recovery = () => 'Foundation could not confirm the output was saved. The private output file is retained for recovery: ' + outputPath + '\nRetry with foundation api PUT "/v1/resources?kind=' + outputKind + '&name=<URL-encoded-name>" --from <file>, then remove that recovery file.';
277
284
  process.once('exit', cleanup);
278
285
  for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.once(signal, () => {
279
286
  interrupted = true;
@@ -312,7 +319,7 @@ async function main() {
312
319
  retainOutput = true;
313
320
  // 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.
314
321
  let saved;
315
- try { saved = await send(forHolder('/v1/resources?kind=credential&name=' + encodeURIComponent(output.name)), bytes, { method: 'PUT', type: 'application/octet-stream' }); }
322
+ try { saved = await send(forHolder('/v1/resources?kind=' + outputKind + '&name=' + encodeURIComponent(output.name)), bytes, { method: 'PUT', type: 'application/octet-stream' }); }
316
323
  catch { throw new Error(recovery()); }
317
324
  try { await send('/v1/relations', { relation: 'editor', object_type: 'resource', object_id: saved.resource.id }, { method: 'DELETE' }); } catch {}
318
325
  retainOutput = false;