@elizaos/vault 2.0.0-alpha.537
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 +159 -0
- package/dist/audit.d.ts +14 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +27 -0
- package/dist/audit.js.map +1 -0
- package/dist/credentials.d.ts +58 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +157 -0
- package/dist/credentials.js.map +1 -0
- package/dist/crypto.d.ts +18 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +67 -0
- package/dist/crypto.js.map +1 -0
- package/dist/external-credentials.d.ts +62 -0
- package/dist/external-credentials.d.ts.map +1 -0
- package/dist/external-credentials.js +335 -0
- package/dist/external-credentials.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +70 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +163 -0
- package/dist/install.js.map +1 -0
- package/dist/inventory.d.ts +140 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +319 -0
- package/dist/inventory.js.map +1 -0
- package/dist/manager.d.ts +161 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +466 -0
- package/dist/manager.js.map +1 -0
- package/dist/master-key.d.ts +86 -0
- package/dist/master-key.d.ts.map +1 -0
- package/dist/master-key.js +247 -0
- package/dist/master-key.js.map +1 -0
- package/dist/password-managers.d.ts +17 -0
- package/dist/password-managers.d.ts.map +1 -0
- package/dist/password-managers.js +59 -0
- package/dist/password-managers.js.map +1 -0
- package/dist/profiles.d.ts +68 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +189 -0
- package/dist/profiles.js.map +1 -0
- package/dist/store.d.ts +22 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +137 -0
- package/dist/store.js.map +1 -0
- package/dist/testing.d.ts +32 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +70 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/vault.d.ts +77 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +269 -0
- package/dist/vault.js.map +1 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# @elizaos/vault
|
|
2
|
+
|
|
3
|
+
Simple secrets/config vault for Eliza. **One** API for sensitive
|
|
4
|
+
credentials and non-sensitive configuration.
|
|
5
|
+
|
|
6
|
+
## Why this exists
|
|
7
|
+
|
|
8
|
+
Eliza's Settings flow had four real bugs that all came from the same
|
|
9
|
+
root cause — credentials and config were scattered across multiple
|
|
10
|
+
writers, multiple file layouts, and a guess-which-field-is-the-key
|
|
11
|
+
heuristic. The vault is the single seam those bugs disappear behind:
|
|
12
|
+
|
|
13
|
+
1. **Model slug overwrote API key** — the save path used
|
|
14
|
+
`Object.values(config).find(non-empty)` to identify the credential,
|
|
15
|
+
so typing the model field before the API-key field corrupted the
|
|
16
|
+
key. The vault's typed API makes this structurally impossible.
|
|
17
|
+
2. **Dual writer** — values landed in `env.X` AND `env.vars.X`. One
|
|
18
|
+
writer, one storage location.
|
|
19
|
+
3. **Orphan `tts/media/embeddings/rpc` routes after Eliza Cloud
|
|
20
|
+
disconnect** — fixed at the disconnect-handler level by clearing
|
|
21
|
+
the routes when the account unlinks.
|
|
22
|
+
4. **No reveal** — saved values were write-only. `vault.reveal(key)`
|
|
23
|
+
round-trips through the audit log.
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createVault } from "@elizaos/vault";
|
|
29
|
+
|
|
30
|
+
const vault = createVault();
|
|
31
|
+
|
|
32
|
+
// Same call signature for sensitive and non-sensitive:
|
|
33
|
+
await vault.set("openrouter.apiKey", "sk-or-v1-...", { sensitive: true });
|
|
34
|
+
await vault.set("ui.theme", "dark");
|
|
35
|
+
|
|
36
|
+
// Reads:
|
|
37
|
+
await vault.get("openrouter.apiKey"); // → "sk-or-v1-..."
|
|
38
|
+
await vault.has("openrouter.apiKey"); // → true
|
|
39
|
+
await vault.describe("openrouter.apiKey"); // → { source, sensitive, lastModified }
|
|
40
|
+
await vault.reveal("openrouter.apiKey", "settings-ui"); // logged in audit
|
|
41
|
+
await vault.list(); // → all keys, no values
|
|
42
|
+
await vault.list("openrouter"); // → prefix-filtered
|
|
43
|
+
await vault.remove("openrouter.apiKey");
|
|
44
|
+
await vault.stats(); // → { total, sensitive, nonSensitive, references }
|
|
45
|
+
|
|
46
|
+
// Password-manager references — value lives there, vault stores reference:
|
|
47
|
+
await vault.setReference("openrouter.apiKey", {
|
|
48
|
+
source: "1password",
|
|
49
|
+
path: "Personal/OpenRouter/api-key",
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## SecretsManager — pick which password managers to use
|
|
54
|
+
|
|
55
|
+
The `Vault` is the storage primitive. The `SecretsManager` sits on top
|
|
56
|
+
and routes direct writes based on user preferences. External password
|
|
57
|
+
managers are not written through this API yet; callers store references
|
|
58
|
+
with `vault.setReference()` after the value already exists in the vendor
|
|
59
|
+
tool.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { createManager } from "@elizaos/vault";
|
|
63
|
+
|
|
64
|
+
const manager = createManager();
|
|
65
|
+
|
|
66
|
+
// Probe what's available on this machine:
|
|
67
|
+
const statuses = await manager.detectBackends();
|
|
68
|
+
// [
|
|
69
|
+
// { id: "in-house", available: true, signedIn: true, label: "Eliza (local, encrypted)" },
|
|
70
|
+
// { id: "1password", available: true, signedIn: true, label: "1Password" },
|
|
71
|
+
// { id: "bitwarden", available: true, signedIn: false, label: "Bitwarden", detail: "...not signed in. Run `bw login`." },
|
|
72
|
+
// { id: "protonpass", available: false, label: "Proton Pass", detail: "...not installed (CLI in beta)." },
|
|
73
|
+
// ]
|
|
74
|
+
|
|
75
|
+
// User picks their backends in Settings:
|
|
76
|
+
await manager.setPreferences({
|
|
77
|
+
enabled: ["1password", "in-house"],
|
|
78
|
+
routing: { "anthropic.apiKey": "in-house" }, // optional per-key override
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// External direct writes fail loudly until vendor write semantics exist:
|
|
82
|
+
await manager.set("openrouter.apiKey", "sk-or-...", { sensitive: true });
|
|
83
|
+
// → throws: backend "1password" cannot accept direct writes yet
|
|
84
|
+
|
|
85
|
+
// Store explicit references through the vault primitive:
|
|
86
|
+
await manager.vault.setReference("openrouter.apiKey", {
|
|
87
|
+
source: "1password",
|
|
88
|
+
path: "Personal/OpenRouter/api-key",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await manager.set("anthropic.apiKey", "sk-ant-...", { sensitive: true });
|
|
92
|
+
// → in-house (per-key override above)
|
|
93
|
+
|
|
94
|
+
await manager.set("ui.theme", "dark");
|
|
95
|
+
// → always in-house (non-sensitive values don't go to password managers)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Three modes the user can run in:**
|
|
99
|
+
|
|
100
|
+
- **None** — nothing enabled but `in-house`. Default. Local-only.
|
|
101
|
+
- **One** — pick 1Password OR Proton Pass OR Bitwarden. Direct sensitive
|
|
102
|
+
writes fail until vendor write support exists; explicit references can
|
|
103
|
+
still be stored with `vault.setReference()`.
|
|
104
|
+
- **All** — all backends enabled. Per-key routing in Settings, or just
|
|
105
|
+
use the priority order.
|
|
106
|
+
|
|
107
|
+
`in-house` is always available. External backend failures are surfaced
|
|
108
|
+
instead of silently falling back to local storage.
|
|
109
|
+
|
|
110
|
+
## Storage
|
|
111
|
+
|
|
112
|
+
- **Sensitive values** — AES-256-GCM encrypted at rest with the vault
|
|
113
|
+
key as additional authenticated data. Master key in OS keychain
|
|
114
|
+
(cross-platform via `@napi-rs/keyring`: macOS Keychain, Windows
|
|
115
|
+
Credential Manager, Linux libsecret).
|
|
116
|
+
- **Non-sensitive values** — plaintext in `~/.eliza/vault.json`
|
|
117
|
+
(mode 0600). Atomic-rename writes.
|
|
118
|
+
- **References** — stored as `{ source, path }`. The actual value lives
|
|
119
|
+
in 1Password / Proton Pass; resolved at use time via the vendor's
|
|
120
|
+
CLI.
|
|
121
|
+
|
|
122
|
+
## Sync
|
|
123
|
+
|
|
124
|
+
Sync = your existing tools. If you want secrets across devices, store
|
|
125
|
+
them as 1Password references — 1Password syncs your vault, the
|
|
126
|
+
references stay portable, your secrets follow. We don't build a
|
|
127
|
+
separate cloud sync.
|
|
128
|
+
|
|
129
|
+
## Audit log
|
|
130
|
+
|
|
131
|
+
Every operation appends one JSONL line to
|
|
132
|
+
`~/.eliza/audit/vault.jsonl`:
|
|
133
|
+
|
|
134
|
+
```jsonl
|
|
135
|
+
{"ts":1714330000000,"action":"set","key":"openrouter.apiKey"}
|
|
136
|
+
{"ts":1714330000010,"action":"get","key":"openrouter.apiKey"}
|
|
137
|
+
{"ts":1714330000020,"action":"reveal","key":"openrouter.apiKey","caller":"settings-ui"}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Records keys, never values. Pass an optional `caller` to `reveal()` so
|
|
141
|
+
the log shows who asked.
|
|
142
|
+
|
|
143
|
+
## Testing
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { createTestVault } from "@elizaos/vault/testing";
|
|
147
|
+
|
|
148
|
+
const test = await createTestVault({
|
|
149
|
+
values: { "ui.theme": "dark" },
|
|
150
|
+
secrets: { "openrouter.apiKey": "test-key" },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await test.vault.set("openai.apiKey", "test-2", { sensitive: true });
|
|
154
|
+
const records = await test.getAuditRecords();
|
|
155
|
+
await test.dispose();
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Real vault, real encryption, real audit log — temp dir cleaned up on
|
|
159
|
+
`dispose()`. No OS keychain access (uses an in-memory master key).
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AuditRecord, VaultLogger } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Append-only JSONL audit log. One line per vault operation. Records
|
|
4
|
+
* keys, never values.
|
|
5
|
+
*/
|
|
6
|
+
export declare class AuditLog {
|
|
7
|
+
private readonly path;
|
|
8
|
+
private readonly logger?;
|
|
9
|
+
constructor(path: string, logger?: VaultLogger | undefined);
|
|
10
|
+
record(entry: Omit<AuditRecord, "ts"> & {
|
|
11
|
+
ts?: number;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE3D;;;GAGG;AACH,qBAAa,QAAQ;IAEjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;gBADP,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,YAAA;IAGjC,MAAM,CACV,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAC/C,OAAO,CAAC,IAAI,CAAC;CAcjB"}
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Append-only JSONL audit log. One line per vault operation. Records
|
|
5
|
+
* keys, never values.
|
|
6
|
+
*/
|
|
7
|
+
export class AuditLog {
|
|
8
|
+
path;
|
|
9
|
+
logger;
|
|
10
|
+
constructor(path, logger) {
|
|
11
|
+
this.path = path;
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
}
|
|
14
|
+
async record(entry) {
|
|
15
|
+
const record = { ts: entry.ts ?? Date.now(), ...entry };
|
|
16
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
17
|
+
try {
|
|
18
|
+
await fs.mkdir(dirname(this.path), { recursive: true });
|
|
19
|
+
await fs.appendFile(this.path, line, { mode: 0o600 });
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
// Audit failure must not block the caller, but it must be visible.
|
|
23
|
+
this.logger?.warn(`[vault] failed to append audit record to ${this.path}`, err);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC;;;GAGG;AACH,MAAM,OAAO,QAAQ;IAEA;IACA;IAFnB,YACmB,IAAY,EACZ,MAAoB;QADpB,SAAI,GAAJ,IAAI,CAAQ;QACZ,WAAM,GAAN,MAAM,CAAc;IACpC,CAAC;IAEJ,KAAK,CAAC,MAAM,CACV,KAAgD;QAEhD,MAAM,MAAM,GAAgB,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,GAAG,KAAK,EAAE,CAAC;QACrE,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC;QAC3C,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACxD,MAAM,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACxD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,mEAAmE;YACnE,IAAI,CAAC,MAAM,EAAE,IAAI,CACf,4CAA4C,IAAI,CAAC,IAAI,EAAE,EACvD,GAAG,CACJ,CAAC;QACJ,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saved-login helpers for in-app browser autofill.
|
|
3
|
+
*
|
|
4
|
+
* The browser tab preload detects login forms and asks the host for
|
|
5
|
+
* matching credentials. The host reads them from the vault using the
|
|
6
|
+
* helpers here. Storage layout:
|
|
7
|
+
*
|
|
8
|
+
* creds.<domain>.<account> → JSON-encoded SavedLoginRecord, sensitive
|
|
9
|
+
* creds.<domain>.__autoallow → "1" / "0", non-sensitive (whitelist toggle)
|
|
10
|
+
*
|
|
11
|
+
* `<domain>` is the registrable hostname (e.g. `github.com`, no port).
|
|
12
|
+
* `<account>` is the URL-encoded username (so `@`, `/`, `.` are safe in
|
|
13
|
+
* the segment). Vault prefix matching uses dot segments, so listing
|
|
14
|
+
* `creds.github.com` returns every account under that domain plus the
|
|
15
|
+
* autoallow flag.
|
|
16
|
+
*
|
|
17
|
+
* Sensitive values are AES-GCM encrypted by the vault. Listing returns
|
|
18
|
+
* metadata only — passwords are never copied into the listing payload.
|
|
19
|
+
*/
|
|
20
|
+
import type { Vault } from "./vault.js";
|
|
21
|
+
export interface SavedLogin {
|
|
22
|
+
/** Registrable hostname, e.g. `github.com`. Lower-cased on write. */
|
|
23
|
+
readonly domain: string;
|
|
24
|
+
/** User identifier as typed: email, handle, etc. */
|
|
25
|
+
readonly username: string;
|
|
26
|
+
/** Plaintext password. Encrypted at rest by the vault. */
|
|
27
|
+
readonly password: string;
|
|
28
|
+
/** TOTP seed for sites with 2FA. */
|
|
29
|
+
readonly otpSeed?: string;
|
|
30
|
+
/** Free-form note. */
|
|
31
|
+
readonly notes?: string;
|
|
32
|
+
/** Unix ms of last write. Set by `setSavedLogin`. */
|
|
33
|
+
readonly lastModified: number;
|
|
34
|
+
}
|
|
35
|
+
export interface SavedLoginSummary {
|
|
36
|
+
readonly domain: string;
|
|
37
|
+
readonly username: string;
|
|
38
|
+
readonly lastModified: number;
|
|
39
|
+
}
|
|
40
|
+
/** Persist (or replace) a login. Stamps `lastModified` automatically. */
|
|
41
|
+
export declare function setSavedLogin(vault: Vault, login: Omit<SavedLogin, "lastModified">): Promise<void>;
|
|
42
|
+
/** Read a login. Returns null when missing. */
|
|
43
|
+
export declare function getSavedLogin(vault: Vault, domain: string, username: string): Promise<SavedLogin | null>;
|
|
44
|
+
/**
|
|
45
|
+
* List logins. With no `domain`, returns every saved login summary
|
|
46
|
+
* across the vault. With a domain, scopes to that hostname.
|
|
47
|
+
*
|
|
48
|
+
* Returns metadata only. The password values stay encrypted at rest;
|
|
49
|
+
* callers must `getSavedLogin` to decrypt one entry at a time.
|
|
50
|
+
*/
|
|
51
|
+
export declare function listSavedLogins(vault: Vault, domain?: string): Promise<readonly SavedLoginSummary[]>;
|
|
52
|
+
/** Remove a single login. Idempotent. */
|
|
53
|
+
export declare function deleteSavedLogin(vault: Vault, domain: string, username: string): Promise<void>;
|
|
54
|
+
/** Read the autoallow flag for a domain. False when unset. */
|
|
55
|
+
export declare function getAutofillAllowed(vault: Vault, domain: string): Promise<boolean>;
|
|
56
|
+
/** Toggle the autoallow flag. `true` skips consent on next autofill for that domain. */
|
|
57
|
+
export declare function setAutofillAllowed(vault: Vault, domain: string, allowed: boolean): Promise<void>;
|
|
58
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAQxC,MAAM,WAAW,UAAU;IACzB,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oCAAoC;IACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAwBD,yEAAyE;AACzE,wBAAsB,aAAa,CACjC,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED,+CAA+C;AAC/C,wBAAsB,aAAa,CACjC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAM5B;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,KAAK,EACZ,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAyBvC;AAED,yCAAyC;AACzC,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,8DAA8D;AAC9D,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAKlB;AAED,wFAAwF;AACxF,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,IAAI,CAAC,CAEf"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saved-login helpers for in-app browser autofill.
|
|
3
|
+
*
|
|
4
|
+
* The browser tab preload detects login forms and asks the host for
|
|
5
|
+
* matching credentials. The host reads them from the vault using the
|
|
6
|
+
* helpers here. Storage layout:
|
|
7
|
+
*
|
|
8
|
+
* creds.<domain>.<account> → JSON-encoded SavedLoginRecord, sensitive
|
|
9
|
+
* creds.<domain>.__autoallow → "1" / "0", non-sensitive (whitelist toggle)
|
|
10
|
+
*
|
|
11
|
+
* `<domain>` is the registrable hostname (e.g. `github.com`, no port).
|
|
12
|
+
* `<account>` is the URL-encoded username (so `@`, `/`, `.` are safe in
|
|
13
|
+
* the segment). Vault prefix matching uses dot segments, so listing
|
|
14
|
+
* `creds.github.com` returns every account under that domain plus the
|
|
15
|
+
* autoallow flag.
|
|
16
|
+
*
|
|
17
|
+
* Sensitive values are AES-GCM encrypted by the vault. Listing returns
|
|
18
|
+
* metadata only — passwords are never copied into the listing payload.
|
|
19
|
+
*/
|
|
20
|
+
const PREFIX = "creds";
|
|
21
|
+
// Sentinel for the per-domain autoallow flag. The colon prefix is URL-
|
|
22
|
+
// encoded by `encodeAccount`, so a literal username `:autoallow` lives at
|
|
23
|
+
// `%3Aautoallow` and cannot collide with this sentinel.
|
|
24
|
+
const AUTOALLOW_SEGMENT = ":autoallow";
|
|
25
|
+
/** Encode an account segment so vault key parsing stays unambiguous. */
|
|
26
|
+
function encodeAccount(username) {
|
|
27
|
+
return encodeURIComponent(username);
|
|
28
|
+
}
|
|
29
|
+
function decodeAccount(segment) {
|
|
30
|
+
return decodeURIComponent(segment);
|
|
31
|
+
}
|
|
32
|
+
/** Lower-case domains so `Github.com` and `github.com` collide. */
|
|
33
|
+
function normalizeDomain(domain) {
|
|
34
|
+
return domain.trim().toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
function loginKey(domain, username) {
|
|
37
|
+
return `${PREFIX}.${normalizeDomain(domain)}.${encodeAccount(username)}`;
|
|
38
|
+
}
|
|
39
|
+
function autoallowKey(domain) {
|
|
40
|
+
return `${PREFIX}.${normalizeDomain(domain)}.${AUTOALLOW_SEGMENT}`;
|
|
41
|
+
}
|
|
42
|
+
/** Persist (or replace) a login. Stamps `lastModified` automatically. */
|
|
43
|
+
export async function setSavedLogin(vault, login) {
|
|
44
|
+
if (login.domain.trim().length === 0) {
|
|
45
|
+
throw new TypeError("setSavedLogin: domain required");
|
|
46
|
+
}
|
|
47
|
+
if (login.username.length === 0) {
|
|
48
|
+
throw new TypeError("setSavedLogin: username required");
|
|
49
|
+
}
|
|
50
|
+
if (typeof login.password !== "string" || login.password.length === 0) {
|
|
51
|
+
throw new TypeError("setSavedLogin: password required");
|
|
52
|
+
}
|
|
53
|
+
const record = {
|
|
54
|
+
domain: normalizeDomain(login.domain),
|
|
55
|
+
username: login.username,
|
|
56
|
+
password: login.password,
|
|
57
|
+
...(login.otpSeed ? { otpSeed: login.otpSeed } : {}),
|
|
58
|
+
...(login.notes ? { notes: login.notes } : {}),
|
|
59
|
+
lastModified: Date.now(),
|
|
60
|
+
};
|
|
61
|
+
await vault.set(loginKey(login.domain, login.username), JSON.stringify(record), { sensitive: true });
|
|
62
|
+
}
|
|
63
|
+
/** Read a login. Returns null when missing. */
|
|
64
|
+
export async function getSavedLogin(vault, domain, username) {
|
|
65
|
+
const key = loginKey(domain, username);
|
|
66
|
+
const has = await vault.has(key);
|
|
67
|
+
if (!has)
|
|
68
|
+
return null;
|
|
69
|
+
const raw = await vault.get(key);
|
|
70
|
+
return parseLogin(raw);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* List logins. With no `domain`, returns every saved login summary
|
|
74
|
+
* across the vault. With a domain, scopes to that hostname.
|
|
75
|
+
*
|
|
76
|
+
* Returns metadata only. The password values stay encrypted at rest;
|
|
77
|
+
* callers must `getSavedLogin` to decrypt one entry at a time.
|
|
78
|
+
*/
|
|
79
|
+
export async function listSavedLogins(vault, domain) {
|
|
80
|
+
const prefix = domain ? `${PREFIX}.${normalizeDomain(domain)}` : PREFIX;
|
|
81
|
+
const keys = await vault.list(prefix);
|
|
82
|
+
const summaries = [];
|
|
83
|
+
const failures = [];
|
|
84
|
+
for (const key of keys) {
|
|
85
|
+
const parsed = parseLoginKey(key);
|
|
86
|
+
if (!parsed)
|
|
87
|
+
continue;
|
|
88
|
+
if (parsed.account === AUTOALLOW_SEGMENT)
|
|
89
|
+
continue;
|
|
90
|
+
const descriptor = await vault.describe(key);
|
|
91
|
+
if (!descriptor)
|
|
92
|
+
continue;
|
|
93
|
+
// describe() returns lastModified directly; we don't need to
|
|
94
|
+
// decrypt the value to render the listing UI.
|
|
95
|
+
summaries.push({
|
|
96
|
+
domain: parsed.domain,
|
|
97
|
+
username: decodeAccount(parsed.account),
|
|
98
|
+
lastModified: descriptor.lastModified,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (failures.length > 0) {
|
|
102
|
+
throw new Error(`listSavedLogins: failed to describe ${failures.length} key(s): ${failures.join(", ")}`);
|
|
103
|
+
}
|
|
104
|
+
return summaries;
|
|
105
|
+
}
|
|
106
|
+
/** Remove a single login. Idempotent. */
|
|
107
|
+
export async function deleteSavedLogin(vault, domain, username) {
|
|
108
|
+
await vault.remove(loginKey(domain, username));
|
|
109
|
+
}
|
|
110
|
+
/** Read the autoallow flag for a domain. False when unset. */
|
|
111
|
+
export async function getAutofillAllowed(vault, domain) {
|
|
112
|
+
const key = autoallowKey(domain);
|
|
113
|
+
if (!(await vault.has(key)))
|
|
114
|
+
return false;
|
|
115
|
+
const raw = await vault.get(key);
|
|
116
|
+
return raw === "1";
|
|
117
|
+
}
|
|
118
|
+
/** Toggle the autoallow flag. `true` skips consent on next autofill for that domain. */
|
|
119
|
+
export async function setAutofillAllowed(vault, domain, allowed) {
|
|
120
|
+
await vault.set(autoallowKey(domain), allowed ? "1" : "0");
|
|
121
|
+
}
|
|
122
|
+
function parseLoginKey(key) {
|
|
123
|
+
// creds.<domain>.<account-or-flag>
|
|
124
|
+
// We split on the first two dots: the first separates the prefix,
|
|
125
|
+
// the second separates the domain from the account segment.
|
|
126
|
+
if (!key.startsWith(`${PREFIX}.`))
|
|
127
|
+
return null;
|
|
128
|
+
const rest = key.slice(PREFIX.length + 1);
|
|
129
|
+
// The domain part itself contains dots (e.g. github.com), so the
|
|
130
|
+
// account segment is everything after the LAST dot.
|
|
131
|
+
const lastDot = rest.lastIndexOf(".");
|
|
132
|
+
if (lastDot <= 0)
|
|
133
|
+
return null;
|
|
134
|
+
const domain = rest.slice(0, lastDot);
|
|
135
|
+
const account = rest.slice(lastDot + 1);
|
|
136
|
+
if (!domain || !account)
|
|
137
|
+
return null;
|
|
138
|
+
return { domain, account };
|
|
139
|
+
}
|
|
140
|
+
function parseLogin(raw) {
|
|
141
|
+
const parsed = JSON.parse(raw);
|
|
142
|
+
if (typeof parsed.domain !== "string" ||
|
|
143
|
+
typeof parsed.username !== "string" ||
|
|
144
|
+
typeof parsed.password !== "string" ||
|
|
145
|
+
typeof parsed.lastModified !== "number") {
|
|
146
|
+
throw new Error(`vault credentials: stored entry is malformed (got keys: ${Object.keys(parsed).join(", ")})`);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
domain: parsed.domain,
|
|
150
|
+
username: parsed.username,
|
|
151
|
+
password: parsed.password,
|
|
152
|
+
...(parsed.otpSeed ? { otpSeed: parsed.otpSeed } : {}),
|
|
153
|
+
...(parsed.notes ? { notes: parsed.notes } : {}),
|
|
154
|
+
lastModified: parsed.lastModified,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"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"}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM envelope for a single value.
|
|
3
|
+
*
|
|
4
|
+
* Wire format: `v1:<nonce_b64>:<tag_b64>:<ct_b64>` (all base64).
|
|
5
|
+
* - nonce: 12 bytes (96-bit GCM standard)
|
|
6
|
+
* - tag: 16 bytes (128-bit auth tag)
|
|
7
|
+
*
|
|
8
|
+
* The vault key is bound as additional authenticated data (AAD) so a
|
|
9
|
+
* swapped ciphertext between slots fails decryption.
|
|
10
|
+
*/
|
|
11
|
+
export declare const KEY_BYTES = 32;
|
|
12
|
+
export declare class CryptoError extends Error {
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
export declare function generateMasterKey(): Buffer;
|
|
16
|
+
export declare function encrypt(masterKey: Buffer, plaintext: string, aad: string): string;
|
|
17
|
+
export declare function decrypt(masterKey: Buffer, ciphertext: string, aad: string): string;
|
|
18
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,eAAO,MAAM,SAAS,KAAK,CAAC;AAI5B,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,OAAO,CACrB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,MAAM,CAUR;AAED,wBAAgB,OAAO,CACrB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,GACV,MAAM,CAkCR"}
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* AES-256-GCM envelope for a single value.
|
|
4
|
+
*
|
|
5
|
+
* Wire format: `v1:<nonce_b64>:<tag_b64>:<ct_b64>` (all base64).
|
|
6
|
+
* - nonce: 12 bytes (96-bit GCM standard)
|
|
7
|
+
* - tag: 16 bytes (128-bit auth tag)
|
|
8
|
+
*
|
|
9
|
+
* The vault key is bound as additional authenticated data (AAD) so a
|
|
10
|
+
* swapped ciphertext between slots fails decryption.
|
|
11
|
+
*/
|
|
12
|
+
export const KEY_BYTES = 32;
|
|
13
|
+
const NONCE_BYTES = 12;
|
|
14
|
+
const TAG_BYTES = 16;
|
|
15
|
+
export class CryptoError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "CryptoError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function generateMasterKey() {
|
|
22
|
+
return randomBytes(KEY_BYTES);
|
|
23
|
+
}
|
|
24
|
+
export function encrypt(masterKey, plaintext, aad) {
|
|
25
|
+
if (masterKey.length !== KEY_BYTES) {
|
|
26
|
+
throw new CryptoError(`master key must be ${KEY_BYTES} bytes`);
|
|
27
|
+
}
|
|
28
|
+
const nonce = randomBytes(NONCE_BYTES);
|
|
29
|
+
const cipher = createCipheriv("aes-256-gcm", masterKey, nonce);
|
|
30
|
+
cipher.setAAD(Buffer.from(aad, "utf8"));
|
|
31
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
32
|
+
const tag = cipher.getAuthTag();
|
|
33
|
+
return `v1:${nonce.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
|
|
34
|
+
}
|
|
35
|
+
export function decrypt(masterKey, ciphertext, aad) {
|
|
36
|
+
if (masterKey.length !== KEY_BYTES) {
|
|
37
|
+
throw new CryptoError(`master key must be ${KEY_BYTES} bytes`);
|
|
38
|
+
}
|
|
39
|
+
const parts = ciphertext.split(":");
|
|
40
|
+
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
41
|
+
throw new CryptoError("malformed ciphertext or unsupported version");
|
|
42
|
+
}
|
|
43
|
+
const nonceB64 = parts[1];
|
|
44
|
+
const tagB64 = parts[2];
|
|
45
|
+
const ctB64 = parts[3];
|
|
46
|
+
if (nonceB64 === undefined || tagB64 === undefined || ctB64 === undefined) {
|
|
47
|
+
throw new CryptoError("malformed ciphertext");
|
|
48
|
+
}
|
|
49
|
+
const nonce = Buffer.from(nonceB64, "base64");
|
|
50
|
+
const tag = Buffer.from(tagB64, "base64");
|
|
51
|
+
const ct = Buffer.from(ctB64, "base64");
|
|
52
|
+
if (nonce.length !== NONCE_BYTES || tag.length !== TAG_BYTES) {
|
|
53
|
+
throw new CryptoError("malformed ciphertext");
|
|
54
|
+
}
|
|
55
|
+
const decipher = createDecipheriv("aes-256-gcm", masterKey, nonce);
|
|
56
|
+
decipher.setAAD(Buffer.from(aad, "utf8"));
|
|
57
|
+
decipher.setAuthTag(tag);
|
|
58
|
+
try {
|
|
59
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
throw new CryptoError(err instanceof Error
|
|
63
|
+
? `decryption failed: ${err.message}`
|
|
64
|
+
: "decryption failed");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External credential adapters for password-manager backends.
|
|
3
|
+
*
|
|
4
|
+
* Reads Login items out of `op` (1Password) and `bw` (Bitwarden) using the
|
|
5
|
+
* session token persisted by the secrets-manager installer at
|
|
6
|
+
* `pm.<backend>.session`. Returns a uniform shape so the manager layer can
|
|
7
|
+
* merge them with the in-house saved-logins list.
|
|
8
|
+
*
|
|
9
|
+
* list* → metadata only, never returns passwords
|
|
10
|
+
* reveal* → explicit second step, returns username + password (+ totp)
|
|
11
|
+
*
|
|
12
|
+
* The CLI is shelled out via an injected `ExecFn` so tests can stub the
|
|
13
|
+
* subprocess without spawning real processes.
|
|
14
|
+
*/
|
|
15
|
+
import type { Vault } from "./vault.js";
|
|
16
|
+
export type ExternalLoginSource = "1password" | "bitwarden";
|
|
17
|
+
export interface ExternalLoginListEntry {
|
|
18
|
+
readonly source: ExternalLoginSource;
|
|
19
|
+
/** op item id / bw item id — opaque to callers. */
|
|
20
|
+
readonly externalId: string;
|
|
21
|
+
readonly title: string;
|
|
22
|
+
readonly username: string;
|
|
23
|
+
/** Best-effort registrable hostname extracted from urls[0]; null when none. */
|
|
24
|
+
readonly domain: string | null;
|
|
25
|
+
readonly url: string | null;
|
|
26
|
+
/** Epoch ms; 0 when the backend didn't supply a timestamp. */
|
|
27
|
+
readonly updatedAt: number;
|
|
28
|
+
}
|
|
29
|
+
export interface ExternalLoginReveal extends ExternalLoginListEntry {
|
|
30
|
+
readonly password: string;
|
|
31
|
+
readonly totp?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare class BackendNotSignedInError extends Error {
|
|
34
|
+
readonly source: ExternalLoginSource;
|
|
35
|
+
constructor(source: ExternalLoginSource);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Subprocess executor injected by the manager (tests pass a stub).
|
|
39
|
+
*
|
|
40
|
+
* Mirrors `node:child_process.execFile` with promises: returns combined
|
|
41
|
+
* stdout/stderr, throws on non-zero exit. The `env` option matters for
|
|
42
|
+
* Bitwarden (BW_SESSION) — 1Password uses an explicit `--session` flag.
|
|
43
|
+
*/
|
|
44
|
+
export type ExecFn = (cmd: string, args: readonly string[], opts: {
|
|
45
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
46
|
+
readonly timeoutMs?: number;
|
|
47
|
+
readonly stdin?: string;
|
|
48
|
+
}) => Promise<{
|
|
49
|
+
readonly stdout: string;
|
|
50
|
+
readonly stderr: string;
|
|
51
|
+
}>;
|
|
52
|
+
export declare function listOnePasswordLogins(vault: Vault, exec: ExecFn): Promise<readonly ExternalLoginListEntry[]>;
|
|
53
|
+
export declare function revealOnePasswordLogin(vault: Vault, exec: ExecFn, externalId: string): Promise<ExternalLoginReveal>;
|
|
54
|
+
export declare function listBitwardenLogins(vault: Vault, exec: ExecFn): Promise<readonly ExternalLoginListEntry[]>;
|
|
55
|
+
export declare function revealBitwardenLogin(vault: Vault, exec: ExecFn, externalId: string): Promise<ExternalLoginReveal>;
|
|
56
|
+
/**
|
|
57
|
+
* Production `ExecFn` wrapping `node:child_process.execFile`. Tests inject
|
|
58
|
+
* stubs instead of using this. Lives here so callers can `import` a single
|
|
59
|
+
* default rather than wiring `child_process` themselves.
|
|
60
|
+
*/
|
|
61
|
+
export declare function defaultExecFn(): ExecFn;
|
|
62
|
+
//# sourceMappingURL=external-credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-credentials.d.ts","sourceRoot":"","sources":["../src/external-credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,WAAW,CAAC;AAE5D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,mDAAmD;IACnD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAoB,SAAQ,sBAAsB;IACjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IACpC,QAAQ,CAAC,MAAM,EAAE,mBAAmB;gBAA3B,MAAM,EAAE,mBAAmB;CAIjD;AAED;;;;;;GAMG;AACH,MAAM,MAAM,MAAM,GAAG,CACnB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,IAAI,EAAE;IACJ,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB,KACE,OAAO,CAAC;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAgCnE,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,SAAS,sBAAsB,EAAE,CAAC,CAyC5C;AAED,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,mBAAmB,CAAC,CAwC9B;AAsDD,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,SAAS,sBAAsB,EAAE,CAAC,CA+B5C;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,mBAAmB,CAAC,CAiC9B;AAuJD;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CA8BtC"}
|