@optique/keyring 1.3.0-dev.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright 2025–2026 Hong Minhee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ @optique/keyring
2
+ ================
3
+
4
+ Operating-system credential-store password fallback for [Optique].
5
+
6
+ Use this package to read secrets from the keyring, with hidden password input
7
+ for interactive commands or environment overrides for API keys. Avoid
8
+ secret-valued CLI options, which can expose values in shell history and
9
+ process listings.
10
+
11
+ [Optique]: https://optique.dev/
12
+
13
+
14
+ Installation
15
+ ------------
16
+
17
+ ~~~~ bash
18
+ deno add jsr:@optique/keyring jsr:@optique/run jsr:@optique/inquirer
19
+ npm add @optique/keyring @optique/run @optique/inquirer
20
+ pnpm add @optique/keyring @optique/run @optique/inquirer
21
+ yarn add @optique/keyring @optique/run @optique/inquirer
22
+ bun add @optique/keyring @optique/run @optique/inquirer
23
+ ~~~~
24
+
25
+
26
+ Quick start
27
+ -----------
28
+
29
+ Keyring lookups are asynchronous. Register the context with `runAsync()`
30
+ through `contexts`, or the fallback cannot read the password. Use `fail()`
31
+ to accept no CLI value and prompt only when the keyring has no password:
32
+
33
+ ~~~~ typescript
34
+ import { object } from "@optique/core/constructs";
35
+ import { fail } from "@optique/core/primitives";
36
+ import { prompt } from "@optique/inquirer";
37
+ import { bindKeyring, createKeyringContext } from "@optique/keyring";
38
+ import { runAsync } from "@optique/run";
39
+
40
+ const keyringContext = createKeyringContext();
41
+
42
+ const parser = object({
43
+ password: prompt(
44
+ bindKeyring(fail<string>(), {
45
+ context: keyringContext,
46
+ service: "com.example.myapp",
47
+ username: "current-user",
48
+ }),
49
+ {
50
+ type: "password",
51
+ message: "Password:",
52
+ mask: false,
53
+ },
54
+ ),
55
+ });
56
+
57
+ const options = await runAsync(parser, {
58
+ contexts: [keyringContext],
59
+ });
60
+ ~~~~
61
+
62
+ The keyring is checked first. A missing password opens a prompt with hidden
63
+ input; credential-store errors reject the parse without prompting. Entered
64
+ passwords are used for this run only. Store a password for your service and
65
+ username using your operating system's credential manager: this package does
66
+ not write or delete credentials.
67
+
68
+
69
+ Priority with environment variables
70
+ -----------------------------------
71
+
72
+ For API keys, put `bindEnv()` outside `bindKeyring()` so an environment
73
+ variable wins over the credential store. Install *@optique/env* for this
74
+ example. Using `fail<string>()` keeps the API key out of CLI arguments;
75
+ this parser never prompts:
76
+
77
+ ~~~~ typescript
78
+ import { fail } from "@optique/core/primitives";
79
+ import { string } from "@optique/core/valueparser";
80
+ import { bindEnv, createEnvContext } from "@optique/env";
81
+ import { bindKeyring, createKeyringContext } from "@optique/keyring";
82
+ import { runAsync } from "@optique/run";
83
+
84
+ const envContext = createEnvContext({ prefix: "MYAPP_" });
85
+ const keyringContext = createKeyringContext();
86
+
87
+ const apiKey = bindEnv(
88
+ bindKeyring(fail<string>(), {
89
+ context: keyringContext,
90
+ service: "com.example.myapp.api-key",
91
+ username: "current-user",
92
+ }),
93
+ {
94
+ context: envContext,
95
+ key: "API_KEY",
96
+ parser: string(),
97
+ },
98
+ );
99
+
100
+ const secret = await runAsync(apiKey, {
101
+ contexts: [envContext, keyringContext],
102
+ });
103
+ ~~~~
104
+
105
+ This checks `MYAPP_API_KEY`, then the keyring, and fails if neither supplies
106
+ an API key. Register both contexts. See the [API key example] for more detail.
107
+ Environment variables can still be exposed or inherited; inject them through
108
+ your automation's secret manager instead of typing literal keys into commands.
109
+
110
+ [API key example]: https://optique.dev/integrations/keyring#priority-with-environment-variables
111
+
112
+
113
+ Custom sources
114
+ --------------
115
+
116
+ Pass an asynchronous `source` to `createKeyringContext()` for tests, alternate
117
+ credential backends, or runtimes that supply their own password reader. Return
118
+ `undefined` only when that source has no password, and reject on lookup errors.
119
+
120
+ ~~~~ typescript
121
+ import { createKeyringContext } from "@optique/keyring";
122
+
123
+ const keyringContext = createKeyringContext({
124
+ source: () => Promise.resolve("test-password"),
125
+ });
126
+ ~~~~
127
+
128
+
129
+ Runtime setup
130
+ -------------
131
+
132
+ The password backend is loaded only when a lookup is needed. On Linux, the
133
+ default source connects to Secret Service over the user's D-Bus session bus.
134
+ Other platforms use the asynchronous `@napi-rs/keyring` API. Node.js and Bun
135
+ use their normal package installations.
136
+
137
+ Deno runs native Node addons from local `node_modules`. Run a Deno program with
138
+ local modules enabled and grant the permissions used by the native loader
139
+ on platforms using `@napi-rs/keyring`:
140
+
141
+ ~~~~ bash
142
+ deno run --node-modules-dir=auto --allow-env --allow-sys \
143
+ --allow-read=node_modules --allow-ffi=node_modules main.ts
144
+ ~~~~
145
+
146
+ On Linux, grant access to the session bus instead. For a standard session bus
147
+ at `$XDG_RUNTIME_DIR/bus`:
148
+
149
+ ~~~~ bash
150
+ deno run --node-modules-dir=auto --allow-env --allow-sys --allow-net \
151
+ --allow-read="node_modules,$XDG_RUNTIME_DIR/bus" \
152
+ --allow-write="$XDG_RUNTIME_DIR/bus" main.ts
153
+ ~~~~
154
+
155
+ Use the socket path from `DBUS_SESSION_BUS_ADDRESS` if it differs.
156
+
157
+
158
+ Missing credentials and errors
159
+ ------------------------------
160
+
161
+ Missing credentials allow the inner parser to provide a fallback or its usual
162
+ missing-value error. Locked, inaccessible, or ambiguous credentials reject the
163
+ parse, including under `optional()` or `withDefault()`. Secret Service
164
+ connection and lookup errors are propagated without switching stores.
165
+ If the inner parser provides a validation hook, it is applied to stored
166
+ passwords. `fail<string>()` has no value validation of its own.
167
+
168
+ Stored-password validation failures use a generic message so the password
169
+ cannot appear in error output. If a validation hook throws or rejects, it is
170
+ replaced with a `TypeError` without the original message or cause.
171
+
172
+ The macOS backend does not provide Touch ID authentication. A custom source can
173
+ supply an alternative backend.
174
+
175
+
176
+ Documentation
177
+ -------------
178
+
179
+ See the [keyring integration guide] for the complete usage guide.
180
+
181
+ [keyring integration guide]: https://optique.dev/integrations/keyring
182
+
183
+
184
+ License
185
+ -------
186
+
187
+ MIT License. See [LICENSE](../../LICENSE) for details.
@@ -0,0 +1,30 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+
25
+ Object.defineProperty(exports, '__toESM', {
26
+ enumerable: true,
27
+ get: function () {
28
+ return __toESM;
29
+ }
30
+ });
package/dist/index.cjs ADDED
@@ -0,0 +1,327 @@
1
+ const require_chunk = require('./chunk-CUT6urMc.cjs');
2
+ const __optique_core_annotations = require_chunk.__toESM(require("@optique/core/annotations"));
3
+ const __optique_core_dependency_runtime = require_chunk.__toESM(require("@optique/core/dependency-runtime"));
4
+ const __optique_core_extension = require_chunk.__toESM(require("@optique/core/extension"));
5
+ const __optique_core_fluent = require_chunk.__toESM(require("@optique/core/fluent"));
6
+ const __optique_core_message = require_chunk.__toESM(require("@optique/core/message"));
7
+
8
+ //#region src/context.ts
9
+ async function defaultKeyringSource(service, username) {
10
+ if (process.platform === "linux") {
11
+ const { readLinuxPassword } = await Promise.resolve().then(() => require("./linux-CtEKvJ1E.cjs"));
12
+ return await readLinuxPassword(service, username);
13
+ }
14
+ const { AsyncEntry } = await import("@napi-rs/keyring");
15
+ return await new AsyncEntry(service, username).getPassword() ?? void 0;
16
+ }
17
+ function getTypeName$1(value) {
18
+ if (value === null) return "null";
19
+ if (Array.isArray(value)) return "array";
20
+ return typeof value;
21
+ }
22
+ /**
23
+ * Creates a single-pass context for keyring-backed parser fallbacks.
24
+ *
25
+ * The selected source is snapshotted into parse annotations. Creating the
26
+ * context and collecting its annotations never reads the credential store.
27
+ * On Linux, passwords are read from Secret Service without switching stores
28
+ * on errors. Other platforms use `@napi-rs/keyring`.
29
+ *
30
+ * @param options Optional custom source configuration.
31
+ * @returns A keyring context with a unique annotation identity.
32
+ * @throws {TypeError} If `source` is present but is not a function.
33
+ * @since 1.3.0
34
+ */
35
+ function createKeyringContext(options = {}) {
36
+ const rawSource = options.source;
37
+ if (rawSource !== void 0 && typeof rawSource !== "function") throw new TypeError(`Expected source to be a function, but got: ${getTypeName$1(rawSource)}.`);
38
+ const source = rawSource ?? defaultKeyringSource;
39
+ const contextId = Symbol(`@optique/keyring context:${Math.random()}`);
40
+ return {
41
+ id: contextId,
42
+ source,
43
+ phase: "single-pass",
44
+ getAnnotations() {
45
+ return { [contextId]: { source } };
46
+ },
47
+ [Symbol.dispose]() {}
48
+ };
49
+ }
50
+
51
+ //#endregion
52
+ //#region src/internal.ts
53
+ function createRunLookup() {
54
+ const cacheByRun = /* @__PURE__ */ new WeakMap();
55
+ return (results, path, lookup) => {
56
+ if (results == null) return lookup();
57
+ let cache = cacheByRun.get(results);
58
+ if (cache == null) {
59
+ cache = {
60
+ values: /* @__PURE__ */ new Map(),
61
+ symbolIds: /* @__PURE__ */ new Map()
62
+ };
63
+ cacheByRun.set(results, cache);
64
+ }
65
+ const { values, symbolIds } = cache;
66
+ const key = (path ?? []).map((segment) => {
67
+ if (typeof segment === "symbol") {
68
+ let id = symbolIds.get(segment);
69
+ if (id == null) {
70
+ id = symbolIds.size;
71
+ symbolIds.set(segment, id);
72
+ }
73
+ return `y${id}:`;
74
+ }
75
+ const tag = typeof segment === "number" ? "n" : "s";
76
+ const text = String(segment);
77
+ return `${tag}${text.length}:${text}`;
78
+ }).join("");
79
+ const cached = values.get(key);
80
+ if (cached != null) return cached;
81
+ const pending = Promise.resolve().then(lookup);
82
+ values.set(key, pending);
83
+ return pending;
84
+ };
85
+ }
86
+ function withAnnotatedInnerState(sourceState, innerState, run, inheritPrimitiveAnnotations = false) {
87
+ const annotations = (0, __optique_core_annotations.getAnnotations)(sourceState);
88
+ const innerStateIsObject = innerState != null && typeof innerState === "object";
89
+ if (annotations == null || (0, __optique_core_annotations.getAnnotations)(innerState) != null || !innerStateIsObject && !inheritPrimitiveAnnotations) return run(innerState);
90
+ const inheritedState = (0, __optique_core_extension.inheritAnnotations)(sourceState, innerState);
91
+ if (inheritedState !== innerState) return run(inheritedState);
92
+ return innerStateIsObject ? run((0, __optique_core_extension.withAnnotationView)(innerState, annotations)) : run(innerState);
93
+ }
94
+
95
+ //#endregion
96
+ //#region src/parser.ts
97
+ const stateKey = Symbol("@optique/keyring/bindState");
98
+ function getTypeName(value) {
99
+ if (value === null) return "null";
100
+ if (Array.isArray(value)) return "array";
101
+ return typeof value;
102
+ }
103
+ function isSourceData(value) {
104
+ return value != null && typeof value === "object" && "source" in value && typeof value.source === "function";
105
+ }
106
+ /**
107
+ * Adds an asynchronous keyring fallback to a string parser.
108
+ *
109
+ * Values resolve in CLI, keyring, then inner-parser fallback order. The
110
+ * credential store is read only during demanded completion, and one lookup is
111
+ * shared by the same wrapper occurrence across all passes of a run.
112
+ * Stored-password validation failures use a generic message to keep the
113
+ * credential out of error output.
114
+ *
115
+ * @param parser String parser whose CLI behavior and fallback are preserved.
116
+ * @param options Keyring context and lookup identity.
117
+ * @returns An always-async fluent parser with keyring fallback behavior.
118
+ * @throws {TypeError} If `service` or `username` is not a string, or if stored
119
+ * password validation throws. Validation exceptions are replaced with a
120
+ * generic error without the original message or cause.
121
+ * @throws Propagates password-source and inner completion errors unchanged.
122
+ * @since 1.3.0
123
+ */
124
+ function bindKeyring(parser, options) {
125
+ if (typeof options.service !== "string") throw new TypeError(`Expected service to be a string, but got: ${getTypeName(options.service)}.`);
126
+ if (typeof options.username !== "string") throw new TypeError(`Expected username to be a string, but got: ${getTypeName(options.username)}.`);
127
+ const stateId = Symbol("@optique/keyring/binding");
128
+ const isBindState = (value) => value != null && typeof value === "object" && stateKey in value && value[stateKey] === stateId;
129
+ const parserInheritsAnnotations = (0, __optique_core_extension.getTraits)(parser).inheritsAnnotations === true;
130
+ const lookupOnce = createRunLookup();
131
+ const innerState = (state) => isBindState(state) ? state.cliState : parser.initialState;
132
+ const withInnerState = (state, run) => withAnnotatedInnerState(state, innerState(state), run, parserInheritsAnnotations);
133
+ const innerNodes = (state, path = []) => withInnerState(state, (annotatedState) => parser.getSuggestRuntimeNodes?.(annotatedState, path) ?? []);
134
+ const isInnerDemanded = (state, exec) => innerNodes(state, exec?.path).some((node) => {
135
+ const id = node.parser.dependencyMetadata?.source?.sourceId;
136
+ return id != null && exec?.effectfulCompletionSession?.demanded.has(id) === true;
137
+ });
138
+ const preparedKey = (path = []) => (0, __optique_core_dependency_runtime.serializeSchedulingPath)([stateId, ...path]);
139
+ const isPreparedInner = (value) => value != null && typeof value === "object" && stateKey in value && value[stateKey] === stateId;
140
+ const completeInner = (state, exec) => {
141
+ const prepared = exec?.effectfulCompletionSession?.preparedByPath.get(preparedKey(exec.path));
142
+ if (isPreparedInner(prepared)) return Promise.resolve(prepared.result);
143
+ return Promise.resolve(withInnerState(state, (annotatedState) => parser.complete(annotatedState, exec)));
144
+ };
145
+ const boundParser = {
146
+ mode: "async",
147
+ $valueType: parser.$valueType,
148
+ $stateType: [],
149
+ priority: parser.priority,
150
+ usage: parser.usage,
151
+ leadingNames: parser.leadingNames,
152
+ acceptingAnyToken: parser.acceptingAnyToken,
153
+ initialState: {
154
+ [stateKey]: stateId,
155
+ hasCliValue: false,
156
+ cliState: parser.initialState
157
+ },
158
+ canSkip(state, exec) {
159
+ if (!(isBindState(state) && state.hasCliValue) && isSourceData((0, __optique_core_annotations.getAnnotations)(state)?.[options.context.id])) return true;
160
+ return withInnerState(state, (annotatedState) => parser.canSkip?.(annotatedState, exec) === true);
161
+ },
162
+ getSuggestRuntimeNodes(state, path) {
163
+ return (0, __optique_core_extension.delegateSuggestNodes)(parser, boundParser, state, path, innerState(state), "prepend");
164
+ },
165
+ async parse(context) {
166
+ const annotations = (0, __optique_core_annotations.getAnnotations)(context.state);
167
+ const state = innerState(context.state);
168
+ const result = await withInnerState(context.state, (annotatedState) => parser.parse({
169
+ ...context,
170
+ state: annotatedState
171
+ }));
172
+ if (!result.success && result.consumed > 0) return result;
173
+ const consumedOnlyTerminator = result.success && !context.optionsTerminated && result.next.optionsTerminated && result.consumed.length === 1 && result.consumed[0] === "--";
174
+ const nextState = (0, __optique_core_extension.injectAnnotations)({
175
+ [stateKey]: stateId,
176
+ hasCliValue: isBindState(context.state) && context.state.hasCliValue || result.success && result.consumed.length > 0 && !consumedOnlyTerminator,
177
+ cliState: result.success ? result.next.state : state
178
+ }, annotations);
179
+ return {
180
+ success: true,
181
+ ...result.success && result.provisional ? { provisional: true } : {},
182
+ next: {
183
+ ...result.success ? result.next : context,
184
+ state: nextState
185
+ },
186
+ consumed: result.success ? result.consumed : []
187
+ };
188
+ },
189
+ async complete(state, exec) {
190
+ if (isBindState(state) && state.hasCliValue) return await completeInner(state, exec);
191
+ const annotations = (0, __optique_core_annotations.getAnnotations)(state);
192
+ const sourceData = annotations?.[options.context.id];
193
+ if (exec != null && exec.phase !== "complete") {
194
+ if (isSourceData(sourceData)) return {
195
+ success: true,
196
+ value: "",
197
+ deferred: true
198
+ };
199
+ return await completeInner(state, exec);
200
+ }
201
+ if (!isSourceData(sourceData)) {
202
+ const innerResult = await completeInner(state, exec);
203
+ return annotations != null && !innerResult.success ? {
204
+ success: false,
205
+ error: __optique_core_message.message`Keyring password could not be read: the keyring context was not passed to run()'s contexts option.`
206
+ } : innerResult;
207
+ }
208
+ const session = exec?.effectfulCompletionSession;
209
+ const sourceId = boundParser.dependencyMetadata?.source?.sourceId;
210
+ if (session?.policy === "demand-only" && (sourceId == null ? !isInnerDemanded(state, exec) : !session.demanded.has(sourceId))) return {
211
+ success: true,
212
+ value: "",
213
+ deferred: true
214
+ };
215
+ const value = await lookupOnce(session?.results, exec?.path, () => sourceData.source(options.service, options.username));
216
+ if (value === void 0) return await completeInner(state, exec);
217
+ let result;
218
+ try {
219
+ result = typeof parser.validateValue === "function" ? await parser.validateValue(value) : {
220
+ success: true,
221
+ value
222
+ };
223
+ } catch {
224
+ throw new TypeError("The password from the keyring could not be validated.");
225
+ }
226
+ if (!result.success) return {
227
+ success: false,
228
+ error: __optique_core_message.message`The password from the keyring failed validation.`
229
+ };
230
+ if (sourceId != null) session?.effectfulSources.add(sourceId);
231
+ return result;
232
+ },
233
+ async *suggest(context, prefix) {
234
+ const suggestions = withInnerState(context.state, (annotatedState) => parser.suggest({
235
+ ...context,
236
+ state: annotatedState
237
+ }, prefix));
238
+ yield* suggestions;
239
+ },
240
+ getDocFragments(state, upperDefaultValue) {
241
+ if (state.kind === "unavailable") return parser.getDocFragments(state, upperDefaultValue);
242
+ return withInnerState(state.state, (annotatedState) => parser.getDocFragments({
243
+ kind: "available",
244
+ state: annotatedState
245
+ }, upperDefaultValue));
246
+ },
247
+ ...typeof parser.shouldDeferCompletion === "function" ? { shouldDeferCompletion: (state, exec) => withInnerState(state, (annotatedState) => parser.shouldDeferCompletion?.(annotatedState, exec) === true) } : {}
248
+ };
249
+ (0, __optique_core_extension.defineTraits)(boundParser, {
250
+ inheritsAnnotations: true,
251
+ completesFromSource: true
252
+ });
253
+ if ("placeholder" in parser) Object.defineProperty(boundParser, "placeholder", {
254
+ get: () => parser.placeholder,
255
+ configurable: true,
256
+ enumerable: false
257
+ });
258
+ for (const hook of ["normalizeValue", "validateValue"]) if (typeof parser[hook] === "function") Object.defineProperty(boundParser, hook, {
259
+ value: parser[hook].bind(parser),
260
+ configurable: true,
261
+ enumerable: false
262
+ });
263
+ const dependencyMetadata = (0, __optique_core_extension.mapSourceMetadata)(parser, (source) => ({
264
+ ...source,
265
+ extractSourceValue: (state) => {
266
+ if (!(isBindState(state) && state.hasCliValue) && isSourceData((0, __optique_core_annotations.getAnnotations)(state)?.[options.context.id])) return void 0;
267
+ return source.extractSourceValue(isBindState(state) ? state.cliState : state);
268
+ },
269
+ completeSource: source.preservesSourceValue === false ? void 0 : async (state, exec) => await boundParser.complete(isBindState(state) ? state : (0, __optique_core_extension.injectAnnotations)(boundParser.initialState, (0, __optique_core_annotations.getAnnotations)(state)), exec)
270
+ }));
271
+ if (dependencyMetadata != null) Object.defineProperty(boundParser, "dependencyMetadata", {
272
+ value: dependencyMetadata,
273
+ configurable: true,
274
+ enumerable: false
275
+ });
276
+ (0, __optique_core_dependency_runtime.defineForwardedEffectfulSchedulingNodes)(boundParser, parser, (state) => withInnerState(state, (annotatedState) => annotatedState));
277
+ const schedulingNodes = boundParser[__optique_core_dependency_runtime.effectfulSchedulingNodesKey];
278
+ if (schedulingNodes != null) Object.defineProperty(boundParser, __optique_core_dependency_runtime.effectfulSchedulingNodesKey, {
279
+ value: ((state, path) => {
280
+ const sourceData = (0, __optique_core_annotations.getAnnotations)(state)?.[options.context.id];
281
+ if (isBindState(state) && state.hasCliValue || !isSourceData(sourceData)) return schedulingNodes(state, path);
282
+ const nodes = innerNodes(state, path);
283
+ const providesSourceIds = /* @__PURE__ */ new Set();
284
+ const dependencyIds = /* @__PURE__ */ new Set();
285
+ for (const node of nodes) {
286
+ const metadata = node.parser.dependencyMetadata;
287
+ if (metadata?.source != null) providesSourceIds.add(metadata.source.sourceId);
288
+ for (const id of metadata?.completion?.dependencyIds ?? []) dependencyIds.add(id);
289
+ }
290
+ if (providesSourceIds.size === 0) return [];
291
+ return [{
292
+ path: path ?? [],
293
+ parser: {},
294
+ state,
295
+ providesSourceIds,
296
+ barrierCompletionDependencies: {
297
+ orderingDependencyIds: [...dependencyIds].filter((id) => !providesSourceIds.has(id)),
298
+ demandEdges: []
299
+ },
300
+ prepare: async ({ exec, runtime }) => {
301
+ if (exec == null) return;
302
+ if (exec.effectfulCompletionSession?.policy === "demand-only" && !isInnerDemanded(state, exec)) return;
303
+ const value = await lookupOnce(exec.effectfulCompletionSession?.results, path, () => sourceData.source(options.service, options.username));
304
+ if (value !== void 0) return;
305
+ const result = await completeInner(isBindState(state) ? state : (0, __optique_core_extension.injectAnnotations)(boundParser.initialState, (0, __optique_core_annotations.getAnnotations)(state)), {
306
+ ...exec,
307
+ path: path ?? [],
308
+ dependencyRuntime: runtime,
309
+ dependencyRegistry: runtime.registry
310
+ });
311
+ if (!result.success || !result.deferred) exec.effectfulCompletionSession?.preparedByPath.set(preparedKey(path), {
312
+ [stateKey]: stateId,
313
+ result
314
+ });
315
+ return result.success ? void 0 : result;
316
+ }
317
+ }];
318
+ }),
319
+ configurable: true,
320
+ enumerable: false
321
+ });
322
+ return (0, __optique_core_fluent.fluent)(boundParser);
323
+ }
324
+
325
+ //#endregion
326
+ exports.bindKeyring = bindKeyring;
327
+ exports.createKeyringContext = createKeyringContext;
@@ -0,0 +1,90 @@
1
+ import { SourceContext } from "@optique/core/context";
2
+ import { FluentParser } from "@optique/core/fluent";
3
+ import { Mode, Parser } from "@optique/core/parser";
4
+
5
+ //#region src/context.d.ts
6
+
7
+ /**
8
+ * Asynchronous source for reading an OS credential-store password.
9
+ *
10
+ * @param service Service name associated with the password.
11
+ * @param username Username associated with the password.
12
+ * @returns The stored password, or `undefined` when no password is available.
13
+ * @throws Propagates credential-store loading and lookup failures.
14
+ * @since 1.3.0
15
+ */
16
+ type KeyringSource = (service: string, username: string) => Promise<string | undefined>;
17
+ /**
18
+ * Options for creating a keyring context.
19
+ *
20
+ * @since 1.3.0
21
+ */
22
+ interface KeyringContextOptions {
23
+ /** Custom password source, primarily for alternate backends and tests. */
24
+ readonly source?: KeyringSource;
25
+ }
26
+ /**
27
+ * Context that provides an asynchronous keyring source to bound parsers.
28
+ *
29
+ * @since 1.3.0
30
+ */
31
+ interface KeyringContext extends SourceContext {
32
+ /** Password source captured by this context. */
33
+ readonly source: KeyringSource;
34
+ }
35
+ /**
36
+ * Creates a single-pass context for keyring-backed parser fallbacks.
37
+ *
38
+ * The selected source is snapshotted into parse annotations. Creating the
39
+ * context and collecting its annotations never reads the credential store.
40
+ * On Linux, passwords are read from Secret Service without switching stores
41
+ * on errors. Other platforms use `@napi-rs/keyring`.
42
+ *
43
+ * @param options Optional custom source configuration.
44
+ * @returns A keyring context with a unique annotation identity.
45
+ * @throws {TypeError} If `source` is present but is not a function.
46
+ * @since 1.3.0
47
+ */
48
+ declare function createKeyringContext(options?: KeyringContextOptions): KeyringContext;
49
+ //#endregion
50
+ //#region src/parser.d.ts
51
+ declare const stateKey: unique symbol;
52
+ interface BindState<TState> {
53
+ readonly [stateKey]: symbol;
54
+ readonly hasCliValue: boolean;
55
+ readonly cliState: TState;
56
+ }
57
+ /**
58
+ * Options for binding a string parser to an OS credential-store password.
59
+ *
60
+ * @since 1.3.0
61
+ */
62
+ interface BindKeyringOptions {
63
+ /** Registered keyring context that supplies the password source. */
64
+ readonly context: KeyringContext;
65
+ /** Service name forwarded to the password source. */
66
+ readonly service: string;
67
+ /** Username forwarded to the password source. */
68
+ readonly username: string;
69
+ }
70
+ /**
71
+ * Adds an asynchronous keyring fallback to a string parser.
72
+ *
73
+ * Values resolve in CLI, keyring, then inner-parser fallback order. The
74
+ * credential store is read only during demanded completion, and one lookup is
75
+ * shared by the same wrapper occurrence across all passes of a run.
76
+ * Stored-password validation failures use a generic message to keep the
77
+ * credential out of error output.
78
+ *
79
+ * @param parser String parser whose CLI behavior and fallback are preserved.
80
+ * @param options Keyring context and lookup identity.
81
+ * @returns An always-async fluent parser with keyring fallback behavior.
82
+ * @throws {TypeError} If `service` or `username` is not a string, or if stored
83
+ * password validation throws. Validation exceptions are replaced with a
84
+ * generic error without the original message or cause.
85
+ * @throws Propagates password-source and inner completion errors unchanged.
86
+ * @since 1.3.0
87
+ */
88
+ declare function bindKeyring<M extends Mode, TState>(parser: Parser<M, string, TState>, options: BindKeyringOptions): FluentParser<"async", string, BindState<TState>>;
89
+ //#endregion
90
+ export { type BindKeyringOptions, type KeyringContext, type KeyringContextOptions, type KeyringSource, bindKeyring, createKeyringContext };
@@ -0,0 +1,90 @@
1
+ import { FluentParser } from "@optique/core/fluent";
2
+ import { SourceContext } from "@optique/core/context";
3
+ import { Mode, Parser } from "@optique/core/parser";
4
+
5
+ //#region src/context.d.ts
6
+
7
+ /**
8
+ * Asynchronous source for reading an OS credential-store password.
9
+ *
10
+ * @param service Service name associated with the password.
11
+ * @param username Username associated with the password.
12
+ * @returns The stored password, or `undefined` when no password is available.
13
+ * @throws Propagates credential-store loading and lookup failures.
14
+ * @since 1.3.0
15
+ */
16
+ type KeyringSource = (service: string, username: string) => Promise<string | undefined>;
17
+ /**
18
+ * Options for creating a keyring context.
19
+ *
20
+ * @since 1.3.0
21
+ */
22
+ interface KeyringContextOptions {
23
+ /** Custom password source, primarily for alternate backends and tests. */
24
+ readonly source?: KeyringSource;
25
+ }
26
+ /**
27
+ * Context that provides an asynchronous keyring source to bound parsers.
28
+ *
29
+ * @since 1.3.0
30
+ */
31
+ interface KeyringContext extends SourceContext {
32
+ /** Password source captured by this context. */
33
+ readonly source: KeyringSource;
34
+ }
35
+ /**
36
+ * Creates a single-pass context for keyring-backed parser fallbacks.
37
+ *
38
+ * The selected source is snapshotted into parse annotations. Creating the
39
+ * context and collecting its annotations never reads the credential store.
40
+ * On Linux, passwords are read from Secret Service without switching stores
41
+ * on errors. Other platforms use `@napi-rs/keyring`.
42
+ *
43
+ * @param options Optional custom source configuration.
44
+ * @returns A keyring context with a unique annotation identity.
45
+ * @throws {TypeError} If `source` is present but is not a function.
46
+ * @since 1.3.0
47
+ */
48
+ declare function createKeyringContext(options?: KeyringContextOptions): KeyringContext;
49
+ //#endregion
50
+ //#region src/parser.d.ts
51
+ declare const stateKey: unique symbol;
52
+ interface BindState<TState> {
53
+ readonly [stateKey]: symbol;
54
+ readonly hasCliValue: boolean;
55
+ readonly cliState: TState;
56
+ }
57
+ /**
58
+ * Options for binding a string parser to an OS credential-store password.
59
+ *
60
+ * @since 1.3.0
61
+ */
62
+ interface BindKeyringOptions {
63
+ /** Registered keyring context that supplies the password source. */
64
+ readonly context: KeyringContext;
65
+ /** Service name forwarded to the password source. */
66
+ readonly service: string;
67
+ /** Username forwarded to the password source. */
68
+ readonly username: string;
69
+ }
70
+ /**
71
+ * Adds an asynchronous keyring fallback to a string parser.
72
+ *
73
+ * Values resolve in CLI, keyring, then inner-parser fallback order. The
74
+ * credential store is read only during demanded completion, and one lookup is
75
+ * shared by the same wrapper occurrence across all passes of a run.
76
+ * Stored-password validation failures use a generic message to keep the
77
+ * credential out of error output.
78
+ *
79
+ * @param parser String parser whose CLI behavior and fallback are preserved.
80
+ * @param options Keyring context and lookup identity.
81
+ * @returns An always-async fluent parser with keyring fallback behavior.
82
+ * @throws {TypeError} If `service` or `username` is not a string, or if stored
83
+ * password validation throws. Validation exceptions are replaced with a
84
+ * generic error without the original message or cause.
85
+ * @throws Propagates password-source and inner completion errors unchanged.
86
+ * @since 1.3.0
87
+ */
88
+ declare function bindKeyring<M extends Mode, TState>(parser: Parser<M, string, TState>, options: BindKeyringOptions): FluentParser<"async", string, BindState<TState>>;
89
+ //#endregion
90
+ export { type BindKeyringOptions, type KeyringContext, type KeyringContextOptions, type KeyringSource, bindKeyring, createKeyringContext };
package/dist/index.js ADDED
@@ -0,0 +1,325 @@
1
+ import { getAnnotations } from "@optique/core/annotations";
2
+ import { defineForwardedEffectfulSchedulingNodes, effectfulSchedulingNodesKey, serializeSchedulingPath } from "@optique/core/dependency-runtime";
3
+ import { defineTraits, delegateSuggestNodes, getTraits, inheritAnnotations, injectAnnotations, mapSourceMetadata, withAnnotationView } from "@optique/core/extension";
4
+ import { fluent } from "@optique/core/fluent";
5
+ import { message } from "@optique/core/message";
6
+
7
+ //#region src/context.ts
8
+ async function defaultKeyringSource(service, username) {
9
+ if (process.platform === "linux") {
10
+ const { readLinuxPassword } = await import("./linux-C36sbmnq.js");
11
+ return await readLinuxPassword(service, username);
12
+ }
13
+ const { AsyncEntry } = await import("@napi-rs/keyring");
14
+ return await new AsyncEntry(service, username).getPassword() ?? void 0;
15
+ }
16
+ function getTypeName$1(value) {
17
+ if (value === null) return "null";
18
+ if (Array.isArray(value)) return "array";
19
+ return typeof value;
20
+ }
21
+ /**
22
+ * Creates a single-pass context for keyring-backed parser fallbacks.
23
+ *
24
+ * The selected source is snapshotted into parse annotations. Creating the
25
+ * context and collecting its annotations never reads the credential store.
26
+ * On Linux, passwords are read from Secret Service without switching stores
27
+ * on errors. Other platforms use `@napi-rs/keyring`.
28
+ *
29
+ * @param options Optional custom source configuration.
30
+ * @returns A keyring context with a unique annotation identity.
31
+ * @throws {TypeError} If `source` is present but is not a function.
32
+ * @since 1.3.0
33
+ */
34
+ function createKeyringContext(options = {}) {
35
+ const rawSource = options.source;
36
+ if (rawSource !== void 0 && typeof rawSource !== "function") throw new TypeError(`Expected source to be a function, but got: ${getTypeName$1(rawSource)}.`);
37
+ const source = rawSource ?? defaultKeyringSource;
38
+ const contextId = Symbol(`@optique/keyring context:${Math.random()}`);
39
+ return {
40
+ id: contextId,
41
+ source,
42
+ phase: "single-pass",
43
+ getAnnotations() {
44
+ return { [contextId]: { source } };
45
+ },
46
+ [Symbol.dispose]() {}
47
+ };
48
+ }
49
+
50
+ //#endregion
51
+ //#region src/internal.ts
52
+ function createRunLookup() {
53
+ const cacheByRun = /* @__PURE__ */ new WeakMap();
54
+ return (results, path, lookup) => {
55
+ if (results == null) return lookup();
56
+ let cache = cacheByRun.get(results);
57
+ if (cache == null) {
58
+ cache = {
59
+ values: /* @__PURE__ */ new Map(),
60
+ symbolIds: /* @__PURE__ */ new Map()
61
+ };
62
+ cacheByRun.set(results, cache);
63
+ }
64
+ const { values, symbolIds } = cache;
65
+ const key = (path ?? []).map((segment) => {
66
+ if (typeof segment === "symbol") {
67
+ let id = symbolIds.get(segment);
68
+ if (id == null) {
69
+ id = symbolIds.size;
70
+ symbolIds.set(segment, id);
71
+ }
72
+ return `y${id}:`;
73
+ }
74
+ const tag = typeof segment === "number" ? "n" : "s";
75
+ const text = String(segment);
76
+ return `${tag}${text.length}:${text}`;
77
+ }).join("");
78
+ const cached = values.get(key);
79
+ if (cached != null) return cached;
80
+ const pending = Promise.resolve().then(lookup);
81
+ values.set(key, pending);
82
+ return pending;
83
+ };
84
+ }
85
+ function withAnnotatedInnerState(sourceState, innerState, run, inheritPrimitiveAnnotations = false) {
86
+ const annotations = getAnnotations(sourceState);
87
+ const innerStateIsObject = innerState != null && typeof innerState === "object";
88
+ if (annotations == null || getAnnotations(innerState) != null || !innerStateIsObject && !inheritPrimitiveAnnotations) return run(innerState);
89
+ const inheritedState = inheritAnnotations(sourceState, innerState);
90
+ if (inheritedState !== innerState) return run(inheritedState);
91
+ return innerStateIsObject ? run(withAnnotationView(innerState, annotations)) : run(innerState);
92
+ }
93
+
94
+ //#endregion
95
+ //#region src/parser.ts
96
+ const stateKey = Symbol("@optique/keyring/bindState");
97
+ function getTypeName(value) {
98
+ if (value === null) return "null";
99
+ if (Array.isArray(value)) return "array";
100
+ return typeof value;
101
+ }
102
+ function isSourceData(value) {
103
+ return value != null && typeof value === "object" && "source" in value && typeof value.source === "function";
104
+ }
105
+ /**
106
+ * Adds an asynchronous keyring fallback to a string parser.
107
+ *
108
+ * Values resolve in CLI, keyring, then inner-parser fallback order. The
109
+ * credential store is read only during demanded completion, and one lookup is
110
+ * shared by the same wrapper occurrence across all passes of a run.
111
+ * Stored-password validation failures use a generic message to keep the
112
+ * credential out of error output.
113
+ *
114
+ * @param parser String parser whose CLI behavior and fallback are preserved.
115
+ * @param options Keyring context and lookup identity.
116
+ * @returns An always-async fluent parser with keyring fallback behavior.
117
+ * @throws {TypeError} If `service` or `username` is not a string, or if stored
118
+ * password validation throws. Validation exceptions are replaced with a
119
+ * generic error without the original message or cause.
120
+ * @throws Propagates password-source and inner completion errors unchanged.
121
+ * @since 1.3.0
122
+ */
123
+ function bindKeyring(parser, options) {
124
+ if (typeof options.service !== "string") throw new TypeError(`Expected service to be a string, but got: ${getTypeName(options.service)}.`);
125
+ if (typeof options.username !== "string") throw new TypeError(`Expected username to be a string, but got: ${getTypeName(options.username)}.`);
126
+ const stateId = Symbol("@optique/keyring/binding");
127
+ const isBindState = (value) => value != null && typeof value === "object" && stateKey in value && value[stateKey] === stateId;
128
+ const parserInheritsAnnotations = getTraits(parser).inheritsAnnotations === true;
129
+ const lookupOnce = createRunLookup();
130
+ const innerState = (state) => isBindState(state) ? state.cliState : parser.initialState;
131
+ const withInnerState = (state, run) => withAnnotatedInnerState(state, innerState(state), run, parserInheritsAnnotations);
132
+ const innerNodes = (state, path = []) => withInnerState(state, (annotatedState) => parser.getSuggestRuntimeNodes?.(annotatedState, path) ?? []);
133
+ const isInnerDemanded = (state, exec) => innerNodes(state, exec?.path).some((node) => {
134
+ const id = node.parser.dependencyMetadata?.source?.sourceId;
135
+ return id != null && exec?.effectfulCompletionSession?.demanded.has(id) === true;
136
+ });
137
+ const preparedKey = (path = []) => serializeSchedulingPath([stateId, ...path]);
138
+ const isPreparedInner = (value) => value != null && typeof value === "object" && stateKey in value && value[stateKey] === stateId;
139
+ const completeInner = (state, exec) => {
140
+ const prepared = exec?.effectfulCompletionSession?.preparedByPath.get(preparedKey(exec.path));
141
+ if (isPreparedInner(prepared)) return Promise.resolve(prepared.result);
142
+ return Promise.resolve(withInnerState(state, (annotatedState) => parser.complete(annotatedState, exec)));
143
+ };
144
+ const boundParser = {
145
+ mode: "async",
146
+ $valueType: parser.$valueType,
147
+ $stateType: [],
148
+ priority: parser.priority,
149
+ usage: parser.usage,
150
+ leadingNames: parser.leadingNames,
151
+ acceptingAnyToken: parser.acceptingAnyToken,
152
+ initialState: {
153
+ [stateKey]: stateId,
154
+ hasCliValue: false,
155
+ cliState: parser.initialState
156
+ },
157
+ canSkip(state, exec) {
158
+ if (!(isBindState(state) && state.hasCliValue) && isSourceData(getAnnotations(state)?.[options.context.id])) return true;
159
+ return withInnerState(state, (annotatedState) => parser.canSkip?.(annotatedState, exec) === true);
160
+ },
161
+ getSuggestRuntimeNodes(state, path) {
162
+ return delegateSuggestNodes(parser, boundParser, state, path, innerState(state), "prepend");
163
+ },
164
+ async parse(context) {
165
+ const annotations = getAnnotations(context.state);
166
+ const state = innerState(context.state);
167
+ const result = await withInnerState(context.state, (annotatedState) => parser.parse({
168
+ ...context,
169
+ state: annotatedState
170
+ }));
171
+ if (!result.success && result.consumed > 0) return result;
172
+ const consumedOnlyTerminator = result.success && !context.optionsTerminated && result.next.optionsTerminated && result.consumed.length === 1 && result.consumed[0] === "--";
173
+ const nextState = injectAnnotations({
174
+ [stateKey]: stateId,
175
+ hasCliValue: isBindState(context.state) && context.state.hasCliValue || result.success && result.consumed.length > 0 && !consumedOnlyTerminator,
176
+ cliState: result.success ? result.next.state : state
177
+ }, annotations);
178
+ return {
179
+ success: true,
180
+ ...result.success && result.provisional ? { provisional: true } : {},
181
+ next: {
182
+ ...result.success ? result.next : context,
183
+ state: nextState
184
+ },
185
+ consumed: result.success ? result.consumed : []
186
+ };
187
+ },
188
+ async complete(state, exec) {
189
+ if (isBindState(state) && state.hasCliValue) return await completeInner(state, exec);
190
+ const annotations = getAnnotations(state);
191
+ const sourceData = annotations?.[options.context.id];
192
+ if (exec != null && exec.phase !== "complete") {
193
+ if (isSourceData(sourceData)) return {
194
+ success: true,
195
+ value: "",
196
+ deferred: true
197
+ };
198
+ return await completeInner(state, exec);
199
+ }
200
+ if (!isSourceData(sourceData)) {
201
+ const innerResult = await completeInner(state, exec);
202
+ return annotations != null && !innerResult.success ? {
203
+ success: false,
204
+ error: message`Keyring password could not be read: the keyring context was not passed to run()'s contexts option.`
205
+ } : innerResult;
206
+ }
207
+ const session = exec?.effectfulCompletionSession;
208
+ const sourceId = boundParser.dependencyMetadata?.source?.sourceId;
209
+ if (session?.policy === "demand-only" && (sourceId == null ? !isInnerDemanded(state, exec) : !session.demanded.has(sourceId))) return {
210
+ success: true,
211
+ value: "",
212
+ deferred: true
213
+ };
214
+ const value = await lookupOnce(session?.results, exec?.path, () => sourceData.source(options.service, options.username));
215
+ if (value === void 0) return await completeInner(state, exec);
216
+ let result;
217
+ try {
218
+ result = typeof parser.validateValue === "function" ? await parser.validateValue(value) : {
219
+ success: true,
220
+ value
221
+ };
222
+ } catch {
223
+ throw new TypeError("The password from the keyring could not be validated.");
224
+ }
225
+ if (!result.success) return {
226
+ success: false,
227
+ error: message`The password from the keyring failed validation.`
228
+ };
229
+ if (sourceId != null) session?.effectfulSources.add(sourceId);
230
+ return result;
231
+ },
232
+ async *suggest(context, prefix) {
233
+ const suggestions = withInnerState(context.state, (annotatedState) => parser.suggest({
234
+ ...context,
235
+ state: annotatedState
236
+ }, prefix));
237
+ yield* suggestions;
238
+ },
239
+ getDocFragments(state, upperDefaultValue) {
240
+ if (state.kind === "unavailable") return parser.getDocFragments(state, upperDefaultValue);
241
+ return withInnerState(state.state, (annotatedState) => parser.getDocFragments({
242
+ kind: "available",
243
+ state: annotatedState
244
+ }, upperDefaultValue));
245
+ },
246
+ ...typeof parser.shouldDeferCompletion === "function" ? { shouldDeferCompletion: (state, exec) => withInnerState(state, (annotatedState) => parser.shouldDeferCompletion?.(annotatedState, exec) === true) } : {}
247
+ };
248
+ defineTraits(boundParser, {
249
+ inheritsAnnotations: true,
250
+ completesFromSource: true
251
+ });
252
+ if ("placeholder" in parser) Object.defineProperty(boundParser, "placeholder", {
253
+ get: () => parser.placeholder,
254
+ configurable: true,
255
+ enumerable: false
256
+ });
257
+ for (const hook of ["normalizeValue", "validateValue"]) if (typeof parser[hook] === "function") Object.defineProperty(boundParser, hook, {
258
+ value: parser[hook].bind(parser),
259
+ configurable: true,
260
+ enumerable: false
261
+ });
262
+ const dependencyMetadata = mapSourceMetadata(parser, (source) => ({
263
+ ...source,
264
+ extractSourceValue: (state) => {
265
+ if (!(isBindState(state) && state.hasCliValue) && isSourceData(getAnnotations(state)?.[options.context.id])) return void 0;
266
+ return source.extractSourceValue(isBindState(state) ? state.cliState : state);
267
+ },
268
+ completeSource: source.preservesSourceValue === false ? void 0 : async (state, exec) => await boundParser.complete(isBindState(state) ? state : injectAnnotations(boundParser.initialState, getAnnotations(state)), exec)
269
+ }));
270
+ if (dependencyMetadata != null) Object.defineProperty(boundParser, "dependencyMetadata", {
271
+ value: dependencyMetadata,
272
+ configurable: true,
273
+ enumerable: false
274
+ });
275
+ defineForwardedEffectfulSchedulingNodes(boundParser, parser, (state) => withInnerState(state, (annotatedState) => annotatedState));
276
+ const schedulingNodes = boundParser[effectfulSchedulingNodesKey];
277
+ if (schedulingNodes != null) Object.defineProperty(boundParser, effectfulSchedulingNodesKey, {
278
+ value: ((state, path) => {
279
+ const sourceData = getAnnotations(state)?.[options.context.id];
280
+ if (isBindState(state) && state.hasCliValue || !isSourceData(sourceData)) return schedulingNodes(state, path);
281
+ const nodes = innerNodes(state, path);
282
+ const providesSourceIds = /* @__PURE__ */ new Set();
283
+ const dependencyIds = /* @__PURE__ */ new Set();
284
+ for (const node of nodes) {
285
+ const metadata = node.parser.dependencyMetadata;
286
+ if (metadata?.source != null) providesSourceIds.add(metadata.source.sourceId);
287
+ for (const id of metadata?.completion?.dependencyIds ?? []) dependencyIds.add(id);
288
+ }
289
+ if (providesSourceIds.size === 0) return [];
290
+ return [{
291
+ path: path ?? [],
292
+ parser: {},
293
+ state,
294
+ providesSourceIds,
295
+ barrierCompletionDependencies: {
296
+ orderingDependencyIds: [...dependencyIds].filter((id) => !providesSourceIds.has(id)),
297
+ demandEdges: []
298
+ },
299
+ prepare: async ({ exec, runtime }) => {
300
+ if (exec == null) return;
301
+ if (exec.effectfulCompletionSession?.policy === "demand-only" && !isInnerDemanded(state, exec)) return;
302
+ const value = await lookupOnce(exec.effectfulCompletionSession?.results, path, () => sourceData.source(options.service, options.username));
303
+ if (value !== void 0) return;
304
+ const result = await completeInner(isBindState(state) ? state : injectAnnotations(boundParser.initialState, getAnnotations(state)), {
305
+ ...exec,
306
+ path: path ?? [],
307
+ dependencyRuntime: runtime,
308
+ dependencyRegistry: runtime.registry
309
+ });
310
+ if (!result.success || !result.deferred) exec.effectfulCompletionSession?.preparedByPath.set(preparedKey(path), {
311
+ [stateKey]: stateId,
312
+ result
313
+ });
314
+ return result.success ? void 0 : result;
315
+ }
316
+ }];
317
+ }),
318
+ configurable: true,
319
+ enumerable: false
320
+ });
321
+ return fluent(boundParser);
322
+ }
323
+
324
+ //#endregion
325
+ export { bindKeyring, createKeyringContext };
@@ -0,0 +1,81 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createDecipheriv, createDiffieHellman, hkdfSync } from "node:crypto";
3
+ import { clearTimeout, setTimeout } from "node:timers";
4
+ import { DBusError, Message, Variant, sessionBus } from "@jellybrick/dbus-next";
5
+
6
+ //#region src/linux.ts
7
+ const destination = "org.freedesktop.secrets";
8
+ const servicePath = "/org/freedesktop/secrets";
9
+ const serviceInterface = "org.freedesktop.Secret.Service";
10
+ const algorithm = "dh-ietf1024-sha256-aes128-cbc-pkcs7";
11
+ const prime = Buffer.from("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF", "hex");
12
+ /**
13
+ * Reads one matching Secret Service item without selecting another store.
14
+ * @param service Service attribute of the credential.
15
+ * @param username Username attribute of the credential.
16
+ * @param bus Connection owned and closed by this lookup.
17
+ * @returns The password, or undefined after a successful search with no match.
18
+ * @throws If the service is inaccessible, the item is locked or ambiguous,
19
+ * or the session or password cannot be read.
20
+ * @internal
21
+ */
22
+ async function readLinuxPassword(service, username, bus = sessionBus()) {
23
+ let timer;
24
+ const unavailable = new Promise((_, reject) => {
25
+ const events = bus;
26
+ events.on("error", (error) => reject(error));
27
+ timer = setTimeout(() => {
28
+ reject(new DOMException("The Secret Service password lookup timed out.", "TimeoutError"));
29
+ }, 3e4);
30
+ });
31
+ try {
32
+ return await Promise.race([readPassword(bus, service, username), unavailable]);
33
+ } finally {
34
+ clearTimeout(timer);
35
+ bus.disconnect();
36
+ }
37
+ }
38
+ async function call(bus, path, iface, member, signature, body, replySignature) {
39
+ const reply = await bus.call(new Message({
40
+ destination,
41
+ path,
42
+ interface: iface,
43
+ member,
44
+ signature,
45
+ body: [...body]
46
+ }));
47
+ if (reply == null || reply.signature !== replySignature) throw new TypeError("The Secret Service returned an invalid response.");
48
+ return reply.body;
49
+ }
50
+ async function readPassword(bus, service, username) {
51
+ const [unlocked, locked] = await call(bus, servicePath, serviceInterface, "SearchItems", "a{ss}", [{
52
+ service,
53
+ username
54
+ }], "aoao");
55
+ if (!isPaths(unlocked) || !isPaths(locked)) throw new TypeError("The Secret Service returned invalid search results.");
56
+ const paths = [...new Set([...unlocked, ...locked])];
57
+ if (paths.length === 0) return void 0;
58
+ if (paths.length > 1) throw new Error("More than one credential matches the service and username.");
59
+ if (locked.length > 0) throw new DBusError("org.freedesktop.Secret.Error.IsLocked", "The matching Secret Service credential is locked.");
60
+ const dh = createDiffieHellman(prime, 2);
61
+ const [output, session] = await call(bus, servicePath, serviceInterface, "OpenSession", "sv", [algorithm, new Variant("ay", dh.generateKeys())], "vo");
62
+ if (!(output instanceof Variant) || output.signature !== "ay" || !isBytes(output.value) || typeof session !== "string" || session === "/") throw new TypeError("The Secret Service returned an invalid session.");
63
+ const key = Buffer.from(hkdfSync("sha256", dh.computeSecret(output.value), "", "", 16));
64
+ const [secret] = await call(bus, paths[0], "org.freedesktop.Secret.Item", "GetSecret", "o", [session], "(oayays)");
65
+ if (!Array.isArray(secret) || secret.length !== 4 || secret[0] !== session || !isBytes(secret[1]) || secret[1].length !== 16 || !isBytes(secret[2]) || typeof secret[3] !== "string") throw new TypeError("The Secret Service returned an invalid secret.");
66
+ const decipher = createDecipheriv("aes-128-cbc", key, secret[1]);
67
+ const password = Buffer.concat([decipher.update(secret[2]), decipher.final()]);
68
+ return new TextDecoder("utf-8", {
69
+ fatal: true,
70
+ ignoreBOM: true
71
+ }).decode(password);
72
+ }
73
+ function isPaths(value) {
74
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
75
+ }
76
+ function isBytes(value) {
77
+ return value instanceof Uint8Array;
78
+ }
79
+
80
+ //#endregion
81
+ export { readLinuxPassword };
@@ -0,0 +1,82 @@
1
+ const require_chunk = require('./chunk-CUT6urMc.cjs');
2
+ const node_buffer = require_chunk.__toESM(require("node:buffer"));
3
+ const node_crypto = require_chunk.__toESM(require("node:crypto"));
4
+ const node_timers = require_chunk.__toESM(require("node:timers"));
5
+ const __jellybrick_dbus_next = require_chunk.__toESM(require("@jellybrick/dbus-next"));
6
+
7
+ //#region src/linux.ts
8
+ const destination = "org.freedesktop.secrets";
9
+ const servicePath = "/org/freedesktop/secrets";
10
+ const serviceInterface = "org.freedesktop.Secret.Service";
11
+ const algorithm = "dh-ietf1024-sha256-aes128-cbc-pkcs7";
12
+ const prime = node_buffer.Buffer.from("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF", "hex");
13
+ /**
14
+ * Reads one matching Secret Service item without selecting another store.
15
+ * @param service Service attribute of the credential.
16
+ * @param username Username attribute of the credential.
17
+ * @param bus Connection owned and closed by this lookup.
18
+ * @returns The password, or undefined after a successful search with no match.
19
+ * @throws If the service is inaccessible, the item is locked or ambiguous,
20
+ * or the session or password cannot be read.
21
+ * @internal
22
+ */
23
+ async function readLinuxPassword(service, username, bus = (0, __jellybrick_dbus_next.sessionBus)()) {
24
+ let timer;
25
+ const unavailable = new Promise((_, reject) => {
26
+ const events = bus;
27
+ events.on("error", (error) => reject(error));
28
+ timer = (0, node_timers.setTimeout)(() => {
29
+ reject(new DOMException("The Secret Service password lookup timed out.", "TimeoutError"));
30
+ }, 3e4);
31
+ });
32
+ try {
33
+ return await Promise.race([readPassword(bus, service, username), unavailable]);
34
+ } finally {
35
+ (0, node_timers.clearTimeout)(timer);
36
+ bus.disconnect();
37
+ }
38
+ }
39
+ async function call(bus, path, iface, member, signature, body, replySignature) {
40
+ const reply = await bus.call(new __jellybrick_dbus_next.Message({
41
+ destination,
42
+ path,
43
+ interface: iface,
44
+ member,
45
+ signature,
46
+ body: [...body]
47
+ }));
48
+ if (reply == null || reply.signature !== replySignature) throw new TypeError("The Secret Service returned an invalid response.");
49
+ return reply.body;
50
+ }
51
+ async function readPassword(bus, service, username) {
52
+ const [unlocked, locked] = await call(bus, servicePath, serviceInterface, "SearchItems", "a{ss}", [{
53
+ service,
54
+ username
55
+ }], "aoao");
56
+ if (!isPaths(unlocked) || !isPaths(locked)) throw new TypeError("The Secret Service returned invalid search results.");
57
+ const paths = [...new Set([...unlocked, ...locked])];
58
+ if (paths.length === 0) return void 0;
59
+ if (paths.length > 1) throw new Error("More than one credential matches the service and username.");
60
+ if (locked.length > 0) throw new __jellybrick_dbus_next.DBusError("org.freedesktop.Secret.Error.IsLocked", "The matching Secret Service credential is locked.");
61
+ const dh = (0, node_crypto.createDiffieHellman)(prime, 2);
62
+ const [output, session] = await call(bus, servicePath, serviceInterface, "OpenSession", "sv", [algorithm, new __jellybrick_dbus_next.Variant("ay", dh.generateKeys())], "vo");
63
+ if (!(output instanceof __jellybrick_dbus_next.Variant) || output.signature !== "ay" || !isBytes(output.value) || typeof session !== "string" || session === "/") throw new TypeError("The Secret Service returned an invalid session.");
64
+ const key = node_buffer.Buffer.from((0, node_crypto.hkdfSync)("sha256", dh.computeSecret(output.value), "", "", 16));
65
+ const [secret] = await call(bus, paths[0], "org.freedesktop.Secret.Item", "GetSecret", "o", [session], "(oayays)");
66
+ if (!Array.isArray(secret) || secret.length !== 4 || secret[0] !== session || !isBytes(secret[1]) || secret[1].length !== 16 || !isBytes(secret[2]) || typeof secret[3] !== "string") throw new TypeError("The Secret Service returned an invalid secret.");
67
+ const decipher = (0, node_crypto.createDecipheriv)("aes-128-cbc", key, secret[1]);
68
+ const password = node_buffer.Buffer.concat([decipher.update(secret[2]), decipher.final()]);
69
+ return new TextDecoder("utf-8", {
70
+ fatal: true,
71
+ ignoreBOM: true
72
+ }).decode(password);
73
+ }
74
+ function isPaths(value) {
75
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
76
+ }
77
+ function isBytes(value) {
78
+ return value instanceof Uint8Array;
79
+ }
80
+
81
+ //#endregion
82
+ exports.readLinuxPassword = readLinuxPassword;
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@optique/keyring",
3
+ "version": "1.3.0-dev.0",
4
+ "description": "OS credential-store support for Optique",
5
+ "keywords": [
6
+ "CLI",
7
+ "command-line",
8
+ "commandline",
9
+ "parser",
10
+ "keyring",
11
+ "credential",
12
+ "password"
13
+ ],
14
+ "license": "MIT",
15
+ "author": {
16
+ "name": "Hong Minhee",
17
+ "email": "hong@minhee.org",
18
+ "url": "https://hongminhee.org/"
19
+ },
20
+ "homepage": "https://optique.dev/",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/dahlia/optique.git",
24
+ "directory": "packages/keyring/"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/dahlia/optique/issues"
28
+ },
29
+ "funding": [
30
+ "https://github.com/sponsors/dahlia"
31
+ ],
32
+ "engines": {
33
+ "node": ">=20.0.0",
34
+ "bun": ">=1.2.0",
35
+ "deno": ">=2.3.0"
36
+ },
37
+ "files": [
38
+ "dist/",
39
+ "package.json",
40
+ "README.md"
41
+ ],
42
+ "type": "module",
43
+ "module": "./dist/index.js",
44
+ "main": "./dist/index.cjs",
45
+ "types": "./dist/index.d.ts",
46
+ "exports": {
47
+ ".": {
48
+ "types": {
49
+ "import": "./dist/index.d.ts",
50
+ "require": "./dist/index.d.cts"
51
+ },
52
+ "import": "./dist/index.js",
53
+ "require": "./dist/index.cjs"
54
+ }
55
+ },
56
+ "imports": {
57
+ "#src/*.ts": {
58
+ "node": "./dist/*.js",
59
+ "default": "./src/*.ts"
60
+ }
61
+ },
62
+ "sideEffects": false,
63
+ "dependencies": {
64
+ "@jellybrick/dbus-next": "0.11.3",
65
+ "@napi-rs/keyring": "2.0.0",
66
+ "@optique/core": "1.3.0"
67
+ },
68
+ "devDependencies": {
69
+ "@optique/env": "1.3.0",
70
+ "@types/node": "^24.0.0",
71
+ "tsdown": "^0.13.0",
72
+ "typescript": "^5.8.3"
73
+ },
74
+ "scripts": {
75
+ "build": "tsdown",
76
+ "prepublish": "tsdown",
77
+ "test": "node --test",
78
+ "test:bun": "bun test",
79
+ "test:deno": "deno test --allow-read --allow-write --allow-env --allow-run --allow-net --allow-sys",
80
+ "test-all": "tsdown && node --test && bun test && deno test --allow-read --allow-write --allow-env --allow-run --allow-net --allow-sys"
81
+ }
82
+ }