@theholocron/kyber 0.0.0-stage → 5.0.0-alpha.338
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/LICENSE +21 -0
- package/README.md +43 -2
- package/dist/flags.d.mts +38 -0
- package/dist/flags.mjs +56 -0
- package/dist/non-interactive-error-Mm_Mn79M.mjs +12 -0
- package/dist/notifier.d.mts +33 -0
- package/dist/notifier.mjs +121 -0
- package/dist/prompts.d.mts +214 -0
- package/dist/prompts.mjs +263 -0
- package/dist/secrets.d.mts +191 -0
- package/dist/secrets.mjs +329 -0
- package/dist/testing.d.mts +83 -0
- package/dist/testing.mjs +139 -0
- package/dist/token-args-BgrCq_kK.mjs +38 -0
- package/dist/token-args.type-arNIDKsQ.d.mts +7 -0
- package/dist/ui.d.mts +151 -0
- package/dist/ui.mjs +391 -0
- package/package.json +126 -3
package/dist/prompts.mjs
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { t as NonInteractiveError } from "./non-interactive-error-Mm_Mn79M.mjs";
|
|
2
|
+
import search from "@inquirer/search";
|
|
3
|
+
import { checkbox, confirm, input, password, select } from "@inquirer/prompts";
|
|
4
|
+
// src/prompts/menu/menu.ts
|
|
5
|
+
/**
|
|
6
|
+
* Searchable autocomplete over one menu level's `items`. `select()` would work too
|
|
7
|
+
* at these list sizes, but `search()` degrades gracefully as the surface grows and
|
|
8
|
+
* costs nothing when it doesn't. Resolves to the picked item's `value`.
|
|
9
|
+
*/
|
|
10
|
+
async function pickMenuItem(items, message = "What would you like to do?") {
|
|
11
|
+
return search({
|
|
12
|
+
message,
|
|
13
|
+
source: (term) => searchChoices(items, term)
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `@inquirer/search`'s `source` callback, factored out as a plain function —
|
|
18
|
+
* unit-testable directly instead of only through a mocked `search()` call.
|
|
19
|
+
* Empty/undefined `term` (nothing typed yet) returns every item.
|
|
20
|
+
*/
|
|
21
|
+
function searchChoices(items, term) {
|
|
22
|
+
const needle = term?.toLowerCase();
|
|
23
|
+
const pool = !needle ? items : items.filter((i) => i.haystack.includes(needle));
|
|
24
|
+
const rank = (i) => {
|
|
25
|
+
if (!needle) return 0;
|
|
26
|
+
const name = i.name.toLowerCase();
|
|
27
|
+
return name.startsWith(needle) ? 0 : name.includes(needle) ? 1 : 2;
|
|
28
|
+
};
|
|
29
|
+
return [...pool].sort((a, b) => rank(a) - rank(b)).map((i) => ({
|
|
30
|
+
name: i.name,
|
|
31
|
+
value: i.value,
|
|
32
|
+
description: i.description,
|
|
33
|
+
short: i.name.replace(/ ›$/, "")
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
// src/prompts/prompt-options/prompt-options.ts
|
|
37
|
+
/**
|
|
38
|
+
* A required option must not resolve to an empty answer — `optionArgv`/
|
|
39
|
+
* `secretEnv` (child-argv.ts) both drop an empty-string value entirely,
|
|
40
|
+
* which would silently invoke the child without a flag the caller asked
|
|
41
|
+
* for. Passed as `validate` so inquirer re-prompts instead of accepting it.
|
|
42
|
+
*/
|
|
43
|
+
const requireNonEmpty = (value) => value.trim().length > 0 || "required";
|
|
44
|
+
async function promptOne(option) {
|
|
45
|
+
if (option.secret || option.type === "password") return await password({
|
|
46
|
+
message: option.message,
|
|
47
|
+
...option.required ? { validate: requireNonEmpty } : {}
|
|
48
|
+
});
|
|
49
|
+
if (option.type === "confirm") return await confirm({
|
|
50
|
+
message: option.message,
|
|
51
|
+
default: option.default
|
|
52
|
+
});
|
|
53
|
+
if (option.type === "select") {
|
|
54
|
+
const choices = typeof option.choices === "function" ? option.choices() : option.choices ?? [];
|
|
55
|
+
return await select({
|
|
56
|
+
message: option.message,
|
|
57
|
+
choices: choices.map((c) => ({ value: c }))
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (option.arity && option.arity > 1) {
|
|
61
|
+
const values = [];
|
|
62
|
+
for (let n = 0; n < option.arity; n++) values.push(await input({
|
|
63
|
+
message: `${option.message} (${n + 1} of ${option.arity})`,
|
|
64
|
+
...option.required ? { validate: requireNonEmpty } : {}
|
|
65
|
+
}));
|
|
66
|
+
return values;
|
|
67
|
+
}
|
|
68
|
+
return await input({
|
|
69
|
+
message: option.message,
|
|
70
|
+
default: option.default,
|
|
71
|
+
...option.required ? { validate: requireNonEmpty } : {}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* For each of `options` not already present in `argv`, resolves a value —
|
|
76
|
+
* `required` ones are always included, like required positionals, never
|
|
77
|
+
* behind a selector; everything else is offered through one `checkbox()`
|
|
78
|
+
* ("set any options?") and only the picked ones are then prompted in
|
|
79
|
+
* `options`' own declaration order. An empty list (or every optional one
|
|
80
|
+
* left unpicked) means zero prompts.
|
|
81
|
+
*
|
|
82
|
+
* A picked option (whether required or checkbox-picked) with
|
|
83
|
+
* `askLast: true` (holocron#1052 follow-up — a time-sensitive secret like
|
|
84
|
+
* an OTP, which should be typed as close as possible to when it's actually
|
|
85
|
+
* used, or one that's always needed for a specific operator's own account
|
|
86
|
+
* but not everyone's) isn't given a value here yet: it's returned in
|
|
87
|
+
* `deferred` instead, for the caller to resolve via
|
|
88
|
+
* `promptForDeferredOptions` right before the real (non-dry-run) spawn.
|
|
89
|
+
*
|
|
90
|
+
* `when`, evaluated against answers collected so far in that same
|
|
91
|
+
* declaration order, can skip a picked option entirely. Skipped this way,
|
|
92
|
+
* the option is simply absent from the result, exactly as if it had never
|
|
93
|
+
* been picked — acceptable for a minor, genuinely optional dependency, but
|
|
94
|
+
* prefer `OptionMode` (see `promptForOptions`) when the options have a
|
|
95
|
+
* real either/or relationship, so nothing contingent is shown at all.
|
|
96
|
+
*/
|
|
97
|
+
async function promptForFlatOptions(commandName, options, argv) {
|
|
98
|
+
if (options.length === 0) return {
|
|
99
|
+
answers: {},
|
|
100
|
+
deferred: []
|
|
101
|
+
};
|
|
102
|
+
const answers = {};
|
|
103
|
+
const deferred = [];
|
|
104
|
+
const needsPrompt = (option) => {
|
|
105
|
+
const existing = argv[option.key];
|
|
106
|
+
return existing === void 0 || existing === "";
|
|
107
|
+
};
|
|
108
|
+
const required = options.filter((o) => o.required);
|
|
109
|
+
const optional = options.filter((o) => !o.required);
|
|
110
|
+
if ((required.some(needsPrompt) || optional.some(needsPrompt)) && !process.stdin.isTTY) throw new NonInteractiveError(`\`${commandName}\` has options only settable interactively — pass them directly: holocron ${commandName} [options]`);
|
|
111
|
+
for (const option of required) {
|
|
112
|
+
if (!needsPrompt(option)) {
|
|
113
|
+
answers[option.key] = argv[option.key];
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (option.askLast) {
|
|
117
|
+
deferred.push(option);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
answers[option.key] = await promptOne(option);
|
|
121
|
+
}
|
|
122
|
+
for (const option of optional) if (!needsPrompt(option)) answers[option.key] = argv[option.key];
|
|
123
|
+
const selectable = optional.filter(needsPrompt);
|
|
124
|
+
if (selectable.length === 0) return {
|
|
125
|
+
answers,
|
|
126
|
+
deferred
|
|
127
|
+
};
|
|
128
|
+
const picked = await checkbox({
|
|
129
|
+
message: "Set any options? (space to select, enter to skip)",
|
|
130
|
+
choices: selectable.map((o) => ({
|
|
131
|
+
name: o.message,
|
|
132
|
+
value: o.key
|
|
133
|
+
}))
|
|
134
|
+
});
|
|
135
|
+
if (picked.length === 0) return {
|
|
136
|
+
answers,
|
|
137
|
+
deferred
|
|
138
|
+
};
|
|
139
|
+
const pickedSet = new Set(picked);
|
|
140
|
+
for (const option of optional) {
|
|
141
|
+
if (!pickedSet.has(option.key)) continue;
|
|
142
|
+
if (option.when && !option.when(answers)) continue;
|
|
143
|
+
if (option.askLast) {
|
|
144
|
+
deferred.push(option);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
answers[option.key] = await promptOne(option);
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
answers,
|
|
151
|
+
deferred
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* For a `modes`-based command (holocron#1052 follow-up): asks which
|
|
156
|
+
* mutually exclusive workflow first ("What would you like to do?"), then
|
|
157
|
+
* offers only that mode's own options, via the exact same
|
|
158
|
+
* `promptForFlatOptions` flow. The picked mode's `flag`, if it has one, is
|
|
159
|
+
* folded in as an already-resolved `confirm`-type option so
|
|
160
|
+
* `buildChildArgv` renders it with no special-casing — it just sees
|
|
161
|
+
* another boolean option whose answer happens to already be `true`.
|
|
162
|
+
*/
|
|
163
|
+
async function promptForModes(entry, modes, argv) {
|
|
164
|
+
if (!process.stdin.isTTY) throw new NonInteractiveError(`\`${entry.name}\` has options only settable interactively — pass them directly: holocron ${entry.name} [options]`);
|
|
165
|
+
const modeKey = await select({
|
|
166
|
+
message: "What would you like to do?",
|
|
167
|
+
choices: modes.map((m) => ({
|
|
168
|
+
name: m.message,
|
|
169
|
+
value: m.key,
|
|
170
|
+
description: m.description
|
|
171
|
+
}))
|
|
172
|
+
});
|
|
173
|
+
const mode = modes.find((m) => m.key === modeKey);
|
|
174
|
+
const options = mode.options ?? [];
|
|
175
|
+
const { answers, deferred } = await promptForFlatOptions(entry.name, options, argv);
|
|
176
|
+
if (!mode.flag) return {
|
|
177
|
+
options,
|
|
178
|
+
answers,
|
|
179
|
+
deferred
|
|
180
|
+
};
|
|
181
|
+
const flagOption = {
|
|
182
|
+
key: `__mode_${mode.key}`,
|
|
183
|
+
flag: mode.flag,
|
|
184
|
+
type: "confirm",
|
|
185
|
+
message: mode.message
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
options: [flagOption, ...options],
|
|
189
|
+
answers: {
|
|
190
|
+
[flagOption.key]: true,
|
|
191
|
+
...answers
|
|
192
|
+
},
|
|
193
|
+
deferred
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Resolves `entry`'s options into a {@link ResolvedOptions} ready for
|
|
198
|
+
* `buildChildArgv`/`secretEnv` — delegating to {@link promptForModes} when
|
|
199
|
+
* `entry.modes` is set, or treating `entry.options` as a flat list
|
|
200
|
+
* otherwise. See `promptForFlatOptions` for the flat-list prompting
|
|
201
|
+
* algorithm itself. `result.deferred` still needs
|
|
202
|
+
* {@link promptForDeferredOptions} before the real spawn.
|
|
203
|
+
*/
|
|
204
|
+
async function promptForOptions(entry, argv) {
|
|
205
|
+
if (entry.modes && entry.modes.length > 0) return promptForModes(entry, entry.modes, argv);
|
|
206
|
+
const options = entry.options ?? [];
|
|
207
|
+
const { answers, deferred } = await promptForFlatOptions(entry.name, options, argv);
|
|
208
|
+
return {
|
|
209
|
+
options,
|
|
210
|
+
answers,
|
|
211
|
+
deferred
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Collects a value for each of `deferred` — the `askLast: true` options
|
|
216
|
+
* `promptForOptions` deliberately left unresolved. Call this as the very
|
|
217
|
+
* last thing before the real (non-dry-run) spawn, never before: a dry-run
|
|
218
|
+
* preview never reaches the real npm call these options exist for (an OTP
|
|
219
|
+
* is never checked during `--dry-run`), and a time-sensitive code typed
|
|
220
|
+
* any earlier — before a dry-run preview runs, before "proceed with the
|
|
221
|
+
* real run?" waits on a human — risks going stale before it's actually
|
|
222
|
+
* used. A no-op (no prompt at all) when `deferred` is empty.
|
|
223
|
+
*/
|
|
224
|
+
async function promptForDeferredOptions(deferred) {
|
|
225
|
+
const answers = {};
|
|
226
|
+
for (const option of deferred) answers[option.key] = await promptOne(option);
|
|
227
|
+
return answers;
|
|
228
|
+
}
|
|
229
|
+
// src/prompts/prompt-positionals/prompt-positionals.ts
|
|
230
|
+
/**
|
|
231
|
+
* For each of `entry.positionals` not already present in `argv`, prompts for
|
|
232
|
+
* it (`input()` or `select()`, per the positional's `type`) and returns the
|
|
233
|
+
* resolved values in positional order — ready to append to a spawned child's
|
|
234
|
+
* argv, or to use directly in the current handler (Layer 3).
|
|
235
|
+
*
|
|
236
|
+
* Throws {@link NonInteractiveError} the first time it would need to prompt
|
|
237
|
+
* on a non-TTY stdin, and again if a `select` positional's choices resolve
|
|
238
|
+
* empty (nothing to pick from — e.g. `auth unset` with no stored tokens).
|
|
239
|
+
*/
|
|
240
|
+
async function promptForPositionals(entry, argv) {
|
|
241
|
+
const out = [];
|
|
242
|
+
for (const positional of entry.positionals) {
|
|
243
|
+
const existing = argv[positional.key];
|
|
244
|
+
if (existing !== void 0 && existing !== "") {
|
|
245
|
+
out.push(String(existing));
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (!process.stdin.isTTY) throw new NonInteractiveError(`\`${entry.name}\` needs "${positional.key}" — pass it directly: holocron ${entry.name} <${positional.cliArg ?? positional.key}>`);
|
|
249
|
+
if (positional.type === "select") {
|
|
250
|
+
const choices = typeof positional.choices === "function" ? positional.choices() : positional.choices ?? [];
|
|
251
|
+
if (choices.length === 0) throw new NonInteractiveError(`\`${entry.name}\`: no ${positional.key} to choose from — nothing stored yet`);
|
|
252
|
+
out.push(await select({
|
|
253
|
+
message: positional.message,
|
|
254
|
+
choices: choices.map((c) => ({ value: c }))
|
|
255
|
+
}));
|
|
256
|
+
} else out.push(await input({
|
|
257
|
+
message: positional.message,
|
|
258
|
+
...positional.validate ? { validate: positional.validate } : {}
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
export { pickMenuItem, promptForDeferredOptions, promptForOptions, promptForPositionals, searchChoices };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { t as ParsedTokenArgs } from "./token-args.type-arNIDKsQ.mjs";
|
|
2
|
+
export declare class TokenParseError extends Error {
|
|
3
|
+
name: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Converts raw --token CLI values into a typed result.
|
|
7
|
+
*
|
|
8
|
+
* Bare form: --token ghp_xxx → { cliToken: "ghp_xxx" }
|
|
9
|
+
* Keyed form: --token github=ghp_xxx → { cliTokens: { github: "ghp_xxx" } }
|
|
10
|
+
* Mixed: --token github=ghp_xxx --token v_yyy
|
|
11
|
+
* → { cliToken: "v_yyy", cliTokens: { github: "ghp_xxx" } }
|
|
12
|
+
*
|
|
13
|
+
* Values may contain "=" (e.g. base64 strings) — only the first "=" is treated as a separator.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseTokenArgs(tokens: string[]): ParsedTokenArgs;
|
|
16
|
+
/** Puts `text` on the system clipboard through `clipboardy`, which uses the platform's own tool. Loaded only when called. */
|
|
17
|
+
export declare function copyToClipboard(text: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* The variables the `credentials` providers returned, kept for the life of the process.
|
|
20
|
+
*
|
|
21
|
+
* The plugin loader reads each provider once (`setCredentials`), and the token resolvers look a token up here
|
|
22
|
+
* (`getCredential`) after the flag and the process environment and before the keyring. It is in memory only: the
|
|
23
|
+
* values were already in this process when the provider returned them, and nothing here writes them anywhere.
|
|
24
|
+
*
|
|
25
|
+
* A source is one provider's answer for one environment. They are kept in the order they were set, which is the
|
|
26
|
+
* order `holocron.config` lists the providers, and the first source that has a value wins.
|
|
27
|
+
*/
|
|
28
|
+
/** Remember `variables` as what the source `source` returned. Setting a source again replaces its variables. */
|
|
29
|
+
export declare function setCredentials(source: string, variables: Record<string, string>): void;
|
|
30
|
+
/** Whether the source `source` has been read already, so a second loader in the same process does not read it again. */
|
|
31
|
+
export declare function hasCredentials(source: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The first non-empty value for any of `names`, from the first source that has one. Sources are tried in the order
|
|
34
|
+
* they were set; within a source the names are tried in the order given (the project's own variable name before the
|
|
35
|
+
* vendor's). `undefined` when no source has one.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getCredential(...names: string[]): string | undefined;
|
|
38
|
+
/** Forget every source. For tests, and for a process that wants the next load to read the providers again. */
|
|
39
|
+
export declare function clearCredentials(): void;
|
|
40
|
+
/**
|
|
41
|
+
* The file {@link writeCredentialsFile} wrote: its device and inode, and an open descriptor for it. The descriptor is
|
|
42
|
+
* held until cleanup, so the inode cannot be freed and handed to some other file, which makes comparing device and inode
|
|
43
|
+
* a sound way to tell this file from one that has since taken its path. Whoever receives it must give it to
|
|
44
|
+
* {@link shredFile}, or close `fd`.
|
|
45
|
+
*/
|
|
46
|
+
interface FileIdentity {
|
|
47
|
+
/** The device the file is on. */
|
|
48
|
+
dev: number;
|
|
49
|
+
/** The file's inode number on that device. */
|
|
50
|
+
ino: number;
|
|
51
|
+
/** An open, writable descriptor for the file, used to overwrite its contents wherever the path points by then. */
|
|
52
|
+
fd: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* What {@link shredFile} did: `deleted` the file it was told about, found it `missing` (already gone), or left the
|
|
56
|
+
* path alone because it is no longer that file (`replaced`: another process swapped it, or it is not a regular file).
|
|
57
|
+
*/
|
|
58
|
+
type ShredOutcome = "deleted" | "missing" | "replaced";
|
|
59
|
+
/**
|
|
60
|
+
* Write `text` to `path` as a file only its owner can read (mode `0600`), and return which file that is, with its
|
|
61
|
+
* descriptor still open, so cleanup can remove only it. Without `force` it will not replace a file that is already
|
|
62
|
+
* there. A write that fails removes the file it had just created, so a failed write never leaves plaintext tokens behind.
|
|
63
|
+
*
|
|
64
|
+
* With `force` the replacement is written to a temporary file beside `path` first and then renamed over it, which is
|
|
65
|
+
* atomic, so a replacement that cannot be written or swapped in leaves the old file exactly as it was. Only once the
|
|
66
|
+
* new file is in place is the old file's content overwritten with zeros through a descriptor opened on it before the
|
|
67
|
+
* swap, so the tokens of an earlier run are not left recoverable. A symlink at `path` is replaced, not followed.
|
|
68
|
+
*
|
|
69
|
+
* Windows does not honour these modes, so the caller does not use this there. `write` is how the text is written; it is
|
|
70
|
+
* a parameter so a test can make the write fail after the file exists.
|
|
71
|
+
*/
|
|
72
|
+
export declare function writeCredentialsFile(path: string, text: string, force?: boolean, write?: (fd: number, text: string) => unknown): FileIdentity;
|
|
73
|
+
/** The name of the file, at the repo root, that holds a developer's own tokens. */
|
|
74
|
+
export declare const DOTENV_FILE = ".env";
|
|
75
|
+
/**
|
|
76
|
+
* Read `<repoRoot>/.env` into a record, or `null` when the file is not there (or cannot be read).
|
|
77
|
+
*
|
|
78
|
+
* Only the repo root is looked at, never the directory the command happens to run in, so a `.env` in a checkout you
|
|
79
|
+
* cloned and ran the CLI from is not trusted. The file is optional: nothing asks for it, and a project that keeps its
|
|
80
|
+
* tokens in a `credentials` provider or the keyring does not need one.
|
|
81
|
+
*/
|
|
82
|
+
export declare function readDotenvFile(repoRoot: string): Record<string, string> | null;
|
|
83
|
+
/**
|
|
84
|
+
* `NAME=value` in a form dotenv reads back as the same value, or `null` when there is none.
|
|
85
|
+
*
|
|
86
|
+
* dotenv has no escape for a quote or a backslash: a double-quoted value only turns `\n` and `\r` into line breaks, and
|
|
87
|
+
* leaves every other backslash, an escaped quote included, in the value. So nothing is escaped here. A plain value is
|
|
88
|
+
* written bare; any other is wrapped in the first of `'`, `"` and `` ` `` it does not contain; a value with a line
|
|
89
|
+
* break can only be double-quoted (with `\n` and `\r`) and so must not contain `"` or a backslash. A value that fits
|
|
90
|
+
* none of these is `null`, and the caller says so rather than exporting a different token.
|
|
91
|
+
*/
|
|
92
|
+
export declare function dotenvLine(name: string, value: string): string | null;
|
|
93
|
+
/**
|
|
94
|
+
* What git says about a path: `ignored` when a rule ignores it, `tracked` when git would track it, `no-repo` when the
|
|
95
|
+
* path is not in a git repository (or git is not installed, in which case nothing can commit it), and `unknown` when
|
|
96
|
+
* git failed in some other way.
|
|
97
|
+
*/
|
|
98
|
+
type GitIgnoreState = "ignored" | "tracked" | "no-repo" | "unknown";
|
|
99
|
+
/**
|
|
100
|
+
* Ask git whether it ignores `path` (see {@link GitIgnoreState}). A caller that must not leave a file where it could be
|
|
101
|
+
* committed accepts only `ignored` and `no-repo`. Reads only; it changes nothing.
|
|
102
|
+
*/
|
|
103
|
+
export declare function isGitIgnored(path: string): GitIgnoreState;
|
|
104
|
+
/** One stored secret, as the OS keyring binding hands it out. */
|
|
105
|
+
interface KeyringEntry {
|
|
106
|
+
/** Store or replace the secret. Throws when the platform has no credential store. */
|
|
107
|
+
setPassword(password: string): void;
|
|
108
|
+
/** The secret, or `null` when nothing is stored. Throws when the platform has no credential store. */
|
|
109
|
+
getPassword(): string | null;
|
|
110
|
+
/** Remove the secret, and say whether there was one. Throws when the platform has no credential store. */
|
|
111
|
+
deletePassword(): boolean;
|
|
112
|
+
}
|
|
113
|
+
/** The part of `@napi-rs/keyring` that {@link createKeyring} uses, so a test can stand in for the OS. */
|
|
114
|
+
interface KeyringBinding {
|
|
115
|
+
/** A handle on one secret: a service (the scope) and an account (the name within it). */
|
|
116
|
+
Entry: new (service: string, account: string) => KeyringEntry;
|
|
117
|
+
/** Every secret stored under `service`. Throws when the platform has no credential store. */
|
|
118
|
+
findCredentials(service: string): Array<{
|
|
119
|
+
account: string;
|
|
120
|
+
}>;
|
|
121
|
+
}
|
|
122
|
+
/** What {@link createKeyring} needs. */
|
|
123
|
+
interface KeyringOptions {
|
|
124
|
+
/**
|
|
125
|
+
* The scope every secret is stored under, usually a reverse-DNS name (`com.example.tool`), so one tool's secrets
|
|
126
|
+
* do not mix with another's.
|
|
127
|
+
*/
|
|
128
|
+
service: string;
|
|
129
|
+
/**
|
|
130
|
+
* The OS keyring binding. Default: `@napi-rs/keyring`, loaded the first time it is needed; a keyring that cannot be
|
|
131
|
+
* loaded behaves like one the platform does not support.
|
|
132
|
+
*/
|
|
133
|
+
binding?: KeyringBinding | null;
|
|
134
|
+
}
|
|
135
|
+
/** A tool's secrets in the OS keyring. Every method is best-effort: where the keyring cannot be used it answers `null`, `false` or an empty list and never throws. */
|
|
136
|
+
interface Keyring {
|
|
137
|
+
/**
|
|
138
|
+
* The secret stored for `account`, or `null` for both "not stored" and "keyring unavailable". The answer, a miss
|
|
139
|
+
* included, is remembered for the life of the process, so the keyring (which can prompt on every read) is asked once
|
|
140
|
+
* per account.
|
|
141
|
+
*/
|
|
142
|
+
get(account: string): string | null;
|
|
143
|
+
/** Store or replace the secret for `account`. `true` on success, `false` when the keyring is unavailable. */
|
|
144
|
+
set(account: string, secret: string): boolean;
|
|
145
|
+
/** Remove the secret for `account`. `true` when one was removed, `false` when there was none or the keyring is unavailable. */
|
|
146
|
+
delete(account: string): boolean;
|
|
147
|
+
/** The accounts that have a secret under this service. */
|
|
148
|
+
list(): string[];
|
|
149
|
+
/** Whether the OS keyring can be used at all, for a caller that must tell an outage from an empty keyring. */
|
|
150
|
+
isAvailable(): boolean;
|
|
151
|
+
/** Forget what `get` remembered, so the next read asks the keyring again. For a test, or after the keyring was changed from outside the process. */
|
|
152
|
+
clearCache(): void;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A tool's secrets in the OS keyring, under one service name.
|
|
156
|
+
*
|
|
157
|
+
* Keyring access is best-effort. Platforms without a supported credential store (some Linux CI images, sandboxed
|
|
158
|
+
* environments) throw from the underlying library, and `@napi-rs/keyring` is an optional peer that may not be installed
|
|
159
|
+
* at all. Every method catches that and answers `null`, `false` or an empty list rather than propagating, so a caller's
|
|
160
|
+
* chain (a flag, then the environment, then the keyring) falls through to the paths that do work, which is how CI is
|
|
161
|
+
* meant to run.
|
|
162
|
+
*
|
|
163
|
+
* Reads are remembered in memory only, for the life of the process. A cache that outlived the process would mean writing
|
|
164
|
+
* secrets somewhere else, which is what the keyring is there to avoid.
|
|
165
|
+
*/
|
|
166
|
+
export declare function createKeyring(options: KeyringOptions): Keyring;
|
|
167
|
+
/**
|
|
168
|
+
* Create `path` as a new file only its owner can read (mode `0600`, set again on the open file so a loose `umask`
|
|
169
|
+
* cannot widen it), write `text` to it and return which file it is, with its descriptor left open. It fails if the path
|
|
170
|
+
* exists, a symlink included, so nothing is written through a planted link or over a file that is there. When the
|
|
171
|
+
* write fails after the file exists, it closes and removes that file, so no plaintext is left behind. `write` is how the
|
|
172
|
+
* text is written; it is a parameter so a test can make the write fail.
|
|
173
|
+
*/
|
|
174
|
+
export declare function createPrivateFile(path: string, text: string, write?: (fd: number, text: string) => unknown): FileIdentity;
|
|
175
|
+
/**
|
|
176
|
+
* Overwrite the file `identity` names with zeros, close it, and remove it from `path`, so the tokens it held are not
|
|
177
|
+
* left in a deleted-but-recoverable file.
|
|
178
|
+
*
|
|
179
|
+
* The overwrite goes through the descriptor held since the file was written, not through the path, so it zeroes that
|
|
180
|
+
* file wherever the path points now and never follows a symlink: a symlink or another file swapped in is not touched.
|
|
181
|
+
* The path itself is removed only if it is still that file (same device and inode, which the held descriptor keeps
|
|
182
|
+
* from being reused); otherwise it is left alone (`replaced`), so a file another process put there, or another
|
|
183
|
+
* export's file, survives. A path that is already gone is `missing`, not an error; any other failure to remove it
|
|
184
|
+
* throws, so the caller never reports a file as deleted while it is still there.
|
|
185
|
+
*
|
|
186
|
+
* The overwrite is best effort: a journaling or copy-on-write filesystem and an SSD may keep older blocks it does not
|
|
187
|
+
* reach, which is why the file is also kept private and short-lived. One that fails does not stop the removal.
|
|
188
|
+
* `remove` is how the path is deleted; it is a parameter so a test can look at the file just before it goes.
|
|
189
|
+
*/
|
|
190
|
+
export declare function shredFile(path: string, identity: FileIdentity, remove?: (path: string) => void): ShredOutcome;
|
|
191
|
+
export type { FileIdentity, GitIgnoreState, Keyring, KeyringBinding, KeyringEntry, KeyringOptions, ParsedTokenArgs, ShredOutcome };
|