@alpacakit/core 0.1.0-beta.35 → 0.1.0-beta.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/keyring/error.d.ts +33 -0
- package/dist/keyring/error.d.ts.map +1 -0
- package/dist/keyring/error.js +45 -0
- package/dist/keyring/index.d.ts +11 -0
- package/dist/keyring/index.d.ts.map +1 -0
- package/dist/keyring/index.js +8 -0
- package/dist/keyring/resolver.d.ts +16 -0
- package/dist/keyring/resolver.d.ts.map +1 -0
- package/dist/keyring/resolver.js +93 -0
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -16,3 +16,27 @@ Main exports: `createMapRegistry` / `createKeyedRegistry`,
|
|
|
16
16
|
`createSchemaRuntimeCodec` and the `schema.ts` helpers, `makeBrandedId` /
|
|
17
17
|
id factories, `produceImmutable` and the draft mutation helpers, and the
|
|
18
18
|
shared empty-value defaults.
|
|
19
|
+
|
|
20
|
+
## `@alpacakit/core/keyring`
|
|
21
|
+
|
|
22
|
+
The ONE module in the workspace that talks to an OS keychain. `@alpacakit/agents`,
|
|
23
|
+
`@alpacakit/integrations`, and the family convention packages above them all
|
|
24
|
+
reach the keychain through this subpath, so the native peer, the
|
|
25
|
+
absence-versus-failure rule, and the error type have a single owner.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { keyringResolver, keyringSecretStore } from "@alpacakit/core/keyring";
|
|
29
|
+
|
|
30
|
+
const store = keyringSecretStore({ service: "alpaca.agents" });
|
|
31
|
+
await store.set("profile:judge-codex:api-key", secret);
|
|
32
|
+
const resolver = keyringResolver({ service: "alpaca.agents" });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `get` returns `null` for an absent (or empty) entry; anything the keychain
|
|
36
|
+
throws surfaces as `KeyringError` with `code: "operation_failed"` — absence
|
|
37
|
+
is never inferred from a failure.
|
|
38
|
+
- `@napi-rs/keyring` is an **optional** peer, loaded lazily on first use. A
|
|
39
|
+
missing peer is `KeyringError` with `code: "peer_missing"`; both codes carry
|
|
40
|
+
the attempted `operation` (`read` / `write` / `delete`) and the `cause`.
|
|
41
|
+
- `module` accepts a loader so tests exercise the whole path without touching
|
|
42
|
+
a real keychain.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ValueOf } from "../types.js";
|
|
2
|
+
export declare const KEYRING_ERROR_CODE: {
|
|
3
|
+
/** The optional `@napi-rs/keyring` peer is absent or exports no `Entry`. */
|
|
4
|
+
readonly peerMissing: "peer_missing";
|
|
5
|
+
/** The OS keychain rejected or failed the operation — never "absent". */
|
|
6
|
+
readonly operationFailed: "operation_failed";
|
|
7
|
+
};
|
|
8
|
+
export type KeyringErrorCode = ValueOf<typeof KEYRING_ERROR_CODE>;
|
|
9
|
+
export declare const KEYRING_OPERATION: {
|
|
10
|
+
readonly read: "read";
|
|
11
|
+
readonly write: "write";
|
|
12
|
+
readonly delete: "delete";
|
|
13
|
+
};
|
|
14
|
+
export type KeyringOperation = ValueOf<typeof KEYRING_OPERATION>;
|
|
15
|
+
export type KeyringErrorOptions = {
|
|
16
|
+
readonly code: KeyringErrorCode;
|
|
17
|
+
readonly operation: KeyringOperation;
|
|
18
|
+
readonly message: string;
|
|
19
|
+
readonly cause?: unknown;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The single error type of the keychain boundary. `operation` records what
|
|
23
|
+
* was attempted (a missing peer is only ever discovered while attempting
|
|
24
|
+
* one), so a caller can report "could not write the secret" without
|
|
25
|
+
* re-deriving it from the call site.
|
|
26
|
+
*/
|
|
27
|
+
export declare class KeyringError extends Error {
|
|
28
|
+
readonly code: KeyringErrorCode;
|
|
29
|
+
readonly operation: KeyringOperation;
|
|
30
|
+
constructor(options: KeyringErrorOptions);
|
|
31
|
+
}
|
|
32
|
+
export declare function isKeyringError(value: unknown, code?: KeyringErrorCode): value is KeyringError;
|
|
33
|
+
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/keyring/error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,eAAO,MAAM,kBAAkB;IAC7B,4EAA4E;;IAE5E,yEAAyE;;CAEjE,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAElE,eAAO,MAAM,iBAAiB;;;;CAIpB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEjE,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAQF;;;;;GAKG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC;gBAEzB,OAAO,EAAE,mBAAmB;CAazC;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,OAAO,EACd,IAAI,CAAC,EAAE,gBAAgB,GACtB,KAAK,IAAI,YAAY,CAUvB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const KEYRING_ERROR_CODE = {
|
|
2
|
+
/** The optional `@napi-rs/keyring` peer is absent or exports no `Entry`. */
|
|
3
|
+
peerMissing: "peer_missing",
|
|
4
|
+
/** The OS keychain rejected or failed the operation — never "absent". */
|
|
5
|
+
operationFailed: "operation_failed",
|
|
6
|
+
};
|
|
7
|
+
export const KEYRING_OPERATION = {
|
|
8
|
+
read: "read",
|
|
9
|
+
write: "write",
|
|
10
|
+
delete: "delete",
|
|
11
|
+
};
|
|
12
|
+
const KEYRING_ERROR_NAME = "KeyringError";
|
|
13
|
+
const KEYRING_ERROR_BRAND = Symbol.for("@alpacakit/core/keyring.KeyringError");
|
|
14
|
+
const KEYRING_ERROR_FIELD = {
|
|
15
|
+
code: "code",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The single error type of the keychain boundary. `operation` records what
|
|
19
|
+
* was attempted (a missing peer is only ever discovered while attempting
|
|
20
|
+
* one), so a caller can report "could not write the secret" without
|
|
21
|
+
* re-deriving it from the call site.
|
|
22
|
+
*/
|
|
23
|
+
export class KeyringError extends Error {
|
|
24
|
+
code;
|
|
25
|
+
operation;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
super(options.message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
28
|
+
this.name = KEYRING_ERROR_NAME;
|
|
29
|
+
this.code = options.code;
|
|
30
|
+
this.operation = options.operation;
|
|
31
|
+
Object.defineProperty(this, KEYRING_ERROR_BRAND, {
|
|
32
|
+
value: true,
|
|
33
|
+
enumerable: false,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function isKeyringError(value, code) {
|
|
38
|
+
if (typeof value !== "object" || value === null) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (Reflect.get(value, KEYRING_ERROR_BRAND) !== true) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return (code === undefined || Reflect.get(value, KEYRING_ERROR_FIELD.code) === code);
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE place in the workspace that talks to an OS keychain. Every other
|
|
3
|
+
* package (agents, integrations, and the family conventions above them)
|
|
4
|
+
* reaches the keychain through this subpath, so the native peer, the
|
|
5
|
+
* absence-vs-failure rule, and the error type have a single owner.
|
|
6
|
+
*/
|
|
7
|
+
export type { KeyringErrorCode, KeyringErrorOptions, KeyringOperation, } from "./error.js";
|
|
8
|
+
export { KEYRING_ERROR_CODE, KEYRING_OPERATION, KeyringError, isKeyringError, } from "./error.js";
|
|
9
|
+
export type { KeyringModuleLoader, KeyringOptions, KeyringSecretStore, } from "./resolver.js";
|
|
10
|
+
export { keyringResolver, keyringSecretStore } from "./resolver.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/keyring/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EACV,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE place in the workspace that talks to an OS keychain. Every other
|
|
3
|
+
* package (agents, integrations, and the family conventions above them)
|
|
4
|
+
* reaches the keychain through this subpath, so the native peer, the
|
|
5
|
+
* absence-vs-failure rule, and the error type have a single owner.
|
|
6
|
+
*/
|
|
7
|
+
export { KEYRING_ERROR_CODE, KEYRING_OPERATION, KeyringError, isKeyringError, } from "./error.js";
|
|
8
|
+
export { keyringResolver, keyringSecretStore } from "./resolver.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SecretResolver } from "../ports.js";
|
|
2
|
+
/** Injection seam: tests supply a fake module instead of touching a keychain. */
|
|
3
|
+
export type KeyringModuleLoader = () => Promise<unknown>;
|
|
4
|
+
export type KeyringOptions = {
|
|
5
|
+
readonly service: string;
|
|
6
|
+
readonly module?: KeyringModuleLoader;
|
|
7
|
+
};
|
|
8
|
+
/** Read/write/delete of one OS keychain service. Absence is `null`, not a throw. */
|
|
9
|
+
export interface KeyringSecretStore {
|
|
10
|
+
get(id: string): Promise<string | null>;
|
|
11
|
+
set(id: string, value: string): Promise<void>;
|
|
12
|
+
delete(id: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function keyringResolver(options: KeyringOptions): SecretResolver;
|
|
15
|
+
export declare function keyringSecretStore(options: KeyringOptions): KeyringSecretStore;
|
|
16
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/keyring/resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAgClD,iFAAiF;AACjF,MAAM,MAAM,mBAAmB,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;AAEzD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,mBAAmB,CAAC;CACvC,CAAC;AAEF,oFAAoF;AACpF,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAKD,wBAAgB,eAAe,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,CAGvE;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,cAAc,GACtB,kBAAkB,CAEpB"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { KEYRING_ERROR_CODE, KEYRING_OPERATION, KeyringError, } from "./error.js";
|
|
2
|
+
const KEYRING_PEER_PACKAGE = "@napi-rs/keyring";
|
|
3
|
+
const KEYRING_ENTRY_EXPORT = "Entry";
|
|
4
|
+
const EMPTY_SECRET_VALUE = "";
|
|
5
|
+
const KEYRING_OPERATION_VERB = {
|
|
6
|
+
[KEYRING_OPERATION.read]: "read",
|
|
7
|
+
[KEYRING_OPERATION.write]: "written",
|
|
8
|
+
[KEYRING_OPERATION.delete]: "deleted",
|
|
9
|
+
};
|
|
10
|
+
const KEYRING_ERROR_MESSAGE = {
|
|
11
|
+
peerMissing: `Keyring peer "${KEYRING_PEER_PACKAGE}" is not installed`,
|
|
12
|
+
operationFailed: (id, operation) => `Keyring secret "${id}" could not be ${KEYRING_OPERATION_VERB[operation]}`,
|
|
13
|
+
};
|
|
14
|
+
const defaultKeyringModuleLoader = () => import(KEYRING_PEER_PACKAGE);
|
|
15
|
+
export function keyringResolver(options) {
|
|
16
|
+
const open = openKeyring(options);
|
|
17
|
+
return { resolve: (id) => open.get(id) };
|
|
18
|
+
}
|
|
19
|
+
export function keyringSecretStore(options) {
|
|
20
|
+
return openKeyring(options);
|
|
21
|
+
}
|
|
22
|
+
function openKeyring(options) {
|
|
23
|
+
const loader = options.module ?? defaultKeyringModuleLoader;
|
|
24
|
+
const service = options.service;
|
|
25
|
+
// Only a SUCCESSFUL load is cached: caching the rejection would replay one
|
|
26
|
+
// operation's `peer_missing` (with its stale `operation`) forever, even
|
|
27
|
+
// after the peer is installed.
|
|
28
|
+
let loaded = null;
|
|
29
|
+
const entry = async (id, operation) => {
|
|
30
|
+
loaded ??= await loadKeyringModule(loader, operation);
|
|
31
|
+
return new loaded.Entry(service, id);
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
get: async (id) => {
|
|
35
|
+
const target = await entry(id, KEYRING_OPERATION.read);
|
|
36
|
+
// Absent entries are the `null` return (`Entry.getPassword():
|
|
37
|
+
// string | null` in @napi-rs/keyring); anything thrown is a real
|
|
38
|
+
// keychain failure, never "missing secret".
|
|
39
|
+
return await run(id, KEYRING_OPERATION.read, async () => {
|
|
40
|
+
const value = await target.getPassword();
|
|
41
|
+
return value === EMPTY_SECRET_VALUE ? null : value;
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
set: async (id, value) => {
|
|
45
|
+
const target = await entry(id, KEYRING_OPERATION.write);
|
|
46
|
+
await run(id, KEYRING_OPERATION.write, async () => await target.setPassword(value));
|
|
47
|
+
},
|
|
48
|
+
delete: async (id) => {
|
|
49
|
+
const target = await entry(id, KEYRING_OPERATION.delete);
|
|
50
|
+
await run(id, KEYRING_OPERATION.delete, async () => {
|
|
51
|
+
await target.deletePassword();
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function run(id, operation, operate) {
|
|
57
|
+
try {
|
|
58
|
+
return await operate();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw new KeyringError({
|
|
62
|
+
code: KEYRING_ERROR_CODE.operationFailed,
|
|
63
|
+
operation,
|
|
64
|
+
message: KEYRING_ERROR_MESSAGE.operationFailed(id, operation),
|
|
65
|
+
cause: error,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function loadKeyringModule(loader, operation) {
|
|
70
|
+
let loaded;
|
|
71
|
+
try {
|
|
72
|
+
loaded = await loader();
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw peerMissing(operation, error);
|
|
76
|
+
}
|
|
77
|
+
if (typeof loaded !== "object" || loaded === null) {
|
|
78
|
+
throw peerMissing(operation, null);
|
|
79
|
+
}
|
|
80
|
+
const entry = Reflect.get(loaded, KEYRING_ENTRY_EXPORT);
|
|
81
|
+
if (typeof entry !== "function") {
|
|
82
|
+
throw peerMissing(operation, null);
|
|
83
|
+
}
|
|
84
|
+
return { Entry: entry };
|
|
85
|
+
}
|
|
86
|
+
function peerMissing(operation, cause) {
|
|
87
|
+
return new KeyringError({
|
|
88
|
+
code: KEYRING_ERROR_CODE.peerMissing,
|
|
89
|
+
operation,
|
|
90
|
+
message: KEYRING_ERROR_MESSAGE.peerMissing,
|
|
91
|
+
cause,
|
|
92
|
+
});
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpacakit/core",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.37",
|
|
4
4
|
"description": "Architecture-neutral kernel primitives: branded ids, ports, registries, pipelines, payload codecs, and immutable mutation helpers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./keyring": {
|
|
13
|
+
"types": "./dist/keyring/index.d.ts",
|
|
14
|
+
"import": "./dist/keyring/index.js"
|
|
11
15
|
}
|
|
12
16
|
},
|
|
13
17
|
"types": "./dist/index.d.ts",
|
|
@@ -29,6 +33,14 @@
|
|
|
29
33
|
"typescript": "^5.9.3",
|
|
30
34
|
"vitest": "^4.0.18"
|
|
31
35
|
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@napi-rs/keyring": "^1.3.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"@napi-rs/keyring": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
32
44
|
"scripts": {
|
|
33
45
|
"build": "tsc -p tsconfig.build.json",
|
|
34
46
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|