@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 +20 -0
- package/README.md +187 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/index.cjs +327 -0
- package/dist/index.d.cts +90 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +325 -0
- package/dist/linux-C36sbmnq.js +81 -0
- package/dist/linux-CtEKvJ1E.cjs +82 -0
- package/package.json +82 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|