@tomato414941/foundation 0.13.0 → 0.15.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 +16 -16
package/package.json
CHANGED
package/runtime.mjs
CHANGED
|
@@ -105,7 +105,7 @@ Commands:
|
|
|
105
105
|
Send one request to the Foundation API with the key attached.
|
|
106
106
|
exec <ENV>=<name> [...] -- <command> [args...]
|
|
107
107
|
Run a command with saved values in its environment.
|
|
108
|
-
exec --inputs '<json>' -- <command> The same, with files, structured inputs, or a
|
|
108
|
+
exec --inputs '<json>' -- <command> The same, with files, structured inputs, or a connection for a service by id.
|
|
109
109
|
exec --output '<json>' -- <command> Also save a file the command writes.
|
|
110
110
|
version Print the version.
|
|
111
111
|
|
|
@@ -132,15 +132,22 @@ async function main() {
|
|
|
132
132
|
const parsed = parseArgs({ args: args.slice(0, separatorAt), options: { inputs: { type: 'string' }, output: { type: 'string' } }, strict: true, allowPositionals: true });
|
|
133
133
|
if (parsed.values.inputs !== undefined && parsed.positionals.length) throw new Error('--inputs and ENV=name are alternatives.');
|
|
134
134
|
if (parsed.values.inputs !== undefined) {
|
|
135
|
-
try { names = JSON.parse(parsed.values.inputs); } catch { throw new Error('--inputs must be a JSON array of {name, as, filename?}.'); }
|
|
135
|
+
try { names = JSON.parse(parsed.values.inputs); } catch { throw new Error('--inputs must be a JSON array of {name, as, filename?} or {id, output?, as?, filename?}.'); }
|
|
136
136
|
} else names = parsed.positionals.map(value => {
|
|
137
137
|
const at = value.indexOf('=');
|
|
138
138
|
if (at < 1) throw new Error('Specify the environment variable explicitly: ENV=name');
|
|
139
139
|
return { name: value.slice(at + 1), as: value.slice(0, at) };
|
|
140
140
|
});
|
|
141
|
-
// A
|
|
142
|
-
if (!Array.isArray(names) || names.length > 16 || names.some(item =>
|
|
143
|
-
|
|
141
|
+
// A connection for a service names its own variables, so an input may leave `as` out; a secret must say where it goes.
|
|
142
|
+
if (!Array.isArray(names) || names.length > 16 || names.some(item => {
|
|
143
|
+
if (!item || typeof item !== 'object' || Array.isArray(item) || Object.keys(item).some(key => !['name', 'id', 'output', 'as', 'filename'].includes(key))) return true;
|
|
144
|
+
if (Object.hasOwn(item, 'name') === Object.hasOwn(item, 'id')) return true;
|
|
145
|
+
if (Object.hasOwn(item, 'name') && (typeof item.name !== 'string' || !item.name || Object.hasOwn(item, 'output'))) return true;
|
|
146
|
+
if (Object.hasOwn(item, 'id') && (typeof item.id !== 'string' || !/^[0-9a-f-]{36}$/.test(item.id))) return true;
|
|
147
|
+
if (Object.hasOwn(item, 'output') && (typeof item.output !== 'string' || !item.output || item.output.length > 200)) return true;
|
|
148
|
+
return item.as !== undefined && item.as !== null && !validEnvName(item.as);
|
|
149
|
+
})) throw new Error('Each input needs exactly one of name or id. Use output with a connection id; as must be a non-reserved environment variable.');
|
|
150
|
+
const chosen = names.map(item => item.as).filter(value => typeof value === 'string');
|
|
144
151
|
if (new Set(chosen).size !== chosen.length) throw new Error('Each input needs a different environment variable.');
|
|
145
152
|
if (parsed.values.output !== undefined) {
|
|
146
153
|
try { output = JSON.parse(parsed.values.output); } catch { throw new Error('--output must be a JSON object {name, as, filename}.'); }
|
|
@@ -210,7 +217,7 @@ async function main() {
|
|
|
210
217
|
await writeKey(keyPath, made.token, !process.env.FOUNDATION_RUNTIME_KEY_FILE);
|
|
211
218
|
token = made.token; me = null;
|
|
212
219
|
}
|
|
213
|
-
const answer = me?.acts_for?.length ? null : await send('/v1/requests', {
|
|
220
|
+
const answer = me?.acts_for?.length ? null : await send('/v1/requests', { authorization_details: [{ type: 'relation', relation: 'actor' }] });
|
|
214
221
|
if (connectTo !== undefined) await saveUrl(url.origin);
|
|
215
222
|
console.log(answer === null ? 'Already approved on ' + url.origin + '.' : JSON.stringify(answer, null, 2));
|
|
216
223
|
console.log('\nKey file: ' + keyPath + '\nServer: ' + url.origin + (connectTo !== undefined ? ' (saved to ' + configPath() + ')' : '') + '\nEverything else is HTTP: Authorization: Bearer <the contents of that file>');
|
|
@@ -228,13 +235,6 @@ async function main() {
|
|
|
228
235
|
const holder = process.env.FOUNDATION_AS || (acting.length === 1 ? acting[0] : null);
|
|
229
236
|
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.');
|
|
230
237
|
const forHolder = target => holder ? target + (target.includes('?') ? '&' : '?') + 'as=' + encodeURIComponent(holder) : target;
|
|
231
|
-
// A client can be updated before its server. Discover the storage kind before the command creates an output.
|
|
232
|
-
let outputKind = 'secret';
|
|
233
|
-
if (output) {
|
|
234
|
-
const probe = await send(forHolder('/v1/resources?kind=secret&prefix=' + encodeURIComponent(output.name)), undefined,
|
|
235
|
-
{ method: 'GET', accept: data => data.error?.code === 'invalid_kind' });
|
|
236
|
-
if (probe.error?.code === 'invalid_kind') outputKind = 'credential';
|
|
237
|
-
}
|
|
238
238
|
let injection;
|
|
239
239
|
if (names.length) ({ injection } = await send(forHolder('/v1/injections'), { names }));
|
|
240
240
|
else injection = { environment: {}, files: [] };
|
|
@@ -254,7 +254,7 @@ async function main() {
|
|
|
254
254
|
fileNames.add(file.filename); variables.add(file.env);
|
|
255
255
|
}
|
|
256
256
|
if (output && variables.has(output.as)) throw new Error('Output needs a different environment variable from every input.');
|
|
257
|
-
environment.FOUNDATION_NAMES = JSON.stringify(names.map(item => item.name));
|
|
257
|
+
environment.FOUNDATION_NAMES = JSON.stringify(names.map(item => item.name ?? item.id));
|
|
258
258
|
// Injected inputs are always cleaned up. A completed output survives only an unconfirmed upload.
|
|
259
259
|
let secretDir, outputDir, outputPath, child, interrupted = false, retainOutput = false;
|
|
260
260
|
const cleanup = () => {
|
|
@@ -266,7 +266,7 @@ async function main() {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
};
|
|
269
|
-
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=
|
|
269
|
+
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=secret&name=<URL-encoded-name>" --from <file>, then remove that recovery file.';
|
|
270
270
|
process.once('exit', cleanup);
|
|
271
271
|
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.once(signal, () => {
|
|
272
272
|
interrupted = true;
|
|
@@ -305,7 +305,7 @@ async function main() {
|
|
|
305
305
|
retainOutput = true;
|
|
306
306
|
// 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.
|
|
307
307
|
let saved;
|
|
308
|
-
try { saved = await send(forHolder('/v1/resources?kind=
|
|
308
|
+
try { saved = await send(forHolder('/v1/resources?kind=secret&name=' + encodeURIComponent(output.name)), bytes, { method: 'PUT', type: 'application/octet-stream' }); }
|
|
309
309
|
catch { throw new Error(recovery()); }
|
|
310
310
|
try { await send('/v1/relations', { relation: 'editor', object_type: 'resource', object_id: saved.resource.id }, { method: 'DELETE' }); } catch {}
|
|
311
311
|
retainOutput = false;
|