@scopewatch/secrets 0.0.1

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.
@@ -0,0 +1,21 @@
1
+ export type SecretErrorCategory = 'no_keychain_backend' | 'item_not_found' | 'not_a_tty' | 'prompt_cancelled' | 'unrecognized';
2
+ export declare class SecretError extends Error {
3
+ category: SecretErrorCategory;
4
+ detail: string | undefined;
5
+ constructor(category: SecretErrorCategory, message: string, detail?: string);
6
+ }
7
+ /**
8
+ * Deliberate design decision (see CLAUDE.md Phase D): when no real OS keychain
9
+ * backend is available (e.g. headless Linux with no Secret Service/D-Bus session
10
+ * running), Scopewatch refuses to store secrets rather than falling back to an
11
+ * encrypted file. Any such fallback would require a custom encryption key stored
12
+ * *somewhere*, which recreates the exact "no plaintext anywhere" problem it's
13
+ * meant to solve. Failing loudly with a clear recovery path is the safe default
14
+ * required by the brief's UX rule 4, not a limitation to engineer around.
15
+ */
16
+ export declare function noKeychainBackendError(platform: string, detail: string): SecretError;
17
+ export declare function itemNotFoundError(ref: string): SecretError;
18
+ export declare function notATtyError(): SecretError;
19
+ export declare function promptCancelledError(): SecretError;
20
+ export declare function unrecognizedSecretError(rawOutput: string): SecretError;
21
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAC3B,qBAAqB,GACrB,gBAAgB,GAChB,WAAW,GACX,kBAAkB,GAClB,cAAc,CAAC;AAEnB,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;gBAEf,QAAQ,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAM5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CASpF;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAE1D;AAED,wBAAgB,YAAY,IAAI,WAAW,CAM1C;AAED,wBAAgB,oBAAoB,IAAI,WAAW,CAElD;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,CAMtE"}
package/dist/errors.js ADDED
@@ -0,0 +1,43 @@
1
+ export class SecretError extends Error {
2
+ category;
3
+ detail;
4
+ constructor(category, message, detail) {
5
+ super(message);
6
+ this.name = 'SecretError';
7
+ this.category = category;
8
+ this.detail = detail;
9
+ }
10
+ }
11
+ /**
12
+ * Deliberate design decision (see CLAUDE.md Phase D): when no real OS keychain
13
+ * backend is available (e.g. headless Linux with no Secret Service/D-Bus session
14
+ * running), Scopewatch refuses to store secrets rather than falling back to an
15
+ * encrypted file. Any such fallback would require a custom encryption key stored
16
+ * *somewhere*, which recreates the exact "no plaintext anywhere" problem it's
17
+ * meant to solve. Failing loudly with a clear recovery path is the safe default
18
+ * required by the brief's UX rule 4, not a limitation to engineer around.
19
+ */
20
+ export function noKeychainBackendError(platform, detail) {
21
+ return new SecretError('no_keychain_backend', `No system keychain backend is available on this ${platform} machine (${detail}). ` +
22
+ `Scopewatch will not store secrets without a real OS keychain backing them - install ` +
23
+ `and run a Secret Service provider (e.g. gnome-keyring) within a session that has one, ` +
24
+ `then retry.`, detail);
25
+ }
26
+ export function itemNotFoundError(ref) {
27
+ return new SecretError('item_not_found', `No secret found in the system keychain for '${ref}'.`);
28
+ }
29
+ export function notATtyError() {
30
+ return new SecretError('not_a_tty', `Cannot securely prompt for a secret: stdin is not an interactive terminal. ` +
31
+ `Run this command directly in a terminal, not piped or redirected.`);
32
+ }
33
+ export function promptCancelledError() {
34
+ return new SecretError('prompt_cancelled', `Secret entry was cancelled.`);
35
+ }
36
+ export function unrecognizedSecretError(rawOutput) {
37
+ return new SecretError('unrecognized', `Keychain operation failed with an unrecognized error: ${truncate(rawOutput, 300)}`, rawOutput);
38
+ }
39
+ function truncate(s, max) {
40
+ const oneLine = s.replace(/\s+/g, ' ').trim();
41
+ return oneLine.length > max ? oneLine.slice(0, max) + '...' : oneLine;
42
+ }
43
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAOA,MAAM,OAAO,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAsB;IAC9B,MAAM,CAAqB;IAE3B,YAAY,QAA6B,EAAE,OAAe,EAAE,MAAe;QACzE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAAgB,EAAE,MAAc;IACrE,OAAO,IAAI,WAAW,CACpB,qBAAqB,EACrB,mDAAmD,QAAQ,aAAa,MAAM,KAAK;QACjF,sFAAsF;QACtF,wFAAwF;QACxF,aAAa,EACf,MAAM,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,IAAI,WAAW,CAAC,gBAAgB,EAAE,+CAA+C,GAAG,IAAI,CAAC,CAAC;AACnG,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,IAAI,WAAW,CACpB,WAAW,EACX,6EAA6E;QAC3E,mEAAmE,CACtE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,IAAI,WAAW,CAAC,kBAAkB,EAAE,6BAA6B,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,SAAiB;IACvD,OAAO,IAAI,WAAW,CACpB,cAAc,EACd,yDAAyD,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,EACnF,SAAS,CACV,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS,EAAE,GAAW;IACtC,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9C,OAAO,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;AACxE,CAAC"}
@@ -0,0 +1,10 @@
1
+ export { secretRef, KEYCHAIN_SERVICE_NAME } from './secret-ref.js';
2
+ export { storeSecret, retrieveSecret, deleteSecret, injectSecrets } from './keychain.js';
3
+ export type { KeychainResult, RetrieveResult } from './keychain.js';
4
+ export { promptSecret } from './prompt.js';
5
+ export type { PromptStreams } from './prompt.js';
6
+ export { createLogger, redact, registerLiveSecret, unregisterLiveSecret, _clearAllLiveSecretsForTesting, } from './logger.js';
7
+ export type { Logger, LogLevel, LogSink } from './logger.js';
8
+ export { SecretError, noKeychainBackendError, itemNotFoundError, notATtyError, promptCancelledError, unrecognizedSecretError, } from './errors.js';
9
+ export type { SecretErrorCategory } from './errors.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACzF,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EACL,YAAY,EACZ,MAAM,EACN,kBAAkB,EAClB,oBAAoB,EACpB,8BAA8B,GAC/B,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EACL,WAAW,EACX,sBAAsB,EACtB,iBAAiB,EACjB,YAAY,EACZ,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { secretRef, KEYCHAIN_SERVICE_NAME } from './secret-ref.js';
2
+ export { storeSecret, retrieveSecret, deleteSecret, injectSecrets } from './keychain.js';
3
+ export { promptSecret } from './prompt.js';
4
+ export { createLogger, redact, registerLiveSecret, unregisterLiveSecret, _clearAllLiveSecretsForTesting, } from './logger.js';
5
+ export { SecretError, noKeychainBackendError, itemNotFoundError, notATtyError, promptCancelledError, unrecognizedSecretError, } from './errors.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEzF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EACL,YAAY,EACZ,MAAM,EACN,kBAAkB,EAClB,oBAAoB,EACpB,8BAA8B,GAC/B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,WAAW,EACX,sBAAsB,EACtB,iBAAiB,EACjB,YAAY,EACZ,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { SecretError } from './errors.js';
2
+ export type SpawnFn = (command: string, args: string[], options: {
3
+ input?: string;
4
+ }) => {
5
+ status: number | null;
6
+ stdout: string;
7
+ stderr: string;
8
+ spawnError?: NodeJS.ErrnoException | undefined;
9
+ };
10
+ export declare function linuxStore(ref: string, value: string, spawn?: SpawnFn): SecretError | null;
11
+ export declare function linuxRetrieve(ref: string, spawn?: SpawnFn): {
12
+ value: string;
13
+ } | {
14
+ error: SecretError;
15
+ };
16
+ export declare function linuxDelete(ref: string, spawn?: SpawnFn): SecretError | null;
17
+ //# sourceMappingURL=keychain-linux.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain-linux.d.ts","sourceRoot":"","sources":["../src/keychain-linux.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE9G,MAAM,MAAM,OAAO,GAAG,CACpB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KACxB;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC,cAAc,GAAG,SAAS,CAAA;CAAE,CAAC;AA+B/G,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,OAAsB,GAAG,WAAW,GAAG,IAAI,CAYxG;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,GAAE,OAAsB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,WAAW,CAAA;CAAE,CAapH;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,GAAE,OAAsB,GAAG,WAAW,GAAG,IAAI,CAQ1F"}
@@ -0,0 +1,62 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { KEYCHAIN_SERVICE_NAME } from './secret-ref.js';
3
+ import { itemNotFoundError, noKeychainBackendError, unrecognizedSecretError } from './errors.js';
4
+ const defaultSpawn = (command, args, options) => {
5
+ const result = spawnSync(command, args, { input: options.input, encoding: 'utf-8' });
6
+ return {
7
+ status: result.status,
8
+ stdout: result.stdout ?? '',
9
+ stderr: result.stderr ?? '',
10
+ spawnError: result.error,
11
+ };
12
+ };
13
+ /**
14
+ * `secret-tool` (from libsecret-tools) talks to the Secret Service API over
15
+ * D-Bus. On headless/CI Linux with no Secret Service daemon running (no
16
+ * gnome-keyring, no D-Bus session), either the binary itself is missing
17
+ * (ENOENT) or it runs but fails to connect - both are detected here and
18
+ * produce noKeychainBackendError rather than any file-based fallback (see
19
+ * errors.ts doc comment: a fallback would require its own encryption key
20
+ * stored somewhere, recreating the exact problem it claims to solve).
21
+ */
22
+ function detectNoBackend(result) {
23
+ if (result.spawnError?.code === 'ENOENT') {
24
+ return 'secret-tool is not installed (part of the libsecret-tools / libsecret-utils package)';
25
+ }
26
+ if (/no such interface|cannot autolaunch|couldn'?t connect|not provided by any \.service|org\.freedesktop\.secrets/i.test(result.stderr)) {
27
+ return 'no D-Bus Secret Service is running (no gnome-keyring or equivalent available in this session)';
28
+ }
29
+ return null;
30
+ }
31
+ export function linuxStore(ref, value, spawn = defaultSpawn) {
32
+ const result = spawn('secret-tool', ['store', '--label', `scopewatch:${ref}`, 'service', KEYCHAIN_SERVICE_NAME, 'account', ref], { input: `${value}\n` });
33
+ const backendGap = detectNoBackend(result);
34
+ if (backendGap)
35
+ return noKeychainBackendError('Linux', backendGap);
36
+ if (result.status === 0)
37
+ return null;
38
+ return unrecognizedSecretError(result.stderr);
39
+ }
40
+ export function linuxRetrieve(ref, spawn = defaultSpawn) {
41
+ const result = spawn('secret-tool', ['lookup', 'service', KEYCHAIN_SERVICE_NAME, 'account', ref], {});
42
+ const backendGap = detectNoBackend(result);
43
+ if (backendGap)
44
+ return { error: noKeychainBackendError('Linux', backendGap) };
45
+ if (result.status === 0 && result.stdout.length > 0) {
46
+ return { value: result.stdout.replace(/\n$/, '') };
47
+ }
48
+ if (result.status !== 0) {
49
+ return { error: itemNotFoundError(ref) };
50
+ }
51
+ return { error: unrecognizedSecretError(result.stderr) };
52
+ }
53
+ export function linuxDelete(ref, spawn = defaultSpawn) {
54
+ const result = spawn('secret-tool', ['clear', 'service', KEYCHAIN_SERVICE_NAME, 'account', ref], {});
55
+ const backendGap = detectNoBackend(result);
56
+ if (backendGap)
57
+ return noKeychainBackendError('Linux', backendGap);
58
+ if (result.status === 0)
59
+ return null;
60
+ return itemNotFoundError(ref);
61
+ }
62
+ //# sourceMappingURL=keychain-linux.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain-linux.js","sourceRoot":"","sources":["../src/keychain-linux.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,uBAAuB,EAAe,MAAM,aAAa,CAAC;AAQ9G,MAAM,YAAY,GAAY,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IACvD,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;IACrF,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;QAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;QAC3B,UAAU,EAAE,MAAM,CAAC,KAA0C;KAC9D,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,MAAiG;IACxH,IAAI,MAAM,CAAC,UAAU,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;QACzC,OAAO,sFAAsF,CAAC;IAChG,CAAC;IACD,IAAI,gHAAgH,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QACzI,OAAO,+FAA+F,CAAC;IACzG,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,KAAa,EAAE,QAAiB,YAAY;IAClF,MAAM,MAAM,GAAG,KAAK,CAClB,aAAa,EACb,CAAC,OAAO,EAAE,SAAS,EAAE,cAAc,GAAG,EAAE,EAAE,SAAS,EAAE,qBAAqB,EAAE,SAAS,EAAE,GAAG,CAAC,EAC3F,EAAE,KAAK,EAAE,GAAG,KAAK,IAAI,EAAE,CACxB,CAAC;IAEF,MAAM,UAAU,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAC3C,IAAI,UAAU;QAAE,OAAO,sBAAsB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAEnE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,uBAAuB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,QAAiB,YAAY;IACtE,MAAM,MAAM,GAAG,KAAK,CAAC,aAAa,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,qBAAqB,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;IAEtG,MAAM,UAAU,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAC3C,IAAI,UAAU;QAAE,OAAO,EAAE,KAAK,EAAE,sBAAsB,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;IAE9E,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpD,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,EAAE,KAAK,EAAE,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;IAC3C,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,uBAAuB,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,QAAiB,YAAY;IACpE,MAAM,MAAM,GAAG,KAAK,CAAC,aAAa,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;IAErG,MAAM,UAAU,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAC3C,IAAI,UAAU;QAAE,OAAO,sBAAsB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAEnE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,iBAAiB,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { SecretError } from './errors.js';
2
+ /** Injected for testability - avoids spawning the real `security` binary in unit tests. */
3
+ export type SpawnFn = (command: string, args: string[], options: {
4
+ input?: string;
5
+ }) => {
6
+ status: number | null;
7
+ stdout: string;
8
+ stderr: string;
9
+ };
10
+ /**
11
+ * Store a secret via macOS Keychain Services, using the `security` CLI.
12
+ *
13
+ * CRITICAL: `-w` must be the truly-last token in argv with NO value following
14
+ * it (confirmed via `security add-generic-password -h`: "Use of the -p or -w
15
+ * options is insecure. Specify -w as the last option to be prompted."). This
16
+ * makes `security` read the password from stdin instead (as a confirm+retype
17
+ * pair, "value\nvalue\n") rather than accepting it as a command-line argument -
18
+ * verified empirically on this dev machine: `security` with `-w <value>` puts
19
+ * the value in argv, visible via `ps`/Activity Monitor for the life of the
20
+ * subprocess; `-w` alone does not. Do not "simplify" this back to `-w <value>`.
21
+ */
22
+ export declare function macosStore(ref: string, value: string, spawn?: SpawnFn): SecretError | null;
23
+ export declare function macosRetrieve(ref: string, spawn?: SpawnFn): {
24
+ value: string;
25
+ } | {
26
+ error: SecretError;
27
+ };
28
+ export declare function macosDelete(ref: string, spawn?: SpawnFn): SecretError | null;
29
+ //# sourceMappingURL=keychain-macos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain-macos.d.ts","sourceRoot":"","sources":["../src/keychain-macos.ts"],"names":[],"mappings":"AAEA,OAAO,EAA8C,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtF,2FAA2F;AAC3F,MAAM,MAAM,OAAO,GAAG,CACpB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KACxB;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAO/D;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,OAAsB,GAAG,WAAW,GAAG,IAAI,CASxG;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,GAAE,OAAsB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,WAAW,CAAA;CAAE,CAUpH;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,GAAE,OAAsB,GAAG,WAAW,GAAG,IAAI,CAQ1F"}
@@ -0,0 +1,45 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { KEYCHAIN_SERVICE_NAME } from './secret-ref.js';
3
+ import { itemNotFoundError, unrecognizedSecretError } from './errors.js';
4
+ const defaultSpawn = (command, args, options) => {
5
+ const result = spawnSync(command, args, { input: options.input, encoding: 'utf-8' });
6
+ return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
7
+ };
8
+ /**
9
+ * Store a secret via macOS Keychain Services, using the `security` CLI.
10
+ *
11
+ * CRITICAL: `-w` must be the truly-last token in argv with NO value following
12
+ * it (confirmed via `security add-generic-password -h`: "Use of the -p or -w
13
+ * options is insecure. Specify -w as the last option to be prompted."). This
14
+ * makes `security` read the password from stdin instead (as a confirm+retype
15
+ * pair, "value\nvalue\n") rather than accepting it as a command-line argument -
16
+ * verified empirically on this dev machine: `security` with `-w <value>` puts
17
+ * the value in argv, visible via `ps`/Activity Monitor for the life of the
18
+ * subprocess; `-w` alone does not. Do not "simplify" this back to `-w <value>`.
19
+ */
20
+ export function macosStore(ref, value, spawn = defaultSpawn) {
21
+ const result = spawn('security', ['add-generic-password', '-a', ref, '-s', KEYCHAIN_SERVICE_NAME, '-U', '-w'], { input: `${value}\n${value}\n` });
22
+ if (result.status === 0)
23
+ return null;
24
+ return unrecognizedSecretError(result.stderr);
25
+ }
26
+ export function macosRetrieve(ref, spawn = defaultSpawn) {
27
+ const result = spawn('security', ['find-generic-password', '-a', ref, '-s', KEYCHAIN_SERVICE_NAME, '-w'], {});
28
+ if (result.status === 0) {
29
+ return { value: result.stdout.replace(/\n$/, '') };
30
+ }
31
+ if (/could not be found/i.test(result.stderr)) {
32
+ return { error: itemNotFoundError(ref) };
33
+ }
34
+ return { error: unrecognizedSecretError(result.stderr) };
35
+ }
36
+ export function macosDelete(ref, spawn = defaultSpawn) {
37
+ const result = spawn('security', ['delete-generic-password', '-a', ref, '-s', KEYCHAIN_SERVICE_NAME], {});
38
+ if (result.status === 0)
39
+ return null;
40
+ if (/could not be found/i.test(result.stderr)) {
41
+ return itemNotFoundError(ref);
42
+ }
43
+ return unrecognizedSecretError(result.stderr);
44
+ }
45
+ //# sourceMappingURL=keychain-macos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain-macos.js","sourceRoot":"","sources":["../src/keychain-macos.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAe,MAAM,aAAa,CAAC;AAStF,MAAM,YAAY,GAAY,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IACvD,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;IACrF,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;AAC7F,CAAC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,KAAa,EAAE,QAAiB,YAAY;IAClF,MAAM,MAAM,GAAG,KAAK,CAClB,UAAU,EACV,CAAC,sBAAsB,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,IAAI,CAAC,EAC5E,EAAE,KAAK,EAAE,GAAG,KAAK,KAAK,KAAK,IAAI,EAAE,CAClC,CAAC;IAEF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,uBAAuB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,QAAiB,YAAY;IACtE,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,EAAE,CAAC,uBAAuB,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,qBAAqB,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IAE9G,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,qBAAqB,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9C,OAAO,EAAE,KAAK,EAAE,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;IAC3C,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,uBAAuB,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,QAAiB,YAAY;IACpE,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,EAAE,CAAC,yBAAyB,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,qBAAqB,CAAC,EAAE,EAAE,CAAC,CAAC;IAE1G,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,IAAI,qBAAqB,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9C,OAAO,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,uBAAuB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { SecretError } from './errors.js';
2
+ /**
3
+ * IMPORTANT - unverified on real Windows: this repo's development environment
4
+ * is macOS (Darwin). This implementation is written strictly per the documented
5
+ * Win32 Credential Manager API contracts (CredWrite/CredRead/CredDelete in
6
+ * Advapi32.dll) and the standard PowerShell `Add-Type` P/Invoke pattern, but it
7
+ * has not been executed on a real Windows machine. It must be validated there
8
+ * before being relied on in production.
9
+ */
10
+ export type SpawnFn = (command: string, args: string[], options: {
11
+ input?: string;
12
+ }) => {
13
+ status: number | null;
14
+ stdout: string;
15
+ stderr: string;
16
+ };
17
+ export declare function windowsStore(ref: string, value: string, spawn?: SpawnFn): SecretError | null;
18
+ export declare function windowsRetrieve(ref: string, spawn?: SpawnFn): {
19
+ value: string;
20
+ } | {
21
+ error: SecretError;
22
+ };
23
+ export declare function windowsDelete(ref: string, spawn?: SpawnFn): SecretError | null;
24
+ //# sourceMappingURL=keychain-windows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain-windows.d.ts","sourceRoot":"","sources":["../src/keychain-windows.ts"],"names":[],"mappings":"AAEA,OAAO,EAA8C,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtF;;;;;;;GAOG;AAEH,MAAM,MAAM,OAAO,GAAG,CACpB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KACxB;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAgH/D,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,OAAsB,GAAG,WAAW,GAAG,IAAI,CAO1G;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,GAAE,OAAsB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,WAAW,CAAA;CAAE,CAMtH;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,GAAE,OAAsB,GAAG,WAAW,GAAG,IAAI,CAM5F"}
@@ -0,0 +1,130 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { itemNotFoundError, unrecognizedSecretError } from './errors.js';
3
+ const defaultSpawn = (command, args, options) => {
4
+ const result = spawnSync(command, args, { input: options.input, encoding: 'utf-8' });
5
+ return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
6
+ };
7
+ /**
8
+ * The P/Invoke glue, shared by store/retrieve/delete. Defined once via
9
+ * Add-Type so each invocation of powershell.exe only pays the compile cost once
10
+ * per process. TargetName is passed as a normal -TargetName argument (not
11
+ * sensitive - it's the "server_id:secret_id" reference, never the value).
12
+ *
13
+ * CRITICAL: the secret VALUE must never be interpolated into the -Command
14
+ * string or any argv element - that would expose it via Task Manager's
15
+ * "Command line" column / `Get-Process | Select CommandLine`, identically to
16
+ * the macOS argv exposure this design avoided. The store script instead reads
17
+ * the value from stdin via [Console]::In.ReadToEnd(), exactly as the macOS
18
+ * `security -w` (no value) and Linux `secret-tool store` paths do.
19
+ */
20
+ const CRED_MANAGER_TYPE = `
21
+ Add-Type -Namespace ScopewatchCred -Name Native -MemberDefinition @'
22
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
23
+ public struct CREDENTIAL {
24
+ public int Flags;
25
+ public int Type;
26
+ public string TargetName;
27
+ public string Comment;
28
+ public long LastWritten;
29
+ public int CredentialBlobSize;
30
+ public IntPtr CredentialBlob;
31
+ public int Persist;
32
+ public int AttributeCount;
33
+ public IntPtr Attributes;
34
+ public string TargetAlias;
35
+ public string UserName;
36
+ }
37
+
38
+ [DllImport("Advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
39
+ public static extern bool CredWrite(ref CREDENTIAL userCredential, uint flags);
40
+
41
+ [DllImport("Advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
42
+ public static extern bool CredRead(string target, int type, int reservedFlag, out IntPtr credentialPtr);
43
+
44
+ [DllImport("Advapi32.dll", SetLastError = true)]
45
+ public static extern bool CredDelete(string target, int type, int flags);
46
+
47
+ [DllImport("Advapi32.dll", SetLastError = true)]
48
+ public static extern bool CredFree(IntPtr credentialPtr);
49
+ '@
50
+ `;
51
+ const CRED_TYPE_GENERIC = 1;
52
+ const CRED_PERSIST_LOCAL_MACHINE = 2;
53
+ function storeScript(ref) {
54
+ return `
55
+ ${CRED_MANAGER_TYPE}
56
+ $secretValue = [Console]::In.ReadToEnd()
57
+ $secretValue = $secretValue.TrimEnd("\`r", "\`n")
58
+ $blobBytes = [System.Text.Encoding]::Unicode.GetBytes($secretValue)
59
+ $blobPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($blobBytes.Length)
60
+ [System.Runtime.InteropServices.Marshal]::Copy($blobBytes, 0, $blobPtr, $blobBytes.Length)
61
+ try {
62
+ $cred = New-Object ScopewatchCred.Native+CREDENTIAL
63
+ $cred.Type = ${CRED_TYPE_GENERIC}
64
+ $cred.TargetName = "${escapePs(ref)}"
65
+ $cred.CredentialBlobSize = $blobBytes.Length
66
+ $cred.CredentialBlob = $blobPtr
67
+ $cred.Persist = ${CRED_PERSIST_LOCAL_MACHINE}
68
+ $cred.UserName = "scopewatch"
69
+ $ok = [ScopewatchCred.Native]::CredWrite([ref]$cred, 0)
70
+ if (-not $ok) { exit 1 }
71
+ } finally {
72
+ [System.Runtime.InteropServices.Marshal]::FreeHGlobal($blobPtr)
73
+ }
74
+ exit 0
75
+ `;
76
+ }
77
+ function retrieveScript(ref) {
78
+ return `
79
+ ${CRED_MANAGER_TYPE}
80
+ $credPtr = [IntPtr]::Zero
81
+ $ok = [ScopewatchCred.Native]::CredRead("${escapePs(ref)}", ${CRED_TYPE_GENERIC}, 0, [ref]$credPtr)
82
+ if (-not $ok) { exit 2 }
83
+ try {
84
+ $cred = [System.Runtime.InteropServices.Marshal]::PtrToStructure($credPtr, [type][ScopewatchCred.Native+CREDENTIAL])
85
+ $bytes = New-Object byte[] $cred.CredentialBlobSize
86
+ [System.Runtime.InteropServices.Marshal]::Copy($cred.CredentialBlob, $bytes, 0, $cred.CredentialBlobSize)
87
+ $value = [System.Text.Encoding]::Unicode.GetString($bytes)
88
+ [Console]::Out.Write($value)
89
+ } finally {
90
+ [ScopewatchCred.Native]::CredFree($credPtr) | Out-Null
91
+ }
92
+ exit 0
93
+ `;
94
+ }
95
+ function deleteScript(ref) {
96
+ return `
97
+ ${CRED_MANAGER_TYPE}
98
+ $ok = [ScopewatchCred.Native]::CredDelete("${escapePs(ref)}", ${CRED_TYPE_GENERIC}, 0)
99
+ if (-not $ok) { exit 2 }
100
+ exit 0
101
+ `;
102
+ }
103
+ function escapePs(s) {
104
+ return s.replace(/"/g, '`"');
105
+ }
106
+ export function windowsStore(ref, value, spawn = defaultSpawn) {
107
+ const result = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', storeScript(ref)], {
108
+ input: `${value}\n`,
109
+ });
110
+ if (result.status === 0)
111
+ return null;
112
+ return unrecognizedSecretError(result.stderr);
113
+ }
114
+ export function windowsRetrieve(ref, spawn = defaultSpawn) {
115
+ const result = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', retrieveScript(ref)], {});
116
+ if (result.status === 0)
117
+ return { value: result.stdout };
118
+ if (result.status === 2)
119
+ return { error: itemNotFoundError(ref) };
120
+ return { error: unrecognizedSecretError(result.stderr) };
121
+ }
122
+ export function windowsDelete(ref, spawn = defaultSpawn) {
123
+ const result = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', deleteScript(ref)], {});
124
+ if (result.status === 0)
125
+ return null;
126
+ if (result.status === 2)
127
+ return itemNotFoundError(ref);
128
+ return unrecognizedSecretError(result.stderr);
129
+ }
130
+ //# sourceMappingURL=keychain-windows.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain-windows.js","sourceRoot":"","sources":["../src/keychain-windows.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import type { SecretError } from './errors.js';
2
+ export type KeychainResult = {
3
+ ok: true;
4
+ } | {
5
+ ok: false;
6
+ error: SecretError;
7
+ };
8
+ export type RetrieveResult = {
9
+ ok: true;
10
+ value: string;
11
+ } | {
12
+ ok: false;
13
+ error: SecretError;
14
+ };
15
+ export declare function storeSecret(ref: string, value: string): KeychainResult;
16
+ /**
17
+ * Retrieve a secret and register it for log redaction in the same call. The
18
+ * registration happens in a finally block around the platform-specific
19
+ * retrieval: if a value was actually obtained, it is guaranteed to be
20
+ * registered even if something else in this function throws afterward - a
21
+ * later log line can never mention a raw value that was successfully
22
+ * retrieved but not yet protected.
23
+ */
24
+ export declare function retrieveSecret(ref: string): RetrieveResult;
25
+ export declare function deleteSecret(ref: string): KeychainResult;
26
+ /**
27
+ * Retrieve multiple secrets for injection into a spawned server process's env.
28
+ * Each retrieval is independently registered for redaction (via retrieveSecret's
29
+ * own finally block) BEFORE this function's loop can move to the next ref or
30
+ * throw - so if retrieving secret #2 fails, secret #1's value (already
31
+ * retrieved) is still protected, not skipped because the batch as a whole
32
+ * later errors.
33
+ */
34
+ export declare function injectSecrets(refs: {
35
+ envVar: string;
36
+ ref: string;
37
+ }[]): {
38
+ ok: true;
39
+ env: Record<string, string>;
40
+ } | {
41
+ ok: false;
42
+ error: SecretError;
43
+ };
44
+ //# sourceMappingURL=keychain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain.d.ts","sourceRoot":"","sources":["../src/keychain.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAAC;AAC9E,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAAC;AAM7F,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,CAMtE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAc1D;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAKxD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAYpJ"}
@@ -0,0 +1,61 @@
1
+ import { macosStore, macosRetrieve, macosDelete } from './keychain-macos.js';
2
+ import { linuxStore, linuxRetrieve, linuxDelete } from './keychain-linux.js';
3
+ import { windowsStore, windowsRetrieve, windowsDelete } from './keychain-windows.js';
4
+ import { registerLiveSecret } from './logger.js';
5
+ function platform() {
6
+ return process.platform;
7
+ }
8
+ export function storeSecret(ref, value) {
9
+ const p = platform();
10
+ const error = p === 'darwin' ? macosStore(ref, value) : p === 'linux' ? linuxStore(ref, value) : windowsStore(ref, value);
11
+ return error ? { ok: false, error } : { ok: true };
12
+ }
13
+ /**
14
+ * Retrieve a secret and register it for log redaction in the same call. The
15
+ * registration happens in a finally block around the platform-specific
16
+ * retrieval: if a value was actually obtained, it is guaranteed to be
17
+ * registered even if something else in this function throws afterward - a
18
+ * later log line can never mention a raw value that was successfully
19
+ * retrieved but not yet protected.
20
+ */
21
+ export function retrieveSecret(ref) {
22
+ const p = platform();
23
+ let value;
24
+ try {
25
+ const result = p === 'darwin' ? macosRetrieve(ref) : p === 'linux' ? linuxRetrieve(ref) : windowsRetrieve(ref);
26
+ if ('error' in result) {
27
+ return { ok: false, error: result.error };
28
+ }
29
+ value = result.value;
30
+ return { ok: true, value: result.value };
31
+ }
32
+ finally {
33
+ if (value !== undefined)
34
+ registerLiveSecret(value);
35
+ }
36
+ }
37
+ export function deleteSecret(ref) {
38
+ const p = platform();
39
+ const error = p === 'darwin' ? macosDelete(ref) : p === 'linux' ? linuxDelete(ref) : windowsDelete(ref);
40
+ return error ? { ok: false, error } : { ok: true };
41
+ }
42
+ /**
43
+ * Retrieve multiple secrets for injection into a spawned server process's env.
44
+ * Each retrieval is independently registered for redaction (via retrieveSecret's
45
+ * own finally block) BEFORE this function's loop can move to the next ref or
46
+ * throw - so if retrieving secret #2 fails, secret #1's value (already
47
+ * retrieved) is still protected, not skipped because the batch as a whole
48
+ * later errors.
49
+ */
50
+ export function injectSecrets(refs) {
51
+ const env = {};
52
+ for (const { envVar, ref } of refs) {
53
+ const result = retrieveSecret(ref);
54
+ if (!result.ok) {
55
+ return { ok: false, error: result.error };
56
+ }
57
+ env[envVar] = result.value;
58
+ }
59
+ return { ok: true, env };
60
+ }
61
+ //# sourceMappingURL=keychain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain.js","sourceRoot":"","sources":["../src/keychain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Redaction-first structured logger. No logging module existed anywhere in the
3
+ * codebase before this - built fresh with redaction as a first-class feature,
4
+ * not retrofitted onto an existing logger.
5
+ *
6
+ * Redaction works by content, not by key name: any string that is currently
7
+ * registered as a "live secret value" gets replaced with [REDACTED] wherever it
8
+ * appears in a log line, including inside unrelated text (e.g. an error message
9
+ * that happens to embed the value) - not just when logged under an obviously
10
+ * secret-sounding field name.
11
+ *
12
+ * A short substring of unrelated log text can occasionally be over-redacted if
13
+ * it happens to match a live secret value. That's the safe failure direction
14
+ * (over-redaction, not under-redaction) and is accepted as-is.
15
+ */
16
+ /** Register a value for redaction in all subsequent log output. Idempotent. */
17
+ export declare function registerLiveSecret(value: string): void;
18
+ /** Stop redacting a value (e.g. once a process using it has exited). */
19
+ export declare function unregisterLiveSecret(value: string): void;
20
+ /** Exposed for tests only - never call in production code paths. */
21
+ export declare function _clearAllLiveSecretsForTesting(): void;
22
+ export declare function redact(text: string): string;
23
+ export type LogLevel = 'info' | 'warn' | 'error';
24
+ export type LogSink = (level: LogLevel, line: string) => void;
25
+ export type Logger = {
26
+ info: (...args: unknown[]) => void;
27
+ warn: (...args: unknown[]) => void;
28
+ error: (...args: unknown[]) => void;
29
+ };
30
+ /** Create a logger. Pass a custom sink for testing (capture output without touching real stdout/stderr). */
31
+ export declare function createLogger(sink?: LogSink): Logger;
32
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEtD;AAED,wEAAwE;AACxE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAExD;AAED,oEAAoE;AACpE,wBAAgB,8BAA8B,IAAI,IAAI,CAErD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO3C;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEjD,MAAM,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAE9D,MAAM,MAAM,MAAM,GAAG;IACnB,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IACnC,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IACnC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACrC,CAAC;AAUF,4GAA4G;AAC5G,wBAAgB,YAAY,CAAC,IAAI,GAAE,OAAqB,GAAG,MAAM,CAahE"}
package/dist/logger.js ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Redaction-first structured logger. No logging module existed anywhere in the
3
+ * codebase before this - built fresh with redaction as a first-class feature,
4
+ * not retrofitted onto an existing logger.
5
+ *
6
+ * Redaction works by content, not by key name: any string that is currently
7
+ * registered as a "live secret value" gets replaced with [REDACTED] wherever it
8
+ * appears in a log line, including inside unrelated text (e.g. an error message
9
+ * that happens to embed the value) - not just when logged under an obviously
10
+ * secret-sounding field name.
11
+ *
12
+ * A short substring of unrelated log text can occasionally be over-redacted if
13
+ * it happens to match a live secret value. That's the safe failure direction
14
+ * (over-redaction, not under-redaction) and is accepted as-is.
15
+ */
16
+ const liveSecrets = new Set();
17
+ /** Register a value for redaction in all subsequent log output. Idempotent. */
18
+ export function registerLiveSecret(value) {
19
+ if (value.length > 0)
20
+ liveSecrets.add(value);
21
+ }
22
+ /** Stop redacting a value (e.g. once a process using it has exited). */
23
+ export function unregisterLiveSecret(value) {
24
+ liveSecrets.delete(value);
25
+ }
26
+ /** Exposed for tests only - never call in production code paths. */
27
+ export function _clearAllLiveSecretsForTesting() {
28
+ liveSecrets.clear();
29
+ }
30
+ export function redact(text) {
31
+ let result = text;
32
+ for (const secret of liveSecrets) {
33
+ if (secret.length === 0)
34
+ continue;
35
+ result = result.split(secret).join('[REDACTED]');
36
+ }
37
+ return result;
38
+ }
39
+ const defaultSink = (level, line) => {
40
+ if (level === 'error') {
41
+ process.stderr.write(line + '\n');
42
+ }
43
+ else {
44
+ process.stdout.write(line + '\n');
45
+ }
46
+ };
47
+ /** Create a logger. Pass a custom sink for testing (capture output without touching real stdout/stderr). */
48
+ export function createLogger(sink = defaultSink) {
49
+ const write = (level, args) => {
50
+ const rendered = args
51
+ .map((a) => (typeof a === 'string' ? a : safeStringify(a)))
52
+ .join(' ');
53
+ sink(level, redact(rendered));
54
+ };
55
+ return {
56
+ info: (...args) => write('info', args),
57
+ warn: (...args) => write('warn', args),
58
+ error: (...args) => write('error', args),
59
+ };
60
+ }
61
+ function safeStringify(value) {
62
+ try {
63
+ return JSON.stringify(value);
64
+ }
65
+ catch {
66
+ return String(value);
67
+ }
68
+ }
69
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;AAEtC,+EAA+E;AAC/E,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC/C,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,8BAA8B;IAC5C,WAAW,CAAC,KAAK,EAAE,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,MAAM,MAAM,IAAI,WAAW,EAAE,CAAC;QACjC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAClC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAYD,MAAM,WAAW,GAAY,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;IAC3C,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;QACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACpC,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACpC,CAAC;AACH,CAAC,CAAC;AAEF,4GAA4G;AAC5G,MAAM,UAAU,YAAY,CAAC,OAAgB,WAAW;IACtD,MAAM,KAAK,GAAG,CAAC,KAAe,EAAE,IAAe,EAAE,EAAE;QACjD,MAAM,QAAQ,GAAG,IAAI;aAClB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;aAC1D,IAAI,CAAC,GAAG,CAAC,CAAC;QACb,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAChC,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC;QACtC,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC;QACtC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC;KACzC,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;AACH,CAAC"}
@@ -0,0 +1,19 @@
1
+ export type PromptStreams = {
2
+ input: NodeJS.ReadableStream & {
3
+ isTTY?: boolean;
4
+ };
5
+ output: NodeJS.WritableStream;
6
+ };
7
+ /**
8
+ * Securely prompt for a secret value: no terminal echo, and the value never
9
+ * appears on the command line or in shell history (it's only ever read as
10
+ * stdin during this interactive prompt).
11
+ *
12
+ * Requires a real TTY on stdin by default (checked via `isTTY`) - a non-TTY
13
+ * context (piped/redirected input) is refused rather than silently read some
14
+ * other way, since that would be an unaudited path that could bypass the
15
+ * "never printed/logged" guarantee this function exists to provide. Tests
16
+ * inject streams explicitly instead of relying on isTTY.
17
+ */
18
+ export declare function promptSecret(message: string, streams?: PromptStreams): Promise<string>;
19
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC,cAAc,GAAG;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACnD,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;CAC/B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCtF"}
package/dist/prompt.js ADDED
@@ -0,0 +1,52 @@
1
+ import * as readline from 'node:readline';
2
+ import { Writable } from 'node:stream';
3
+ import { notATtyError, promptCancelledError } from './errors.js';
4
+ /**
5
+ * Securely prompt for a secret value: no terminal echo, and the value never
6
+ * appears on the command line or in shell history (it's only ever read as
7
+ * stdin during this interactive prompt).
8
+ *
9
+ * Requires a real TTY on stdin by default (checked via `isTTY`) - a non-TTY
10
+ * context (piped/redirected input) is refused rather than silently read some
11
+ * other way, since that would be an unaudited path that could bypass the
12
+ * "never printed/logged" guarantee this function exists to provide. Tests
13
+ * inject streams explicitly instead of relying on isTTY.
14
+ */
15
+ export function promptSecret(message, streams) {
16
+ const input = streams?.input ?? process.stdin;
17
+ const output = streams?.output ?? process.stdout;
18
+ if (!streams && !process.stdin.isTTY) {
19
+ return Promise.reject(notATtyError());
20
+ }
21
+ return new Promise((resolve, reject) => {
22
+ // Mute the output stream's echo of typed characters while still allowing
23
+ // the prompt message itself to be written.
24
+ let muted = false;
25
+ const mutableOutput = new Writable({
26
+ write(chunk, _enc, callback) {
27
+ if (!muted)
28
+ output.write(chunk);
29
+ callback();
30
+ },
31
+ });
32
+ const rl = readline.createInterface({
33
+ input,
34
+ output: mutableOutput,
35
+ terminal: true,
36
+ });
37
+ output.write(message);
38
+ muted = true;
39
+ rl.question('', (answer) => {
40
+ muted = false;
41
+ rl.close();
42
+ output.write('\n');
43
+ if (answer.length === 0) {
44
+ reject(promptCancelledError());
45
+ }
46
+ else {
47
+ resolve(answer);
48
+ }
49
+ });
50
+ });
51
+ }
52
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAOjE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,OAAuB;IACnE,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC;IAC9C,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAEjD,IAAI,CAAC,OAAO,IAAI,CAAE,OAAO,CAAC,KAAa,CAAC,KAAK,EAAE,CAAC;QAC9C,OAAO,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC;IACxC,CAAC;IAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,yEAAyE;QACzE,2CAA2C;QAC3C,IAAI,KAAK,GAAG,KAAK,CAAC;QAClB,MAAM,aAAa,GAAG,IAAI,QAAQ,CAAC;YACjC,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ;gBACzB,IAAI,CAAC,KAAK;oBAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBAChC,QAAQ,EAAE,CAAC;YACb,CAAC;SACF,CAAC,CAAC;QAEH,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC;YAClC,KAAK;YACL,MAAM,EAAE,aAAa;YACrB,QAAQ,EAAE,IAAI;SACf,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACtB,KAAK,GAAG,IAAI,CAAC;QAEb,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE;YACzB,KAAK,GAAG,KAAK,CAAC;YACd,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACnB,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxB,MAAM,CAAC,oBAAoB,EAAE,CAAC,CAAC;YACjC,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,MAAM,CAAC,CAAC;YAClB,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The single code path allowed to construct a keychain account reference.
3
+ * Composite (server_id:secret_id) because the same secret id (e.g. "GITHUB_TOKEN")
4
+ * could otherwise collide across two unrelated servers that happen to name their
5
+ * secret the same thing. Store, retrieve, and delete must all go through this -
6
+ * never construct the string inline elsewhere, or store-time and lookup-time
7
+ * keys can silently drift apart.
8
+ */
9
+ export declare function secretRef(server_id: string, secret_id: string): string;
10
+ export declare const KEYCHAIN_SERVICE_NAME = "scopewatch";
11
+ //# sourceMappingURL=secret-ref.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-ref.d.ts","sourceRoot":"","sources":["../src/secret-ref.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,eAAO,MAAM,qBAAqB,eAAe,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The single code path allowed to construct a keychain account reference.
3
+ * Composite (server_id:secret_id) because the same secret id (e.g. "GITHUB_TOKEN")
4
+ * could otherwise collide across two unrelated servers that happen to name their
5
+ * secret the same thing. Store, retrieve, and delete must all go through this -
6
+ * never construct the string inline elsewhere, or store-time and lookup-time
7
+ * keys can silently drift apart.
8
+ */
9
+ export function secretRef(server_id, secret_id) {
10
+ return `${server_id}:${secret_id}`;
11
+ }
12
+ export const KEYCHAIN_SERVICE_NAME = 'scopewatch';
13
+ //# sourceMappingURL=secret-ref.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-ref.js","sourceRoot":"","sources":["../src/secret-ref.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,SAAiB,EAAE,SAAiB;IAC5D,OAAO,GAAG,SAAS,IAAI,SAAS,EAAE,CAAC;AACrC,CAAC;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG,YAAY,CAAC"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@scopewatch/secrets",
3
+ "version": "0.0.1",
4
+ "description": "OS keychain abstraction, secure secret prompt, and redaction-first logging",
5
+ "license": "MIT",
6
+ "author": "Sathvik Putta <puttasathvik16@gmail.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/PuttaSathvik16/scopewatch.git",
10
+ "directory": "packages/secrets"
11
+ },
12
+ "type": "module",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "import": "./dist/index.js",
18
+ "types": "./dist/index.d.ts"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "test": "node --import tsx --test 'test/**/*.test.ts'",
27
+ "clean": "rm -rf dist"
28
+ },
29
+ "devDependencies": {
30
+ "@scopewatch/state": "0.0.1",
31
+ "@types/node": "^22.0.0",
32
+ "typescript": "^5.7.0",
33
+ "tsx": "^4.19.0"
34
+ }
35
+ }