@tomato414941/foundation 0.9.0 → 0.11.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/guide.mjs +23 -4
- package/package.json +1 -1
- package/runtime.mjs +19 -8
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.
|
|
45
|
-
' POST /v1/relations {"subject":"<principal id>","relation":"
|
|
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',
|
|
@@ -134,8 +139,22 @@ export function guide(services) {
|
|
|
134
139
|
' Poll GET /v1/requests/<id> every few seconds until done; result.credential_id identifies the credential.',
|
|
135
140
|
' DELETE /v1/resources/<id> {"revoke": true|false} disconnects one (the owner, in a browser); revoke also asks the',
|
|
136
141
|
' service to withdraw what it granted. What was already handed out stays where it went.', '',
|
|
142
|
+
'ENVIRONMENTS (a machine with a shell, for when you have none)',
|
|
143
|
+
' A lent machine: a shell, files and the network, thrown away when done. By itself it reaches nothing of Foundation.',
|
|
144
|
+
' Give it an identity - a principal you may act as (yourself, one you own, or one you act for) - and it holds that',
|
|
145
|
+
' principal\'s key for its life: inside, the foundation CLI works as that principal (foundation exec, foundation api).',
|
|
146
|
+
' POST /v1/environments {"identity":"<principal id>"|null, "size":"small|medium|large",',
|
|
147
|
+
' "lifetime":{"end":"idle|exit","idle_seconds":600,"max_seconds":3600}} opens one; it is a resource (kind environment).',
|
|
148
|
+
' POST /v1/environments/<id>/commands {"command":["npm","test"],"stdin":null,"timeout_seconds":300}',
|
|
149
|
+
' runs one command: exit_code, stdout, stderr. 202 with status running if it takes longer; then',
|
|
150
|
+
' GET /v1/environments/<id>/commands/<command id>. One command at a time; files persist between commands.',
|
|
151
|
+
' PATCH /v1/environments/<id> {"identity":… or null} gives or takes away its identity. DELETE closes it.',
|
|
152
|
+
' POST /v1/runs {…as opening, plus "command"} opens, runs one command, stops: the answer carries the result.',
|
|
153
|
+
' What a command prints is cleaned of values handed in with injections. Keep what matters as an object.',
|
|
154
|
+
' Computing is spent: GET /v1/principals/<id>/compute shows this month\'s use and limit; its owner may lower the',
|
|
155
|
+
' limit with PUT {"monthly_seconds": n}. A medium machine spends twice its time, a large one four times.', '',
|
|
137
156
|
'FUNCTIONS',
|
|
138
|
-
' GET /v1/functions catalog of built-in operations and their invocation endpoints
|
|
157
|
+
' GET /v1/functions catalog of built-in operations and their invocation endpoints.', '',
|
|
139
158
|
'WHEN IT DOES NOT WORK',
|
|
140
159
|
' GET /v1/requests/<id> one of your requests, and what happened at its page (events).',
|
|
141
160
|
' GET /v1/requests?status=pending your requests. Several may be open at once (up to 10).',
|
package/package.json
CHANGED
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]
|
|
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) });
|
|
@@ -233,11 +233,22 @@ async function main() {
|
|
|
233
233
|
// Nothing runs before someone has accepted this key: a key that acts for nobody reaches only its own empty resources,
|
|
234
234
|
// and the person it asked has yet to answer.
|
|
235
235
|
const current = await send('/v1/principals/me', undefined, { method: 'GET' });
|
|
236
|
-
|
|
237
|
-
//
|
|
238
|
-
const
|
|
239
|
-
if (!
|
|
240
|
-
|
|
236
|
+
// A key given to a lent machine acts as its principal's own self. Any other key acts for someone once approved;
|
|
237
|
+
// until then, whether waiting or refused, it has nothing to run with.
|
|
238
|
+
const own = Boolean(current.key?.environment);
|
|
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
|
+
// Whose resources a run reaches: the one this key acts for, the one named when it acts for several, or its own.
|
|
241
|
+
const acting = current.acts_for ?? [];
|
|
242
|
+
const holder = process.env.FOUNDATION_AS || (acting.length === 1 ? acting[0] : null);
|
|
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
|
+
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
|
+
}
|
|
241
252
|
let injection;
|
|
242
253
|
if (names.length) ({ injection } = await send(forHolder('/v1/injections'), { names }));
|
|
243
254
|
else injection = { environment: {}, files: [] };
|
|
@@ -269,7 +280,7 @@ async function main() {
|
|
|
269
280
|
}
|
|
270
281
|
}
|
|
271
282
|
};
|
|
272
|
-
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=
|
|
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.';
|
|
273
284
|
process.once('exit', cleanup);
|
|
274
285
|
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.once(signal, () => {
|
|
275
286
|
interrupted = true;
|
|
@@ -308,7 +319,7 @@ async function main() {
|
|
|
308
319
|
retainOutput = true;
|
|
309
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.
|
|
310
321
|
let saved;
|
|
311
|
-
try { saved = await send(forHolder('/v1/resources?kind=
|
|
322
|
+
try { saved = await send(forHolder('/v1/resources?kind=' + outputKind + '&name=' + encodeURIComponent(output.name)), bytes, { method: 'PUT', type: 'application/octet-stream' }); }
|
|
312
323
|
catch { throw new Error(recovery()); }
|
|
313
324
|
try { await send('/v1/relations', { relation: 'editor', object_type: 'resource', object_id: saved.resource.id }, { method: 'DELETE' }); } catch {}
|
|
314
325
|
retainOutput = false;
|