@ekanos/cli 0.15.0 → 0.16.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/README.md +41 -4
- package/dist/auth/ekanos-api.d.ts +23 -0
- package/dist/auth/ekanos-api.js +33 -6
- package/dist/auth/ekanos-api.js.map +1 -1
- package/dist/commands/agents.js +3 -2
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/apps.d.ts +11 -3
- package/dist/commands/apps.js +86 -28
- package/dist/commands/apps.js.map +1 -1
- package/dist/commands/catalog.d.ts +4 -2
- package/dist/commands/catalog.js +166 -35
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +17 -0
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/runs.d.ts +7 -3
- package/dist/commands/runs.js +21 -5
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/scope-notice.d.ts +7 -0
- package/dist/commands/scope-notice.js +4 -11
- package/dist/commands/scope-notice.js.map +1 -1
- package/dist/commands/sources-roles.d.ts +5 -0
- package/dist/commands/sources-roles.js +235 -0
- package/dist/commands/sources-roles.js.map +1 -0
- package/dist/commands/sources.d.ts +5 -0
- package/dist/commands/sources.js +6 -1
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/team-agent-readiness.d.ts +18 -2
- package/dist/commands/team-agent-readiness.js +39 -39
- package/dist/commands/team-agent-readiness.js.map +1 -1
- package/dist/commands/team-agents-agents.d.ts +5 -2
- package/dist/commands/team-agents-agents.js +152 -15
- package/dist/commands/team-agents-agents.js.map +1 -1
- package/dist/commands/team-agents-define.d.ts +31 -0
- package/dist/commands/team-agents-define.js +176 -0
- package/dist/commands/team-agents-define.js.map +1 -0
- package/dist/commands/team-agents-mcp.js +112 -10
- package/dist/commands/team-agents-mcp.js.map +1 -1
- package/dist/commands/team-agents-triggers.d.ts +1 -1
- package/dist/commands/team-agents-triggers.js +137 -5
- package/dist/commands/team-agents-triggers.js.map +1 -1
- package/dist/commands/team-agents.d.ts +4 -1
- package/dist/commands/team-agents.js +17 -4
- package/dist/commands/team-agents.js.map +1 -1
- package/dist/commands/team-llm-key.d.ts +14 -0
- package/dist/commands/team-llm-key.js +122 -0
- package/dist/commands/team-llm-key.js.map +1 -0
- package/dist/commands/teams-connect.js +123 -31
- package/dist/commands/teams-connect.js.map +1 -1
- package/dist/commands/teams-lifecycle.d.ts +90 -0
- package/dist/commands/teams-lifecycle.js +457 -0
- package/dist/commands/teams-lifecycle.js.map +1 -0
- package/dist/commands/teams-onboard.d.ts +14 -0
- package/dist/commands/teams-onboard.js +62 -13
- package/dist/commands/teams-onboard.js.map +1 -1
- package/dist/commands/teams-overview.js +6 -2
- package/dist/commands/teams-overview.js.map +1 -1
- package/dist/commands/teams.d.ts +25 -5
- package/dist/commands/teams.js +101 -39
- package/dist/commands/teams.js.map +1 -1
- package/dist/commands/workflows.d.ts +37 -0
- package/dist/commands/workflows.js +589 -0
- package/dist/commands/workflows.js.map +1 -0
- package/dist/connect-fields.d.ts +22 -0
- package/dist/connect-fields.js +115 -0
- package/dist/connect-fields.js.map +1 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +32 -9
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +357 -122
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +31 -0
- package/dist/invocation.js +58 -0
- package/dist/invocation.js.map +1 -0
- package/dist/runs-api.js +8 -3
- package/dist/runs-api.js.map +1 -1
- package/dist/safe-fs.d.ts +18 -0
- package/dist/safe-fs.js +57 -0
- package/dist/safe-fs.js.map +1 -1
- package/dist/secret-form.d.ts +48 -0
- package/dist/secret-form.js +212 -0
- package/dist/secret-form.js.map +1 -0
- package/dist/secret-input.d.ts +96 -0
- package/dist/secret-input.js +299 -0
- package/dist/secret-input.js.map +1 -0
- package/dist/team-connect-api.d.ts +78 -3
- package/dist/team-connect-api.js +17 -2
- package/dist/team-connect-api.js.map +1 -1
- package/dist/team-lifecycle-api.d.ts +435 -0
- package/dist/team-lifecycle-api.js +301 -0
- package/dist/team-lifecycle-api.js.map +1 -0
- package/dist/teams-api.d.ts +13 -1
- package/dist/teams-api.js +88 -27
- package/dist/teams-api.js.map +1 -1
- package/dist/unknown-command.d.ts +18 -0
- package/dist/unknown-command.js +50 -0
- package/dist/unknown-command.js.map +1 -0
- package/dist/workflows-api.d.ts +69 -0
- package/dist/workflows-api.js +182 -0
- package/dist/workflows-api.js.map +1 -0
- package/package.json +3 -3
- package/templates/AGENTS.md.tmpl +6 -4
- package/templates/claude-skill.md.tmpl +4 -1
- package/templates/package.json.tmpl +4 -4
- package/templates/skills/build-agent/SKILL.md +43 -10
- package/templates/skills/build-agent/references/agent-json.md +4 -1
- package/templates/skills/build-agent/references/app-setup.md +14 -7
- package/templates/skills/build-agent/references/rollout.md +26 -2
- package/templates/skills/build-agent/references/set-up-an-integration.md +46 -29
- package/templates/skills/build-integration/references/widget-design.md +1 -1
- package/templates/skills/ekanos-platform/SKILL.md +66 -14
- package/templates/skills/polish-widgets/references/patterns.md +3 -3
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { CliContext } from './context.js';
|
|
2
|
+
import { type SecretFormField } from './secret-form.js';
|
|
3
|
+
/**
|
|
4
|
+
* How every command that takes a secret gets it — `apps set`, `apps
|
|
5
|
+
* connect`, `teams app set`, `teams connect`, `teams mcp token|add`,
|
|
6
|
+
* `teams llm-key set` — so a coding agent or a script can run them as well
|
|
7
|
+
* as a person at a terminal, and none of them ever has the secret in argv.
|
|
8
|
+
*
|
|
9
|
+
* In order:
|
|
10
|
+
*
|
|
11
|
+
* 1. **`--browser`**: a one-shot form on this computer (`secret-form.ts`).
|
|
12
|
+
* The person types it in their browser; the run that asked never sees it.
|
|
13
|
+
* 2. **A named source**: `--secret-env NAME`, `--secret-file PATH` (a
|
|
14
|
+
* 0600 file of yours, never through a symlink) or `--secret-stdin` (piped)
|
|
15
|
+
* for the one secret; `--set-secret <field>=env:NAME|file:PATH|stdin` for
|
|
16
|
+
* each field of a form. Nothing else is asked.
|
|
17
|
+
* 3. **A terminal**: hidden input, as before.
|
|
18
|
+
* 4. Otherwise a usage error naming the forms above.
|
|
19
|
+
*
|
|
20
|
+
* A literal (`--set-secret field=sk-…`) is refused: argv lands in shell
|
|
21
|
+
* history and the process list. No error, log or output here ever carries a
|
|
22
|
+
* value — not even a path or a name that might be one by mistake.
|
|
23
|
+
*
|
|
24
|
+
* `--browser` is never chosen for you. A run with no terminal is usually an
|
|
25
|
+
* agent or CI, and the CLI cannot tell whether anyone is at the desktop: a
|
|
26
|
+
* form nobody fills would stall the run for its whole timeout. So it is
|
|
27
|
+
* explicit, refused up front where it cannot work (CI, SSH, no display),
|
|
28
|
+
* and bounded by a timeout everywhere.
|
|
29
|
+
*/
|
|
30
|
+
export interface SecretFlags {
|
|
31
|
+
/** `--secret-stdin` */
|
|
32
|
+
stdin: boolean;
|
|
33
|
+
/** `--secret-env <NAME>` */
|
|
34
|
+
env?: string;
|
|
35
|
+
/** `--secret-file <path>` */
|
|
36
|
+
file?: string;
|
|
37
|
+
/** `--set-secret <field>=env:NAME|file:PATH|stdin`, repeatable. */
|
|
38
|
+
sets: string[];
|
|
39
|
+
/** `--browser` */
|
|
40
|
+
browser: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** The flags, from a command's parsed flags. */
|
|
43
|
+
export declare function readSecretFlags(flags: Record<string, string | boolean | string[]> | undefined): SecretFlags;
|
|
44
|
+
/** Whether any way of giving a secret without a terminal was passed. */
|
|
45
|
+
export declare function secretInputGiven(flags: SecretFlags): boolean;
|
|
46
|
+
/** The forms, as a hint names them. */
|
|
47
|
+
export declare const SINGLE_SECRET_FORMS = "--secret-env <NAME>, --secret-file <path> (a file only you can read), --secret-stdin (piped in), or --browser (a form in your browser on this computer)";
|
|
48
|
+
export declare const FIELD_SECRET_FORMS = "--set-secret <field>=env:<NAME>|file:<path>|stdin for each secret (--secret-env, --secret-file or --secret-stdin when there is one), or --browser (a form in your browser on this computer)";
|
|
49
|
+
export interface SecretIO {
|
|
50
|
+
env: Record<string, string | undefined>;
|
|
51
|
+
stdinIsTTY: boolean;
|
|
52
|
+
/** Test seam: the whole of stdin. */
|
|
53
|
+
readStdin?: () => Promise<string>;
|
|
54
|
+
/** Test seam: the browser opener for `--browser`. */
|
|
55
|
+
openUrl?: (url: string) => void;
|
|
56
|
+
/** Test seam: how long `--browser` waits. */
|
|
57
|
+
formTimeoutMs?: number;
|
|
58
|
+
/** Test seam: the platform `--browser` checks for a desktop. */
|
|
59
|
+
platform?: NodeJS.Platform;
|
|
60
|
+
/** Where a relative `--secret-file` resolves (default: the working directory). */
|
|
61
|
+
cwd?: string;
|
|
62
|
+
}
|
|
63
|
+
export type SecretField = SecretFormField;
|
|
64
|
+
export interface CollectSecretsInput {
|
|
65
|
+
/** Every field this run would ask, in order. */
|
|
66
|
+
fields: readonly SecretField[];
|
|
67
|
+
flags: SecretFlags;
|
|
68
|
+
io: SecretIO;
|
|
69
|
+
/** A human at a terminal (not `--json`, stdin a TTY). */
|
|
70
|
+
interactive: boolean;
|
|
71
|
+
/** The terminal question for one field — hidden for a secret. */
|
|
72
|
+
prompt: (field: SecretField) => Promise<string>;
|
|
73
|
+
/** The secure form's heading. */
|
|
74
|
+
title: string;
|
|
75
|
+
/** What is needed, when nothing can be asked: the refusal's message. */
|
|
76
|
+
refusal: string;
|
|
77
|
+
/** The command, without `npx @ekanos/cli`, for hints. */
|
|
78
|
+
again: string;
|
|
79
|
+
/**
|
|
80
|
+
* `--set <field>=<value>` for a field that is not a secret (a subdomain, a
|
|
81
|
+
* client ID): taken as given, and never asked. A secret here is refused.
|
|
82
|
+
*/
|
|
83
|
+
plain?: Record<string, string>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `--set <field>=<value>` pairs. A malformed one is refused without echoing
|
|
87
|
+
* it — it may be a secret typed where a field name was meant.
|
|
88
|
+
*/
|
|
89
|
+
export declare function parsePlainValues(raw: string | boolean | string[] | undefined, example?: string): Record<string, string>;
|
|
90
|
+
/**
|
|
91
|
+
* Each field's value by name; a field left empty is left out (its saved
|
|
92
|
+
* value stays). Refuses a required field that is neither saved nor given.
|
|
93
|
+
*/
|
|
94
|
+
export declare function collectSecrets(ctx: CliContext, input: CollectSecretsInput): Promise<Record<string, string>>;
|
|
95
|
+
/** Why a browser form cannot be filled here, or null when it can. */
|
|
96
|
+
export declare function desktopProblem(env: Record<string, string | undefined>, platform: NodeJS.Platform): string | null;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { notFoundError, preconditionError, usageError, validationError, } from './errors.js';
|
|
3
|
+
import { openInBrowser } from './open-url.js';
|
|
4
|
+
import { readPrivateFileNoFollow } from './safe-fs.js';
|
|
5
|
+
import { collectWithSecretForm } from './secret-form.js';
|
|
6
|
+
/** The flags, from a command's parsed flags. */
|
|
7
|
+
export function readSecretFlags(flags) {
|
|
8
|
+
const text = (name) => {
|
|
9
|
+
const value = flags === null || flags === void 0 ? void 0 : flags[name];
|
|
10
|
+
return typeof value === 'string' ? value : undefined;
|
|
11
|
+
};
|
|
12
|
+
const sets = flags === null || flags === void 0 ? void 0 : flags['set-secret'];
|
|
13
|
+
return {
|
|
14
|
+
stdin: (flags === null || flags === void 0 ? void 0 : flags['secret-stdin']) === true,
|
|
15
|
+
env: text('secret-env'),
|
|
16
|
+
file: text('secret-file'),
|
|
17
|
+
sets: Array.isArray(sets) ? sets : typeof sets === 'string' ? [sets] : [],
|
|
18
|
+
browser: (flags === null || flags === void 0 ? void 0 : flags.browser) === true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Whether any way of giving a secret without a terminal was passed. */
|
|
22
|
+
export function secretInputGiven(flags) {
|
|
23
|
+
return (flags.browser ||
|
|
24
|
+
flags.stdin ||
|
|
25
|
+
flags.env !== undefined ||
|
|
26
|
+
flags.file !== undefined ||
|
|
27
|
+
flags.sets.length > 0);
|
|
28
|
+
}
|
|
29
|
+
/** The forms, as a hint names them. */
|
|
30
|
+
export const SINGLE_SECRET_FORMS = '--secret-env <NAME>, --secret-file <path> (a file only you can read), --secret-stdin (piped in), or --browser (a form in your browser on this computer)';
|
|
31
|
+
export const FIELD_SECRET_FORMS = '--set-secret <field>=env:<NAME>|file:<path>|stdin for each secret (--secret-env, --secret-file or --secret-stdin when there is one), or --browser (a form in your browser on this computer)';
|
|
32
|
+
/**
|
|
33
|
+
* `--set <field>=<value>` pairs. A malformed one is refused without echoing
|
|
34
|
+
* it — it may be a secret typed where a field name was meant.
|
|
35
|
+
*/
|
|
36
|
+
export function parsePlainValues(raw, example = 'subdomain=acme') {
|
|
37
|
+
const pairs = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];
|
|
38
|
+
const values = {};
|
|
39
|
+
for (const pair of pairs) {
|
|
40
|
+
const at = pair.indexOf('=');
|
|
41
|
+
const name = at > 0 ? pair.slice(0, at).trim() : '';
|
|
42
|
+
if (!name) {
|
|
43
|
+
throw usageError('--set takes <field>=<value>, for a field that is not a secret.', `For example: --set ${example}. A secret is never passed as an argument: use --set-secret <field>=env:<NAME>|file:<path>|stdin.`);
|
|
44
|
+
}
|
|
45
|
+
values[name] = pair.slice(at + 1).trim();
|
|
46
|
+
}
|
|
47
|
+
return values;
|
|
48
|
+
}
|
|
49
|
+
const MAX_SECRET_BYTES = 64 * 1024;
|
|
50
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
51
|
+
const FIELD_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
52
|
+
/**
|
|
53
|
+
* Each field's value by name; a field left empty is left out (its saved
|
|
54
|
+
* value stays). Refuses a required field that is neither saved nor given.
|
|
55
|
+
*/
|
|
56
|
+
export async function collectSecrets(ctx, input) {
|
|
57
|
+
var _a;
|
|
58
|
+
const { flags } = input;
|
|
59
|
+
const plain = checkPlain(input.fields, (_a = input.plain) !== null && _a !== void 0 ? _a : {});
|
|
60
|
+
// What is left to ask: every field `--set` did not give.
|
|
61
|
+
const fields = input.fields.filter((field) => plain[field.name] === undefined);
|
|
62
|
+
const singles = [
|
|
63
|
+
flags.stdin,
|
|
64
|
+
flags.env !== undefined,
|
|
65
|
+
flags.file !== undefined,
|
|
66
|
+
].filter(Boolean).length;
|
|
67
|
+
if (singles > 1) {
|
|
68
|
+
throw usageError('Pass one of --secret-env, --secret-file and --secret-stdin, not several.', `Each names where the one secret comes from. For several, use --set-secret <field>=… per field.`);
|
|
69
|
+
}
|
|
70
|
+
if (flags.browser && (singles > 0 || flags.sets.length > 0)) {
|
|
71
|
+
throw usageError('--browser asks for every field in the form; it does not mix with --secret-* or --set-secret.', 'Drop --browser to pass them from the environment, a file or stdin, or drop those to use the form.');
|
|
72
|
+
}
|
|
73
|
+
const rest = Object.assign(Object.assign({}, input), { fields });
|
|
74
|
+
let values;
|
|
75
|
+
if (fields.length === 0) {
|
|
76
|
+
values = {};
|
|
77
|
+
}
|
|
78
|
+
else if (flags.browser) {
|
|
79
|
+
values = await viaBrowser(ctx, rest);
|
|
80
|
+
}
|
|
81
|
+
else if (singles > 0 || flags.sets.length > 0) {
|
|
82
|
+
values = await viaSources(rest);
|
|
83
|
+
}
|
|
84
|
+
else if (input.interactive) {
|
|
85
|
+
values = {};
|
|
86
|
+
for (const field of fields) {
|
|
87
|
+
const value = (await input.prompt(field)).trim();
|
|
88
|
+
if (value)
|
|
89
|
+
values[field.name] = value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (Object.keys(plain).length > 0) {
|
|
93
|
+
// Only non-secret values were given: the rest keep what is saved.
|
|
94
|
+
values = {};
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
throw usageError(input.refusal, `Without a terminal, pass ${fields.filter((field) => field.sensitive).length > 1 ? FIELD_SECRET_FORMS : SINGLE_SECRET_FORMS}. Or run it yourself in your terminal: npx @ekanos/cli ${input.again}`);
|
|
98
|
+
}
|
|
99
|
+
const missing = fields.filter((field) => field.required && !field.set && !values[field.name]);
|
|
100
|
+
if (missing.length > 0) {
|
|
101
|
+
throw validationError(`${missing.map((field) => field.label).join(', ')} ${missing.length === 1 ? 'is' : 'are'} needed; nothing was saved.`, flags.browser || input.interactive
|
|
102
|
+
? `Run "ekanos ${input.again}" again and fill ${missing.length === 1 ? 'it' : 'them'} in.`
|
|
103
|
+
: `Pass ${missing.map((field) => (field.sensitive ? `--set-secret ${field.name}=env:<NAME>` : `--set ${field.name}=<value>`)).join(' ')}.`);
|
|
104
|
+
}
|
|
105
|
+
return Object.assign(Object.assign({}, plainGiven(plain)), values);
|
|
106
|
+
}
|
|
107
|
+
/** `--set` values, checked against the fields: known, and never a secret. */
|
|
108
|
+
function checkPlain(fields, plain) {
|
|
109
|
+
const byName = new Map(fields.map((field) => [field.name, field]));
|
|
110
|
+
for (const name of Object.keys(plain)) {
|
|
111
|
+
const field = byName.get(name);
|
|
112
|
+
const shown = FIELD_NAME.test(name) ? `"${name}"` : 'That field';
|
|
113
|
+
if (!field) {
|
|
114
|
+
throw usageError(`${shown} is not a field this asks for.`, `Its fields are: ${fields.map((entry) => entry.name).join(', ') || 'none'}.`);
|
|
115
|
+
}
|
|
116
|
+
if (field.sensitive) {
|
|
117
|
+
throw usageError(`${shown} is a secret: it is never passed as an argument.`, `Pass it as --set-secret ${name}=env:<NAME>|file:<path>|stdin, or use --browser.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return plain;
|
|
121
|
+
}
|
|
122
|
+
/** An empty `--set name=` means nothing to change, as Enter does at a prompt. */
|
|
123
|
+
function plainGiven(plain) {
|
|
124
|
+
return Object.fromEntries(Object.entries(plain).filter(([, value]) => value !== ''));
|
|
125
|
+
}
|
|
126
|
+
async function viaBrowser(ctx, input) {
|
|
127
|
+
var _a, _b;
|
|
128
|
+
const problem = desktopProblem(input.io.env, (_a = input.io.platform) !== null && _a !== void 0 ? _a : process.platform);
|
|
129
|
+
if (problem) {
|
|
130
|
+
throw preconditionError(`--browser needs a desktop to open a form on: ${problem}`, `Pass it as ${input.fields.filter((field) => field.sensitive).length > 1 ? '--set-secret <field>=env:<NAME>|file:<path>|stdin' : '--secret-env <NAME>, --secret-file <path> or --secret-stdin'} instead.`);
|
|
131
|
+
}
|
|
132
|
+
ctx.log(`Opened a secure form in your browser for ${input.title}… (waiting; if no browser opened, re-run with --secret-env, --secret-file or --secret-stdin)`);
|
|
133
|
+
return collectWithSecretForm({
|
|
134
|
+
title: input.title,
|
|
135
|
+
fields: input.fields,
|
|
136
|
+
open: (_b = input.io.openUrl) !== null && _b !== void 0 ? _b : openInBrowser,
|
|
137
|
+
timeoutMs: input.io.formTimeoutMs,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** Why a browser form cannot be filled here, or null when it can. */
|
|
141
|
+
export function desktopProblem(env, platform) {
|
|
142
|
+
var _a;
|
|
143
|
+
const ci = (_a = env.CI) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
|
|
144
|
+
if (ci && ci !== '0' && ci !== 'false') {
|
|
145
|
+
return 'this is a CI run (CI is set), with no one to fill it in.';
|
|
146
|
+
}
|
|
147
|
+
if (env.SSH_CONNECTION || env.SSH_TTY) {
|
|
148
|
+
return 'this is an SSH session, and a browser on this machine is not in front of you.';
|
|
149
|
+
}
|
|
150
|
+
if (platform !== 'darwin' &&
|
|
151
|
+
platform !== 'win32' &&
|
|
152
|
+
!env.DISPLAY &&
|
|
153
|
+
!env.WAYLAND_DISPLAY) {
|
|
154
|
+
return 'there is no desktop session (DISPLAY or WAYLAND_DISPLAY) to open a browser in.';
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
async function viaSources(input) {
|
|
159
|
+
var _a;
|
|
160
|
+
var _b;
|
|
161
|
+
const { flags, fields } = input;
|
|
162
|
+
const byName = new Map(fields.map((field) => [field.name, field]));
|
|
163
|
+
const specs = new Map();
|
|
164
|
+
for (const entry of flags.sets) {
|
|
165
|
+
const at = entry.indexOf('=');
|
|
166
|
+
const name = at > 0 ? entry.slice(0, at).trim() : '';
|
|
167
|
+
if (!name) {
|
|
168
|
+
throw usageError('--set-secret takes <field>=env:<NAME>, <field>=file:<path> or <field>=stdin.', `For example: --set-secret ${(_b = (_a = fields.find((field) => field.sensitive)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : 'client_secret'}=env:MY_SECRET.`);
|
|
169
|
+
}
|
|
170
|
+
const shown = FIELD_NAME.test(name) ? `"${name}"` : 'that field';
|
|
171
|
+
const field = byName.get(name);
|
|
172
|
+
if (!field) {
|
|
173
|
+
throw usageError(`${shown} is not a field this asks for.`, `Its fields are: ${fields.map((entry) => entry.name).join(', ') || 'none'}.`);
|
|
174
|
+
}
|
|
175
|
+
if (specs.has(name)) {
|
|
176
|
+
throw usageError(`--set-secret names ${shown} twice.`, 'Pass each field once.');
|
|
177
|
+
}
|
|
178
|
+
specs.set(name, parseSpec(entry.slice(at + 1), `--set-secret ${name}`));
|
|
179
|
+
}
|
|
180
|
+
const single = flags.stdin
|
|
181
|
+
? { kind: 'stdin' }
|
|
182
|
+
: flags.env !== undefined
|
|
183
|
+
? envSpec(flags.env, '--secret-env')
|
|
184
|
+
: flags.file !== undefined
|
|
185
|
+
? { kind: 'file', path: flags.file }
|
|
186
|
+
: null;
|
|
187
|
+
if (single) {
|
|
188
|
+
const secrets = fields.filter((field) => field.sensitive && !specs.has(field.name));
|
|
189
|
+
if (secrets.length !== 1) {
|
|
190
|
+
throw usageError(secrets.length === 0
|
|
191
|
+
? 'There is no secret here for --secret-env, --secret-file or --secret-stdin to fill.'
|
|
192
|
+
: `This asks for ${secrets.length} secrets (${secrets.map((field) => field.name).join(', ')}), so name each one.`, secrets.length === 0
|
|
193
|
+
? 'Drop the flag.'
|
|
194
|
+
: `Pass --set-secret <field>=env:<NAME>|file:<path>|stdin for each, e.g. --set-secret ${secrets[0].name}=env:MY_SECRET.`);
|
|
195
|
+
}
|
|
196
|
+
specs.set(secrets[0].name, single);
|
|
197
|
+
}
|
|
198
|
+
if ([...specs.values()].filter((spec) => spec.kind === 'stdin').length > 1) {
|
|
199
|
+
throw usageError('Only one secret can come from stdin.', 'Take the others from the environment (env:<NAME>) or a file (file:<path>).');
|
|
200
|
+
}
|
|
201
|
+
const values = {};
|
|
202
|
+
for (const [name, spec] of specs) {
|
|
203
|
+
const value = (await resolveSpec(spec, name, input.io)).trim();
|
|
204
|
+
if (!value) {
|
|
205
|
+
throw preconditionError(`The value for ${name} (from ${describe(spec)}) is empty; nothing was saved.`, 'Put the secret there, then run the command again.');
|
|
206
|
+
}
|
|
207
|
+
values[name] = value;
|
|
208
|
+
}
|
|
209
|
+
return values;
|
|
210
|
+
}
|
|
211
|
+
function parseSpec(raw, flag) {
|
|
212
|
+
if (raw === 'stdin')
|
|
213
|
+
return { kind: 'stdin' };
|
|
214
|
+
if (raw.startsWith('env:'))
|
|
215
|
+
return envSpec(raw.slice(4), flag);
|
|
216
|
+
if (raw.startsWith('file:') && raw.length > 5) {
|
|
217
|
+
return { kind: 'file', path: raw.slice(5) };
|
|
218
|
+
}
|
|
219
|
+
throw usageError(`${flag}=… is not env:<NAME>, file:<path> or stdin. A secret is never passed as an argument: it would land in your shell history and the process list.`, `Put it in an environment variable (${flag}=env:MY_SECRET), a file only you can read (${flag}=file:./secret.txt), or pipe it in (${flag}=stdin).`);
|
|
220
|
+
}
|
|
221
|
+
function envSpec(name, flag) {
|
|
222
|
+
if (ENV_NAME.test(name))
|
|
223
|
+
return { kind: 'env', name };
|
|
224
|
+
// Never echoed: a value that is not a name may be the secret itself.
|
|
225
|
+
throw usageError(`${flag} takes the NAME of an environment variable (letters, digits and _), never the secret itself.`, `For example: MY_SECRET=… then ${flag}${flag.startsWith('--set-secret') ? '=env:' : ' '}MY_SECRET.`);
|
|
226
|
+
}
|
|
227
|
+
function describe(spec) {
|
|
228
|
+
if (spec.kind === 'env')
|
|
229
|
+
return `$${spec.name}`;
|
|
230
|
+
if (spec.kind === 'file')
|
|
231
|
+
return 'the file';
|
|
232
|
+
return 'stdin';
|
|
233
|
+
}
|
|
234
|
+
async function resolveSpec(spec, field, io) {
|
|
235
|
+
if (spec.kind === 'env') {
|
|
236
|
+
const value = io.env[spec.name];
|
|
237
|
+
if (value === undefined || value === '') {
|
|
238
|
+
throw preconditionError(`The environment variable ${spec.name} (for ${field}) is not set; nothing was saved.`, `Set it in the environment the command runs in, then run it again.`);
|
|
239
|
+
}
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
242
|
+
if (spec.kind === 'file')
|
|
243
|
+
return readSecretFile(spec.path, field, io.cwd);
|
|
244
|
+
return readStdinSecret(io);
|
|
245
|
+
}
|
|
246
|
+
function readSecretFile(given, field, cwd = process.cwd()) {
|
|
247
|
+
var _a;
|
|
248
|
+
const result = readPrivateFileNoFollow(path.resolve(cwd, given), MAX_SECRET_BYTES);
|
|
249
|
+
if (result.ok)
|
|
250
|
+
return stripNewline(result.bytes.toString('utf8'));
|
|
251
|
+
// The path is not echoed: a mistyped flag may have put the secret there.
|
|
252
|
+
const what = `The file given for ${field}`;
|
|
253
|
+
switch (result.problem) {
|
|
254
|
+
case 'missing':
|
|
255
|
+
throw notFoundError(`${what} does not exist; nothing was saved.`, 'Check the path, then run the command again.');
|
|
256
|
+
case 'symlink':
|
|
257
|
+
throw preconditionError(`${what} is a symlink, and the CLI never reads a secret through one.`, 'Name the real file instead.');
|
|
258
|
+
case 'not-a-file':
|
|
259
|
+
throw preconditionError(`${what} is not a regular file.`, 'Name a file holding just the secret.');
|
|
260
|
+
case 'readable-by-others':
|
|
261
|
+
throw preconditionError(`${what} can be read by other users on this machine (mode ${((_a = result.mode) !== null && _a !== void 0 ? _a : 0).toString(8).padStart(3, '0')}), so it was not read.`, 'Make it yours alone with "chmod 600 <file>", then run the command again.');
|
|
262
|
+
case 'not-yours':
|
|
263
|
+
throw preconditionError(`${what} belongs to another user, so it was not read.`, 'Use a file you own, readable only by you (chmod 600).');
|
|
264
|
+
case 'too-large':
|
|
265
|
+
throw preconditionError(`${what} is larger than a secret (over 64 KB).`, 'Name a file holding just the secret.');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function readStdinSecret(io) {
|
|
269
|
+
var _a;
|
|
270
|
+
if (io.stdinIsTTY) {
|
|
271
|
+
throw usageError('stdin is a terminal, so there is no piped secret to read.', 'Pipe it in (e.g. `printf %s "$MY_SECRET" | ekanos …`), or use --secret-env or --secret-file.');
|
|
272
|
+
}
|
|
273
|
+
const raw = await ((_a = io.readStdin) !== null && _a !== void 0 ? _a : readAllStdin)();
|
|
274
|
+
return stripNewline(raw);
|
|
275
|
+
}
|
|
276
|
+
function readAllStdin() {
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const chunks = [];
|
|
279
|
+
let size = 0;
|
|
280
|
+
const stdin = process.stdin;
|
|
281
|
+
stdin.on('data', (chunk) => {
|
|
282
|
+
const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
283
|
+
size += buffer.length;
|
|
284
|
+
if (size > MAX_SECRET_BYTES) {
|
|
285
|
+
stdin.destroy();
|
|
286
|
+
reject(usageError('stdin holds more than a secret (over 64 KB).', 'Pipe in just the secret.'));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
chunks.push(buffer);
|
|
290
|
+
});
|
|
291
|
+
stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
292
|
+
stdin.on('error', reject);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/** One trailing newline (`echo`, an editor's final line) is not part of it. */
|
|
296
|
+
function stripNewline(value) {
|
|
297
|
+
return value.replace(/\r?\n$/, '');
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=secret-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-input.js","sourceRoot":"","sources":["../src/secret-input.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'node:path';\n\nimport type { CliContext } from './context';\nimport {\n notFoundError,\n preconditionError,\n usageError,\n validationError,\n} from './errors';\nimport { openInBrowser } from './open-url';\nimport { readPrivateFileNoFollow } from './safe-fs';\nimport { type SecretFormField, collectWithSecretForm } from './secret-form';\n\n/**\n * How every command that takes a secret gets it — `apps set`, `apps\n * connect`, `teams app set`, `teams connect`, `teams mcp token|add`,\n * `teams llm-key set` — so a coding agent or a script can run them as well\n * as a person at a terminal, and none of them ever has the secret in argv.\n *\n * In order:\n *\n * 1. **`--browser`**: a one-shot form on this computer (`secret-form.ts`).\n * The person types it in their browser; the run that asked never sees it.\n * 2. **A named source**: `--secret-env NAME`, `--secret-file PATH` (a\n * 0600 file of yours, never through a symlink) or `--secret-stdin` (piped)\n * for the one secret; `--set-secret <field>=env:NAME|file:PATH|stdin` for\n * each field of a form. Nothing else is asked.\n * 3. **A terminal**: hidden input, as before.\n * 4. Otherwise a usage error naming the forms above.\n *\n * A literal (`--set-secret field=sk-…`) is refused: argv lands in shell\n * history and the process list. No error, log or output here ever carries a\n * value — not even a path or a name that might be one by mistake.\n *\n * `--browser` is never chosen for you. A run with no terminal is usually an\n * agent or CI, and the CLI cannot tell whether anyone is at the desktop: a\n * form nobody fills would stall the run for its whole timeout. So it is\n * explicit, refused up front where it cannot work (CI, SSH, no display),\n * and bounded by a timeout everywhere.\n */\n\nexport interface SecretFlags {\n /** `--secret-stdin` */\n stdin: boolean;\n /** `--secret-env <NAME>` */\n env?: string;\n /** `--secret-file <path>` */\n file?: string;\n /** `--set-secret <field>=env:NAME|file:PATH|stdin`, repeatable. */\n sets: string[];\n /** `--browser` */\n browser: boolean;\n}\n\n/** The flags, from a command's parsed flags. */\nexport function readSecretFlags(\n flags: Record<string, string | boolean | string[]> | undefined,\n): SecretFlags {\n const text = (name: string) => {\n const value = flags?.[name];\n return typeof value === 'string' ? value : undefined;\n };\n const sets = flags?.['set-secret'];\n return {\n stdin: flags?.['secret-stdin'] === true,\n env: text('secret-env'),\n file: text('secret-file'),\n sets: Array.isArray(sets) ? sets : typeof sets === 'string' ? [sets] : [],\n browser: flags?.browser === true,\n };\n}\n\n/** Whether any way of giving a secret without a terminal was passed. */\nexport function secretInputGiven(flags: SecretFlags): boolean {\n return (\n flags.browser ||\n flags.stdin ||\n flags.env !== undefined ||\n flags.file !== undefined ||\n flags.sets.length > 0\n );\n}\n\n/** The forms, as a hint names them. */\nexport const SINGLE_SECRET_FORMS =\n '--secret-env <NAME>, --secret-file <path> (a file only you can read), --secret-stdin (piped in), or --browser (a form in your browser on this computer)';\nexport const FIELD_SECRET_FORMS =\n '--set-secret <field>=env:<NAME>|file:<path>|stdin for each secret (--secret-env, --secret-file or --secret-stdin when there is one), or --browser (a form in your browser on this computer)';\n\nexport interface SecretIO {\n env: Record<string, string | undefined>;\n stdinIsTTY: boolean;\n /** Test seam: the whole of stdin. */\n readStdin?: () => Promise<string>;\n /** Test seam: the browser opener for `--browser`. */\n openUrl?: (url: string) => void;\n /** Test seam: how long `--browser` waits. */\n formTimeoutMs?: number;\n /** Test seam: the platform `--browser` checks for a desktop. */\n platform?: NodeJS.Platform;\n /** Where a relative `--secret-file` resolves (default: the working directory). */\n cwd?: string;\n}\n\nexport type SecretField = SecretFormField;\n\nexport interface CollectSecretsInput {\n /** Every field this run would ask, in order. */\n fields: readonly SecretField[];\n flags: SecretFlags;\n io: SecretIO;\n /** A human at a terminal (not `--json`, stdin a TTY). */\n interactive: boolean;\n /** The terminal question for one field — hidden for a secret. */\n prompt: (field: SecretField) => Promise<string>;\n /** The secure form's heading. */\n title: string;\n /** What is needed, when nothing can be asked: the refusal's message. */\n refusal: string;\n /** The command, without `npx @ekanos/cli`, for hints. */\n again: string;\n /**\n * `--set <field>=<value>` for a field that is not a secret (a subdomain, a\n * client ID): taken as given, and never asked. A secret here is refused.\n */\n plain?: Record<string, string>;\n}\n\n/**\n * `--set <field>=<value>` pairs. A malformed one is refused without echoing\n * it — it may be a secret typed where a field name was meant.\n */\nexport function parsePlainValues(\n raw: string | boolean | string[] | undefined,\n example = 'subdomain=acme',\n): Record<string, string> {\n const pairs = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];\n const values: Record<string, string> = {};\n for (const pair of pairs) {\n const at = pair.indexOf('=');\n const name = at > 0 ? pair.slice(0, at).trim() : '';\n if (!name) {\n throw usageError(\n '--set takes <field>=<value>, for a field that is not a secret.',\n `For example: --set ${example}. A secret is never passed as an argument: use --set-secret <field>=env:<NAME>|file:<path>|stdin.`,\n );\n }\n values[name] = pair.slice(at + 1).trim();\n }\n return values;\n}\n\nconst MAX_SECRET_BYTES = 64 * 1024;\nconst ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst FIELD_NAME = /^[A-Za-z0-9_.-]{1,64}$/;\n\ntype Spec =\n | { kind: 'env'; name: string }\n | { kind: 'file'; path: string }\n | { kind: 'stdin' };\n\n/**\n * Each field's value by name; a field left empty is left out (its saved\n * value stays). Refuses a required field that is neither saved nor given.\n */\nexport async function collectSecrets(\n ctx: CliContext,\n input: CollectSecretsInput,\n): Promise<Record<string, string>> {\n const { flags } = input;\n const plain = checkPlain(input.fields, input.plain ?? {});\n // What is left to ask: every field `--set` did not give.\n const fields = input.fields.filter(\n (field) => plain[field.name] === undefined,\n );\n const singles = [\n flags.stdin,\n flags.env !== undefined,\n flags.file !== undefined,\n ].filter(Boolean).length;\n if (singles > 1) {\n throw usageError(\n 'Pass one of --secret-env, --secret-file and --secret-stdin, not several.',\n `Each names where the one secret comes from. For several, use --set-secret <field>=… per field.`,\n );\n }\n if (flags.browser && (singles > 0 || flags.sets.length > 0)) {\n throw usageError(\n '--browser asks for every field in the form; it does not mix with --secret-* or --set-secret.',\n 'Drop --browser to pass them from the environment, a file or stdin, or drop those to use the form.',\n );\n }\n\n const rest = { ...input, fields };\n let values: Record<string, string>;\n if (fields.length === 0) {\n values = {};\n } else if (flags.browser) {\n values = await viaBrowser(ctx, rest);\n } else if (singles > 0 || flags.sets.length > 0) {\n values = await viaSources(rest);\n } else if (input.interactive) {\n values = {};\n for (const field of fields) {\n const value = (await input.prompt(field)).trim();\n if (value) values[field.name] = value;\n }\n } else if (Object.keys(plain).length > 0) {\n // Only non-secret values were given: the rest keep what is saved.\n values = {};\n } else {\n throw usageError(\n input.refusal,\n `Without a terminal, pass ${fields.filter((field) => field.sensitive).length > 1 ? FIELD_SECRET_FORMS : SINGLE_SECRET_FORMS}. Or run it yourself in your terminal: npx @ekanos/cli ${input.again}`,\n );\n }\n\n const missing = fields.filter(\n (field) => field.required && !field.set && !values[field.name],\n );\n if (missing.length > 0) {\n throw validationError(\n `${missing.map((field) => field.label).join(', ')} ${missing.length === 1 ? 'is' : 'are'} needed; nothing was saved.`,\n flags.browser || input.interactive\n ? `Run \"ekanos ${input.again}\" again and fill ${missing.length === 1 ? 'it' : 'them'} in.`\n : `Pass ${missing.map((field) => (field.sensitive ? `--set-secret ${field.name}=env:<NAME>` : `--set ${field.name}=<value>`)).join(' ')}.`,\n );\n }\n return { ...plainGiven(plain), ...values };\n}\n\n/** `--set` values, checked against the fields: known, and never a secret. */\nfunction checkPlain(\n fields: readonly SecretField[],\n plain: Record<string, string>,\n): Record<string, string> {\n const byName = new Map(fields.map((field) => [field.name, field]));\n for (const name of Object.keys(plain)) {\n const field = byName.get(name);\n const shown = FIELD_NAME.test(name) ? `\"${name}\"` : 'That field';\n if (!field) {\n throw usageError(\n `${shown} is not a field this asks for.`,\n `Its fields are: ${fields.map((entry) => entry.name).join(', ') || 'none'}.`,\n );\n }\n if (field.sensitive) {\n throw usageError(\n `${shown} is a secret: it is never passed as an argument.`,\n `Pass it as --set-secret ${name}=env:<NAME>|file:<path>|stdin, or use --browser.`,\n );\n }\n }\n return plain;\n}\n\n/** An empty `--set name=` means nothing to change, as Enter does at a prompt. */\nfunction plainGiven(plain: Record<string, string>): Record<string, string> {\n return Object.fromEntries(\n Object.entries(plain).filter(([, value]) => value !== ''),\n );\n}\n\nasync function viaBrowser(\n ctx: CliContext,\n input: CollectSecretsInput,\n): Promise<Record<string, string>> {\n const problem = desktopProblem(\n input.io.env,\n input.io.platform ?? process.platform,\n );\n if (problem) {\n throw preconditionError(\n `--browser needs a desktop to open a form on: ${problem}`,\n `Pass it as ${input.fields.filter((field) => field.sensitive).length > 1 ? '--set-secret <field>=env:<NAME>|file:<path>|stdin' : '--secret-env <NAME>, --secret-file <path> or --secret-stdin'} instead.`,\n );\n }\n ctx.log(\n `Opened a secure form in your browser for ${input.title}… (waiting; if no browser opened, re-run with --secret-env, --secret-file or --secret-stdin)`,\n );\n return collectWithSecretForm({\n title: input.title,\n fields: input.fields,\n open: input.io.openUrl ?? openInBrowser,\n timeoutMs: input.io.formTimeoutMs,\n });\n}\n\n/** Why a browser form cannot be filled here, or null when it can. */\nexport function desktopProblem(\n env: Record<string, string | undefined>,\n platform: NodeJS.Platform,\n): string | null {\n const ci = env.CI?.trim().toLowerCase();\n if (ci && ci !== '0' && ci !== 'false') {\n return 'this is a CI run (CI is set), with no one to fill it in.';\n }\n if (env.SSH_CONNECTION || env.SSH_TTY) {\n return 'this is an SSH session, and a browser on this machine is not in front of you.';\n }\n if (\n platform !== 'darwin' &&\n platform !== 'win32' &&\n !env.DISPLAY &&\n !env.WAYLAND_DISPLAY\n ) {\n return 'there is no desktop session (DISPLAY or WAYLAND_DISPLAY) to open a browser in.';\n }\n return null;\n}\n\nasync function viaSources(\n input: CollectSecretsInput,\n): Promise<Record<string, string>> {\n const { flags, fields } = input;\n const byName = new Map(fields.map((field) => [field.name, field]));\n const specs = new Map<string, Spec>();\n\n for (const entry of flags.sets) {\n const at = entry.indexOf('=');\n const name = at > 0 ? entry.slice(0, at).trim() : '';\n if (!name) {\n throw usageError(\n '--set-secret takes <field>=env:<NAME>, <field>=file:<path> or <field>=stdin.',\n `For example: --set-secret ${fields.find((field) => field.sensitive)?.name ?? 'client_secret'}=env:MY_SECRET.`,\n );\n }\n const shown = FIELD_NAME.test(name) ? `\"${name}\"` : 'that field';\n const field = byName.get(name);\n if (!field) {\n throw usageError(\n `${shown} is not a field this asks for.`,\n `Its fields are: ${fields.map((entry) => entry.name).join(', ') || 'none'}.`,\n );\n }\n if (specs.has(name)) {\n throw usageError(\n `--set-secret names ${shown} twice.`,\n 'Pass each field once.',\n );\n }\n specs.set(name, parseSpec(entry.slice(at + 1), `--set-secret ${name}`));\n }\n\n const single: Spec | null = flags.stdin\n ? { kind: 'stdin' }\n : flags.env !== undefined\n ? envSpec(flags.env, '--secret-env')\n : flags.file !== undefined\n ? { kind: 'file', path: flags.file }\n : null;\n if (single) {\n const secrets = fields.filter(\n (field) => field.sensitive && !specs.has(field.name),\n );\n if (secrets.length !== 1) {\n throw usageError(\n secrets.length === 0\n ? 'There is no secret here for --secret-env, --secret-file or --secret-stdin to fill.'\n : `This asks for ${secrets.length} secrets (${secrets.map((field) => field.name).join(', ')}), so name each one.`,\n secrets.length === 0\n ? 'Drop the flag.'\n : `Pass --set-secret <field>=env:<NAME>|file:<path>|stdin for each, e.g. --set-secret ${secrets[0]!.name}=env:MY_SECRET.`,\n );\n }\n specs.set(secrets[0]!.name, single);\n }\n\n if ([...specs.values()].filter((spec) => spec.kind === 'stdin').length > 1) {\n throw usageError(\n 'Only one secret can come from stdin.',\n 'Take the others from the environment (env:<NAME>) or a file (file:<path>).',\n );\n }\n\n const values: Record<string, string> = {};\n for (const [name, spec] of specs) {\n const value = (await resolveSpec(spec, name, input.io)).trim();\n if (!value) {\n throw preconditionError(\n `The value for ${name} (from ${describe(spec)}) is empty; nothing was saved.`,\n 'Put the secret there, then run the command again.',\n );\n }\n values[name] = value;\n }\n return values;\n}\n\nfunction parseSpec(raw: string, flag: string): Spec {\n if (raw === 'stdin') return { kind: 'stdin' };\n if (raw.startsWith('env:')) return envSpec(raw.slice(4), flag);\n if (raw.startsWith('file:') && raw.length > 5) {\n return { kind: 'file', path: raw.slice(5) };\n }\n throw usageError(\n `${flag}=… is not env:<NAME>, file:<path> or stdin. A secret is never passed as an argument: it would land in your shell history and the process list.`,\n `Put it in an environment variable (${flag}=env:MY_SECRET), a file only you can read (${flag}=file:./secret.txt), or pipe it in (${flag}=stdin).`,\n );\n}\n\nfunction envSpec(name: string, flag: string): Spec {\n if (ENV_NAME.test(name)) return { kind: 'env', name };\n // Never echoed: a value that is not a name may be the secret itself.\n throw usageError(\n `${flag} takes the NAME of an environment variable (letters, digits and _), never the secret itself.`,\n `For example: MY_SECRET=… then ${flag}${flag.startsWith('--set-secret') ? '=env:' : ' '}MY_SECRET.`,\n );\n}\n\nfunction describe(spec: Spec): string {\n if (spec.kind === 'env') return `$${spec.name}`;\n if (spec.kind === 'file') return 'the file';\n return 'stdin';\n}\n\nasync function resolveSpec(\n spec: Spec,\n field: string,\n io: SecretIO,\n): Promise<string> {\n if (spec.kind === 'env') {\n const value = io.env[spec.name];\n if (value === undefined || value === '') {\n throw preconditionError(\n `The environment variable ${spec.name} (for ${field}) is not set; nothing was saved.`,\n `Set it in the environment the command runs in, then run it again.`,\n );\n }\n return value;\n }\n if (spec.kind === 'file') return readSecretFile(spec.path, field, io.cwd);\n return readStdinSecret(io);\n}\n\nfunction readSecretFile(\n given: string,\n field: string,\n cwd: string = process.cwd(),\n): string {\n const result = readPrivateFileNoFollow(\n path.resolve(cwd, given),\n MAX_SECRET_BYTES,\n );\n if (result.ok) return stripNewline(result.bytes.toString('utf8'));\n // The path is not echoed: a mistyped flag may have put the secret there.\n const what = `The file given for ${field}`;\n switch (result.problem) {\n case 'missing':\n throw notFoundError(\n `${what} does not exist; nothing was saved.`,\n 'Check the path, then run the command again.',\n );\n case 'symlink':\n throw preconditionError(\n `${what} is a symlink, and the CLI never reads a secret through one.`,\n 'Name the real file instead.',\n );\n case 'not-a-file':\n throw preconditionError(\n `${what} is not a regular file.`,\n 'Name a file holding just the secret.',\n );\n case 'readable-by-others':\n throw preconditionError(\n `${what} can be read by other users on this machine (mode ${(result.mode ?? 0).toString(8).padStart(3, '0')}), so it was not read.`,\n 'Make it yours alone with \"chmod 600 <file>\", then run the command again.',\n );\n case 'not-yours':\n throw preconditionError(\n `${what} belongs to another user, so it was not read.`,\n 'Use a file you own, readable only by you (chmod 600).',\n );\n case 'too-large':\n throw preconditionError(\n `${what} is larger than a secret (over 64 KB).`,\n 'Name a file holding just the secret.',\n );\n }\n}\n\nasync function readStdinSecret(io: SecretIO): Promise<string> {\n if (io.stdinIsTTY) {\n throw usageError(\n 'stdin is a terminal, so there is no piped secret to read.',\n 'Pipe it in (e.g. `printf %s \"$MY_SECRET\" | ekanos …`), or use --secret-env or --secret-file.',\n );\n }\n const raw = await (io.readStdin ?? readAllStdin)();\n return stripNewline(raw);\n}\n\nfunction readAllStdin(): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n const stdin = process.stdin;\n stdin.on('data', (chunk: Buffer | string) => {\n const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;\n size += buffer.length;\n if (size > MAX_SECRET_BYTES) {\n stdin.destroy();\n reject(\n usageError(\n 'stdin holds more than a secret (over 64 KB).',\n 'Pipe in just the secret.',\n ),\n );\n return;\n }\n chunks.push(buffer);\n });\n stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));\n stdin.on('error', reject);\n });\n}\n\n/** One trailing newline (`echo`, an editor's final line) is not part of it. */\nfunction stripNewline(value: string): string {\n return value.replace(/\\r?\\n$/, '');\n}\n"]}
|
|
@@ -7,9 +7,9 @@ import { type TeamsApiResult } from './teams-api.js';
|
|
|
7
7
|
* GET /api/partner/teams/<team>/integrations/<product>/connect?source=<slug>[&as=<email>][&connection=<id>]
|
|
8
8
|
* → { plan }
|
|
9
9
|
* POST /api/partner/teams/<team>/integrations/<product>/connect?source=<slug>
|
|
10
|
-
* { as?, fields?, connection? }
|
|
10
|
+
* { as?, fields?, set?, connection? }
|
|
11
11
|
* → { plan, result: "activated" } fields / no-fields
|
|
12
|
-
* → { plan, result: "consent", authorizeUrl, expiresAt } oauth
|
|
12
|
+
* → { plan, result: "consent", authorizeUrl, expiresAt } oauth (fields too, for signIn "app")
|
|
13
13
|
*
|
|
14
14
|
* The plan's `mode`, decided by the host, says how the product is
|
|
15
15
|
* connected. The shapes are read here rather than from
|
|
@@ -49,6 +49,10 @@ export declare const TeamConnectPlanSchema: z.ZodObject<{
|
|
|
49
49
|
unsupported: "unsupported";
|
|
50
50
|
"works-as-shipped": "works-as-shipped";
|
|
51
51
|
}>;
|
|
52
|
+
signIn: z.ZodOptional<z.ZodEnum<{
|
|
53
|
+
app: "app";
|
|
54
|
+
partner: "partner";
|
|
55
|
+
}>>;
|
|
52
56
|
reason: z.ZodNullable<z.ZodString>;
|
|
53
57
|
fields: z.ZodArray<z.ZodObject<{
|
|
54
58
|
name: z.ZodString;
|
|
@@ -56,6 +60,19 @@ export declare const TeamConnectPlanSchema: z.ZodObject<{
|
|
|
56
60
|
description: z.ZodNullable<z.ZodString>;
|
|
57
61
|
sensitive: z.ZodBoolean;
|
|
58
62
|
required: z.ZodBoolean;
|
|
63
|
+
type: z.ZodDefault<z.ZodEnum<{
|
|
64
|
+
boolean: "boolean";
|
|
65
|
+
json: "json";
|
|
66
|
+
list: "list";
|
|
67
|
+
number: "number";
|
|
68
|
+
select: "select";
|
|
69
|
+
text: "text";
|
|
70
|
+
}>>;
|
|
71
|
+
options: z.ZodDefault<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
72
|
+
itemType: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
|
|
73
|
+
number: "number";
|
|
74
|
+
text: "text";
|
|
75
|
+
}>>>;
|
|
59
76
|
}, z.core.$strip>>;
|
|
60
77
|
teamApp: z.ZodBoolean;
|
|
61
78
|
member: z.ZodNullable<z.ZodObject<{
|
|
@@ -78,6 +95,11 @@ export declare const TeamConnectPlanSchema: z.ZodObject<{
|
|
|
78
95
|
}, z.core.$strip>>;
|
|
79
96
|
}, z.core.$strip>;
|
|
80
97
|
export type TeamConnectPlan = z.infer<typeof TeamConnectPlanSchema>;
|
|
98
|
+
export type TeamConnectField = TeamConnectPlan['fields'][number];
|
|
99
|
+
/** A field's value as sent: text, or typed by `connect-fields.ts`. */
|
|
100
|
+
export type FieldValue = string | number | boolean | null | FieldValue[] | {
|
|
101
|
+
[key: string]: FieldValue;
|
|
102
|
+
};
|
|
81
103
|
declare const ShowSchema: z.ZodObject<{
|
|
82
104
|
plan: z.ZodObject<{
|
|
83
105
|
source: z.ZodObject<{
|
|
@@ -112,6 +134,10 @@ declare const ShowSchema: z.ZodObject<{
|
|
|
112
134
|
unsupported: "unsupported";
|
|
113
135
|
"works-as-shipped": "works-as-shipped";
|
|
114
136
|
}>;
|
|
137
|
+
signIn: z.ZodOptional<z.ZodEnum<{
|
|
138
|
+
app: "app";
|
|
139
|
+
partner: "partner";
|
|
140
|
+
}>>;
|
|
115
141
|
reason: z.ZodNullable<z.ZodString>;
|
|
116
142
|
fields: z.ZodArray<z.ZodObject<{
|
|
117
143
|
name: z.ZodString;
|
|
@@ -119,6 +145,19 @@ declare const ShowSchema: z.ZodObject<{
|
|
|
119
145
|
description: z.ZodNullable<z.ZodString>;
|
|
120
146
|
sensitive: z.ZodBoolean;
|
|
121
147
|
required: z.ZodBoolean;
|
|
148
|
+
type: z.ZodDefault<z.ZodEnum<{
|
|
149
|
+
boolean: "boolean";
|
|
150
|
+
json: "json";
|
|
151
|
+
list: "list";
|
|
152
|
+
number: "number";
|
|
153
|
+
select: "select";
|
|
154
|
+
text: "text";
|
|
155
|
+
}>>;
|
|
156
|
+
options: z.ZodDefault<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
157
|
+
itemType: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
|
|
158
|
+
number: "number";
|
|
159
|
+
text: "text";
|
|
160
|
+
}>>>;
|
|
122
161
|
}, z.core.$strip>>;
|
|
123
162
|
teamApp: z.ZodBoolean;
|
|
124
163
|
member: z.ZodNullable<z.ZodObject<{
|
|
@@ -175,6 +214,10 @@ declare const StartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
175
214
|
unsupported: "unsupported";
|
|
176
215
|
"works-as-shipped": "works-as-shipped";
|
|
177
216
|
}>;
|
|
217
|
+
signIn: z.ZodOptional<z.ZodEnum<{
|
|
218
|
+
app: "app";
|
|
219
|
+
partner: "partner";
|
|
220
|
+
}>>;
|
|
178
221
|
reason: z.ZodNullable<z.ZodString>;
|
|
179
222
|
fields: z.ZodArray<z.ZodObject<{
|
|
180
223
|
name: z.ZodString;
|
|
@@ -182,6 +225,19 @@ declare const StartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
182
225
|
description: z.ZodNullable<z.ZodString>;
|
|
183
226
|
sensitive: z.ZodBoolean;
|
|
184
227
|
required: z.ZodBoolean;
|
|
228
|
+
type: z.ZodDefault<z.ZodEnum<{
|
|
229
|
+
boolean: "boolean";
|
|
230
|
+
json: "json";
|
|
231
|
+
list: "list";
|
|
232
|
+
number: "number";
|
|
233
|
+
select: "select";
|
|
234
|
+
text: "text";
|
|
235
|
+
}>>;
|
|
236
|
+
options: z.ZodDefault<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
237
|
+
itemType: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
|
|
238
|
+
number: "number";
|
|
239
|
+
text: "text";
|
|
240
|
+
}>>>;
|
|
185
241
|
}, z.core.$strip>>;
|
|
186
242
|
teamApp: z.ZodBoolean;
|
|
187
243
|
member: z.ZodNullable<z.ZodObject<{
|
|
@@ -238,6 +294,10 @@ declare const StartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
238
294
|
unsupported: "unsupported";
|
|
239
295
|
"works-as-shipped": "works-as-shipped";
|
|
240
296
|
}>;
|
|
297
|
+
signIn: z.ZodOptional<z.ZodEnum<{
|
|
298
|
+
app: "app";
|
|
299
|
+
partner: "partner";
|
|
300
|
+
}>>;
|
|
241
301
|
reason: z.ZodNullable<z.ZodString>;
|
|
242
302
|
fields: z.ZodArray<z.ZodObject<{
|
|
243
303
|
name: z.ZodString;
|
|
@@ -245,6 +305,19 @@ declare const StartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
245
305
|
description: z.ZodNullable<z.ZodString>;
|
|
246
306
|
sensitive: z.ZodBoolean;
|
|
247
307
|
required: z.ZodBoolean;
|
|
308
|
+
type: z.ZodDefault<z.ZodEnum<{
|
|
309
|
+
boolean: "boolean";
|
|
310
|
+
json: "json";
|
|
311
|
+
list: "list";
|
|
312
|
+
number: "number";
|
|
313
|
+
select: "select";
|
|
314
|
+
text: "text";
|
|
315
|
+
}>>;
|
|
316
|
+
options: z.ZodDefault<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
317
|
+
itemType: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
|
|
318
|
+
number: "number";
|
|
319
|
+
text: "text";
|
|
320
|
+
}>>>;
|
|
248
321
|
}, z.core.$strip>>;
|
|
249
322
|
teamApp: z.ZodBoolean;
|
|
250
323
|
member: z.ZodNullable<z.ZodObject<{
|
|
@@ -281,6 +354,8 @@ interface Target {
|
|
|
281
354
|
export declare function showTeamConnect(host: string, accessToken: string, target: Target, asEmail?: string): Promise<TeamsApiResult<z.infer<typeof ShowSchema>>>;
|
|
282
355
|
export declare function startTeamConnect(host: string, accessToken: string, target: Target, body: {
|
|
283
356
|
as?: string;
|
|
284
|
-
fields?: Record<string,
|
|
357
|
+
fields?: Record<string, FieldValue>;
|
|
358
|
+
/** The field names given by `--set`, not a prompt: the host refuses a secret among them. */
|
|
359
|
+
set?: string[];
|
|
285
360
|
}): Promise<TeamsApiResult<TeamConnectStart>>;
|
|
286
361
|
export {};
|