@moxxy/plugin-vault 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/crypto.d.ts +30 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +87 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +212 -0
- package/dist/index.js.map +1 -0
- package/dist/keysource.d.ts +50 -0
- package/dist/keysource.d.ts.map +1 -0
- package/dist/keysource.js +145 -0
- package/dist/keysource.js.map +1 -0
- package/dist/placeholder.d.ts +10 -0
- package/dist/placeholder.d.ts.map +1 -0
- package/dist/placeholder.js +92 -0
- package/dist/placeholder.js.map +1 -0
- package/dist/store.d.ts +106 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +415 -0
- package/dist/store.js.map +1 -0
- package/package.json +65 -0
- package/src/command.test.ts +101 -0
- package/src/crypto.test.ts +66 -0
- package/src/crypto.ts +106 -0
- package/src/index.ts +269 -0
- package/src/keysource.test.ts +285 -0
- package/src/keysource.ts +198 -0
- package/src/placeholder.test.ts +142 -0
- package/src/placeholder.ts +104 -0
- package/src/store.canary.test.ts +151 -0
- package/src/store.test.ts +283 -0
- package/src/store.ts +457 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { MoxxyError } from '@moxxy/sdk';
|
|
2
|
+
const PLACEHOLDER_RE = /\$\{vault:([A-Za-z0-9_.-]+)\}/g;
|
|
3
|
+
// Bound recursion over caller-supplied objects: a pathologically deep config
|
|
4
|
+
// (or an in-memory reference cycle, which is legal for JS objects even though
|
|
5
|
+
// JSON can't express one) would otherwise overflow the stack and take down the
|
|
6
|
+
// process. 64 levels is far deeper than any real config tree.
|
|
7
|
+
const MAX_DEPTH = 64;
|
|
8
|
+
function tooDeepError() {
|
|
9
|
+
return new MoxxyError({
|
|
10
|
+
code: 'CONFIG_INVALID',
|
|
11
|
+
message: `vault: value nested too deeply (> ${MAX_DEPTH} levels) — possible reference cycle`,
|
|
12
|
+
hint: 'Flatten the config or remove the cyclic reference before resolving vault placeholders.',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolve every `${vault:NAME}` placeholder in a string against the vault. If
|
|
17
|
+
* any referenced key is missing, throws — secret refs are not optional.
|
|
18
|
+
*/
|
|
19
|
+
export async function resolveString(input, vault) {
|
|
20
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
21
|
+
if (!PLACEHOLDER_RE.test(input))
|
|
22
|
+
return input;
|
|
23
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
24
|
+
const names = new Set();
|
|
25
|
+
let m;
|
|
26
|
+
while ((m = PLACEHOLDER_RE.exec(input)))
|
|
27
|
+
names.add(m[1]);
|
|
28
|
+
const values = new Map();
|
|
29
|
+
for (const name of names) {
|
|
30
|
+
const value = await vault.get(name);
|
|
31
|
+
if (value === null) {
|
|
32
|
+
throw new MoxxyError({
|
|
33
|
+
code: 'CONFIG_INVALID',
|
|
34
|
+
message: `vault: missing required entry '${name}' referenced in config`,
|
|
35
|
+
hint: `Add it with \`/vault set ${name} <value>\` (or the \`vault_set\` tool), then retry.`,
|
|
36
|
+
context: { name },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
values.set(name, value);
|
|
40
|
+
}
|
|
41
|
+
return input.replace(PLACEHOLDER_RE, (_match, name) => values.get(name) ?? '');
|
|
42
|
+
}
|
|
43
|
+
/** Walk an arbitrary value, resolving all vault placeholders in nested strings. */
|
|
44
|
+
export async function resolveValue(value, vault) {
|
|
45
|
+
return resolveValueInner(value, vault, new Set());
|
|
46
|
+
}
|
|
47
|
+
// `ancestors` is the chain of objects on the path from the root to (but not
|
|
48
|
+
// including) the current node, so a node that appears as its own ancestor is a
|
|
49
|
+
// true cycle — while a shared object reachable via two *sibling* paths (a legal
|
|
50
|
+
// DAG) is resolved on each path rather than falsely flagged. The chain length
|
|
51
|
+
// is the nesting depth, so its size doubles as the depth bound.
|
|
52
|
+
async function resolveValueInner(value, vault, ancestors) {
|
|
53
|
+
if (typeof value === 'string')
|
|
54
|
+
return await resolveString(value, vault);
|
|
55
|
+
if (value && typeof value === 'object') {
|
|
56
|
+
if (ancestors.has(value))
|
|
57
|
+
throw tooDeepError(); // reference cycle
|
|
58
|
+
if (ancestors.size >= MAX_DEPTH)
|
|
59
|
+
throw tooDeepError();
|
|
60
|
+
const nextAncestors = new Set(ancestors).add(value);
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
return Promise.all(value.map((v) => resolveValueInner(v, vault, nextAncestors)));
|
|
63
|
+
}
|
|
64
|
+
// Resolve object properties concurrently (mirrors the array branch); each
|
|
65
|
+
// leaf may await vault.get(), so serializing them needlessly serializes I/O.
|
|
66
|
+
const pairs = await Promise.all(Object.entries(value).map(async ([k, v]) => [k, await resolveValueInner(v, vault, nextAncestors)]));
|
|
67
|
+
return Object.fromEntries(pairs);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
export function containsPlaceholder(value) {
|
|
72
|
+
return containsPlaceholderInner(value, new Set());
|
|
73
|
+
}
|
|
74
|
+
function containsPlaceholderInner(value, ancestors) {
|
|
75
|
+
if (typeof value === 'string') {
|
|
76
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
77
|
+
return PLACEHOLDER_RE.test(value);
|
|
78
|
+
}
|
|
79
|
+
if (value && typeof value === 'object') {
|
|
80
|
+
if (ancestors.has(value))
|
|
81
|
+
return false; // cycle — this subtree already inspected on the path
|
|
82
|
+
if (ancestors.size >= MAX_DEPTH)
|
|
83
|
+
throw tooDeepError();
|
|
84
|
+
const nextAncestors = new Set(ancestors).add(value);
|
|
85
|
+
if (Array.isArray(value)) {
|
|
86
|
+
return value.some((v) => containsPlaceholderInner(v, nextAncestors));
|
|
87
|
+
}
|
|
88
|
+
return Object.values(value).some((v) => containsPlaceholderInner(v, nextAncestors));
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=placeholder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placeholder.js","sourceRoot":"","sources":["../src/placeholder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAGxC,MAAM,cAAc,GAAG,gCAAgC,CAAC;AAExD,6EAA6E;AAC7E,8EAA8E;AAC9E,+EAA+E;AAC/E,8DAA8D;AAC9D,MAAM,SAAS,GAAG,EAAE,CAAC;AAErB,SAAS,YAAY;IACnB,OAAO,IAAI,UAAU,CAAC;QACpB,IAAI,EAAE,gBAAgB;QACtB,OAAO,EAAE,qCAAqC,SAAS,qCAAqC;QAC5F,IAAI,EAAE,wFAAwF;KAC/F,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAa,EAAE,KAAiB;IAClE,cAAc,CAAC,SAAS,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9C,cAAc,CAAC,SAAS,GAAG,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;IAE1D,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,MAAM,IAAI,UAAU,CAAC;gBACnB,IAAI,EAAE,gBAAgB;gBACtB,OAAO,EAAE,kCAAkC,IAAI,wBAAwB;gBACvE,IAAI,EAAE,4BAA4B,IAAI,qDAAqD;gBAC3F,OAAO,EAAE,EAAE,IAAI,EAAE;aAClB,CAAC,CAAC;QACL,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;AACzF,CAAC;AAED,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAc,EAAE,KAAiB;IAClE,OAAO,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;AACpD,CAAC;AAED,4EAA4E;AAC5E,+EAA+E;AAC/E,gFAAgF;AAChF,8EAA8E;AAC9E,gEAAgE;AAChE,KAAK,UAAU,iBAAiB,CAC9B,KAAc,EACd,KAAiB,EACjB,SAAsB;IAEtB,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACxE,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,MAAM,YAAY,EAAE,CAAC,CAAC,kBAAkB;QAClE,IAAI,SAAS,CAAC,IAAI,IAAI,SAAS;YAAE,MAAM,YAAY,EAAE,CAAC;QACtD,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;QACnF,CAAC;QACD,0EAA0E;QAC1E,6EAA6E;QAC7E,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAC7B,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,CAAC,GAAG,CAClD,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,iBAAiB,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAU,CACjF,CACF,CAAC;QACF,OAAO,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAc;IAChD,OAAO,wBAAwB,CAAC,KAAK,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;AACpD,CAAC;AAED,SAAS,wBAAwB,CAAC,KAAc,EAAE,SAAsB;IACtE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,cAAc,CAAC,SAAS,GAAG,CAAC,CAAC;QAC7B,OAAO,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IACD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC,CAAC,qDAAqD;QAC7F,IAAI,SAAS,CAAC,IAAI,IAAI,SAAS;YAAE,MAAM,YAAY,EAAE,CAAC;QACtD,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,wBAAwB,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC;QACvE,CAAC;QACD,OAAO,MAAM,CAAC,MAAM,CAAC,KAAgC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAChE,wBAAwB,CAAC,CAAC,EAAE,aAAa,CAAC,CAC3C,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type EncryptedBlob } from './crypto.js';
|
|
2
|
+
import type { MasterKeySource } from './keysource.js';
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when the supplied passphrase doesn't match the stored vault.
|
|
5
|
+
* Surfaced to the user with a recovery hint instead of the raw AES-GCM
|
|
6
|
+
* "Unsupported state or unable to authenticate data" error.
|
|
7
|
+
*/
|
|
8
|
+
export declare class VaultPassphraseError extends Error {
|
|
9
|
+
readonly filePath: string;
|
|
10
|
+
constructor(filePath: string);
|
|
11
|
+
}
|
|
12
|
+
export interface VaultEntry extends EncryptedBlob {
|
|
13
|
+
readonly createdAt: string;
|
|
14
|
+
readonly updatedAt: string;
|
|
15
|
+
readonly tags?: ReadonlyArray<string>;
|
|
16
|
+
}
|
|
17
|
+
export interface VaultEntryInfo {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly createdAt: string;
|
|
20
|
+
readonly updatedAt: string;
|
|
21
|
+
readonly tags?: ReadonlyArray<string>;
|
|
22
|
+
}
|
|
23
|
+
export interface VaultStoreOptions {
|
|
24
|
+
readonly filePath: string;
|
|
25
|
+
readonly keySource: MasterKeySource;
|
|
26
|
+
}
|
|
27
|
+
export declare class VaultStore {
|
|
28
|
+
private readonly filePath;
|
|
29
|
+
private readonly keySource;
|
|
30
|
+
private file;
|
|
31
|
+
private masterKey;
|
|
32
|
+
private readonly mutex;
|
|
33
|
+
private lastSynced;
|
|
34
|
+
constructor(opts: VaultStoreOptions);
|
|
35
|
+
open(): Promise<void>;
|
|
36
|
+
get sourceName(): string;
|
|
37
|
+
private load;
|
|
38
|
+
/**
|
|
39
|
+
* Obtain the master key and return a copy the store solely owns. Key sources
|
|
40
|
+
* may hand back a buffer they (or the caller) keep a reference to — copying
|
|
41
|
+
* lets `close()` zero our buffer without corrupting that shared one.
|
|
42
|
+
*/
|
|
43
|
+
private obtainOwnedKey;
|
|
44
|
+
private corruptError;
|
|
45
|
+
/**
|
|
46
|
+
* Confirm the master key decrypts the file's canary (or, for legacy
|
|
47
|
+
* vaults without a canary, the first stored entry). On mismatch throw
|
|
48
|
+
* a friendly `VaultPassphraseError` rather than letting the cryptic
|
|
49
|
+
* "Unsupported state or unable to authenticate data" error from
|
|
50
|
+
* Node's AES-GCM bubble up later.
|
|
51
|
+
*/
|
|
52
|
+
private verifyPassphrase;
|
|
53
|
+
/**
|
|
54
|
+
* Fold other writers' entries in from the on-disk file. Historically the
|
|
55
|
+
* vault persisted a whole-file snapshot of THIS instance's memory, so a
|
|
56
|
+
* write from any other `VaultStore` (another moxxy process, or a second
|
|
57
|
+
* instance in this one) was silently clobbered by our next persist —
|
|
58
|
+
* last-writer-wins, fatal for single-use rotated OAuth refresh tokens.
|
|
59
|
+
* Now every read and every mutation first re-reads the file (mtime/size
|
|
60
|
+
* gated, so the common no-other-writer case costs one `stat`) and merges:
|
|
61
|
+
* - key on both sides → newer `updatedAt` wins (ISO timestamps);
|
|
62
|
+
* - key only on disk → adopt it (another writer added it);
|
|
63
|
+
* - key only in memory → drop it (every mutation persists before
|
|
64
|
+
* returning, so a key missing on disk was deleted by another writer).
|
|
65
|
+
* Skipped when the on-disk salt differs (vault wiped/recreated — those
|
|
66
|
+
* entries are undecryptable under our master key) or the file is
|
|
67
|
+
* unreadable/corrupt (keep memory; the next persist restores a good file).
|
|
68
|
+
*
|
|
69
|
+
* Must be called while holding `this.mutex`.
|
|
70
|
+
*/
|
|
71
|
+
private syncFromDisk;
|
|
72
|
+
/**
|
|
73
|
+
* Crash-atomic write: serialize to a sibling tmp file, then rename. POSIX
|
|
74
|
+
* rename is atomic, so a crash mid-write leaves the previous vault intact
|
|
75
|
+
* rather than truncated. mode 0o600 keeps the secret store owner-only.
|
|
76
|
+
*/
|
|
77
|
+
private persist;
|
|
78
|
+
set(name: string, value: string, tags?: ReadonlyArray<string>): Promise<void>;
|
|
79
|
+
get(name: string): Promise<string | null>;
|
|
80
|
+
/**
|
|
81
|
+
* Decrypt a single stored blob, converting any low-level crypto failure into
|
|
82
|
+
* a friendly `VAULT_CORRUPT` error. `validateVaultFile` only guarantees the
|
|
83
|
+
* blob's iv/tag/data are *strings*, not that they base64-decode to the right
|
|
84
|
+
* lengths — a partially-corrupted entry (e.g. a truncated `iv` or `tag`, or a
|
|
85
|
+
* value re-keyed out-of-band) passes structural validation yet makes Node's
|
|
86
|
+
* AES-GCM throw a raw `ERR_CRYPTO_INVALID_IV` / `ERR_CRYPTO_INVALID_AUTH_TAG`
|
|
87
|
+
* / auth-tag-mismatch error. The vault's passphrase is already verified
|
|
88
|
+
* against the canary on open(), so a per-entry decrypt failure means THAT
|
|
89
|
+
* entry is corrupt, not that the whole vault is locked — surface a targeted
|
|
90
|
+
* recovery hint instead of crashing the caller with a cryptic crypto error.
|
|
91
|
+
*/
|
|
92
|
+
private decryptEntry;
|
|
93
|
+
has(name: string): Promise<boolean>;
|
|
94
|
+
delete(name: string): Promise<boolean>;
|
|
95
|
+
list(): Promise<ReadonlyArray<VaultEntryInfo>>;
|
|
96
|
+
/**
|
|
97
|
+
* Wipe the in-memory master key and cached file so the AES key is no longer
|
|
98
|
+
* recoverable from a heap/core dump or swap after the store is done. The
|
|
99
|
+
* store can be reopened — `open()` re-derives the key. Returned plaintext
|
|
100
|
+
* strings from `get()` cannot be wiped (JS strings are immutable); only the
|
|
101
|
+
* long-lived key Buffer is zeroed here.
|
|
102
|
+
*/
|
|
103
|
+
close(): void;
|
|
104
|
+
[Symbol.dispose](): void;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkC,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AA2BtD;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;aACjB,QAAQ,EAAE,MAAM;gBAAhB,QAAQ,EAAE,MAAM;CAU7C;AAED,MAAM,WAAW,UAAW,SAAQ,aAAa;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;CACrC;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkB;IAC5C,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,SAAS,CAAuB;IAKxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAG9C,OAAO,CAAC,UAAU,CAAkD;gBAExD,IAAI,EAAE,iBAAiB;IAK7B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ3B,IAAI,UAAU,IAAI,MAAM,CAEvB;YAEa,IAAI;IAwDlB;;;;OAIG;YACW,cAAc;IAI5B,OAAO,CAAC,YAAY;IASpB;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAexB;;;;;;;;;;;;;;;;;OAiBG;YACW,YAAY;IA6C1B;;;;OAIG;YACW,OAAO;IAYf,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAwB7E,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAc/C;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,YAAY;IAcd,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQnC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IActC,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC;IAcpD;;;;;;OAMG;IACH,KAAK,IAAI,IAAI;IAOb,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAGzB"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { createMutex, MoxxyError } from '@moxxy/sdk';
|
|
3
|
+
import { writeFileAtomic } from '@moxxy/sdk/server';
|
|
4
|
+
import { decrypt, encrypt, generateSalt } from './crypto.js';
|
|
5
|
+
const CANARY_PLAINTEXT = 'moxxy:vault:v1';
|
|
6
|
+
// Upper bound on filesystem mtime granularity (HFS+/old ext are ~1-2s). Within
|
|
7
|
+
// this window of "now" an unchanged (mtime,size) fingerprint can't be trusted
|
|
8
|
+
// to mean "no other writer", so syncFromDisk re-reads instead of fast-pathing.
|
|
9
|
+
const MTIME_GRANULARITY_MS = 2000;
|
|
10
|
+
/**
|
|
11
|
+
* Thrown when the supplied passphrase doesn't match the stored vault.
|
|
12
|
+
* Surfaced to the user with a recovery hint instead of the raw AES-GCM
|
|
13
|
+
* "Unsupported state or unable to authenticate data" error.
|
|
14
|
+
*/
|
|
15
|
+
export class VaultPassphraseError extends Error {
|
|
16
|
+
filePath;
|
|
17
|
+
constructor(filePath) {
|
|
18
|
+
super(`Wrong vault passphrase for ${filePath}.\n` +
|
|
19
|
+
` If you've forgotten it, wipe the vault and key cache, then re-run \`moxxy init\`:\n` +
|
|
20
|
+
` rm ${filePath} ~/.moxxy/vault.key\n` +
|
|
21
|
+
` (If the OS keychain holds a cached key, also clear it: \`security delete-generic-password -s moxxy\` on macOS.)\n` +
|
|
22
|
+
` Or set MOXXY_VAULT_PASSPHRASE to a known value to skip the prompt entirely.`);
|
|
23
|
+
this.filePath = filePath;
|
|
24
|
+
this.name = 'VaultPassphraseError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class VaultStore {
|
|
28
|
+
filePath;
|
|
29
|
+
keySource;
|
|
30
|
+
file = null;
|
|
31
|
+
masterKey = null;
|
|
32
|
+
// Serializes every mutator (set/delete) and persist() so concurrent
|
|
33
|
+
// writers don't clobber each other through the read-modify-write +
|
|
34
|
+
// whole-file rewrite. open() is also chained through this so two
|
|
35
|
+
// parallel `open()` calls never both derive a fresh salt.
|
|
36
|
+
mutex = createMutex();
|
|
37
|
+
// stat() fingerprint of the file as of our last load/sync/persist; lets
|
|
38
|
+
// syncFromDisk() skip the read+parse when nothing else has written.
|
|
39
|
+
lastSynced = null;
|
|
40
|
+
constructor(opts) {
|
|
41
|
+
this.filePath = opts.filePath;
|
|
42
|
+
this.keySource = opts.keySource;
|
|
43
|
+
}
|
|
44
|
+
async open() {
|
|
45
|
+
if (this.file && this.masterKey)
|
|
46
|
+
return;
|
|
47
|
+
return this.mutex.run(async () => {
|
|
48
|
+
if (this.file && this.masterKey)
|
|
49
|
+
return;
|
|
50
|
+
await this.load();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
get sourceName() {
|
|
54
|
+
return this.keySource.name;
|
|
55
|
+
}
|
|
56
|
+
async load() {
|
|
57
|
+
let raw = null;
|
|
58
|
+
try {
|
|
59
|
+
raw = await fs.readFile(this.filePath, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (!isEnoent(err))
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
if (raw === null) {
|
|
66
|
+
const salt = generateSalt();
|
|
67
|
+
this.masterKey = await this.obtainOwnedKey(salt);
|
|
68
|
+
this.file = {
|
|
69
|
+
version: 1,
|
|
70
|
+
kdf: 'scrypt',
|
|
71
|
+
salt: salt.toString('base64'),
|
|
72
|
+
entries: {},
|
|
73
|
+
canary: encrypt(CANARY_PLAINTEXT, this.masterKey),
|
|
74
|
+
};
|
|
75
|
+
await this.persist();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
let parsedRaw;
|
|
79
|
+
try {
|
|
80
|
+
parsedRaw = JSON.parse(raw);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
throw this.corruptError('Vault file is not valid JSON');
|
|
84
|
+
}
|
|
85
|
+
if (!isPlainObject(parsedRaw)) {
|
|
86
|
+
throw this.corruptError('Vault file is not a JSON object');
|
|
87
|
+
}
|
|
88
|
+
const parsed = parsedRaw;
|
|
89
|
+
if (parsed.version !== 1 || parsed.kdf !== 'scrypt') {
|
|
90
|
+
throw new MoxxyError({
|
|
91
|
+
code: 'VAULT_CORRUPT',
|
|
92
|
+
message: `Unsupported vault file: version=${String(parsed.version)} kdf=${String(parsed.kdf)}`,
|
|
93
|
+
hint: `This vault was written by an incompatible version. Back up and remove ${this.filePath} to re-initialize.`,
|
|
94
|
+
context: { filePath: this.filePath },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (!validateVaultFile(parsed)) {
|
|
98
|
+
throw this.corruptError('Vault file is malformed (bad salt/entries/canary shape)');
|
|
99
|
+
}
|
|
100
|
+
this.file = parsed;
|
|
101
|
+
const salt = Buffer.from(parsed.salt, 'base64');
|
|
102
|
+
this.masterKey = await this.obtainOwnedKey(salt);
|
|
103
|
+
this.verifyPassphrase();
|
|
104
|
+
// Backfill the canary on the first successful open of a legacy vault.
|
|
105
|
+
if (!this.file.canary) {
|
|
106
|
+
this.file = { ...this.file, canary: encrypt(CANARY_PLAINTEXT, this.masterKey) };
|
|
107
|
+
try {
|
|
108
|
+
await this.persist();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Best-effort — failing to backfill doesn't break the open.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Obtain the master key and return a copy the store solely owns. Key sources
|
|
117
|
+
* may hand back a buffer they (or the caller) keep a reference to — copying
|
|
118
|
+
* lets `close()` zero our buffer without corrupting that shared one.
|
|
119
|
+
*/
|
|
120
|
+
async obtainOwnedKey(salt) {
|
|
121
|
+
return Buffer.from(await this.keySource.obtain(salt));
|
|
122
|
+
}
|
|
123
|
+
corruptError(message) {
|
|
124
|
+
return new MoxxyError({
|
|
125
|
+
code: 'VAULT_CORRUPT',
|
|
126
|
+
message,
|
|
127
|
+
hint: `Back up and remove ${this.filePath} to re-initialize the vault.`,
|
|
128
|
+
context: { filePath: this.filePath },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Confirm the master key decrypts the file's canary (or, for legacy
|
|
133
|
+
* vaults without a canary, the first stored entry). On mismatch throw
|
|
134
|
+
* a friendly `VaultPassphraseError` rather than letting the cryptic
|
|
135
|
+
* "Unsupported state or unable to authenticate data" error from
|
|
136
|
+
* Node's AES-GCM bubble up later.
|
|
137
|
+
*/
|
|
138
|
+
verifyPassphrase() {
|
|
139
|
+
if (!this.file || !this.masterKey)
|
|
140
|
+
return;
|
|
141
|
+
const probe = this.file.canary ?? firstEntry(this.file.entries);
|
|
142
|
+
if (!probe)
|
|
143
|
+
return; // empty legacy vault — nothing to verify yet.
|
|
144
|
+
try {
|
|
145
|
+
const plaintext = decrypt(probe, this.masterKey);
|
|
146
|
+
if (this.file.canary && plaintext !== CANARY_PLAINTEXT) {
|
|
147
|
+
throw new VaultPassphraseError(this.filePath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
if (err instanceof VaultPassphraseError)
|
|
152
|
+
throw err;
|
|
153
|
+
throw new VaultPassphraseError(this.filePath);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Fold other writers' entries in from the on-disk file. Historically the
|
|
158
|
+
* vault persisted a whole-file snapshot of THIS instance's memory, so a
|
|
159
|
+
* write from any other `VaultStore` (another moxxy process, or a second
|
|
160
|
+
* instance in this one) was silently clobbered by our next persist —
|
|
161
|
+
* last-writer-wins, fatal for single-use rotated OAuth refresh tokens.
|
|
162
|
+
* Now every read and every mutation first re-reads the file (mtime/size
|
|
163
|
+
* gated, so the common no-other-writer case costs one `stat`) and merges:
|
|
164
|
+
* - key on both sides → newer `updatedAt` wins (ISO timestamps);
|
|
165
|
+
* - key only on disk → adopt it (another writer added it);
|
|
166
|
+
* - key only in memory → drop it (every mutation persists before
|
|
167
|
+
* returning, so a key missing on disk was deleted by another writer).
|
|
168
|
+
* Skipped when the on-disk salt differs (vault wiped/recreated — those
|
|
169
|
+
* entries are undecryptable under our master key) or the file is
|
|
170
|
+
* unreadable/corrupt (keep memory; the next persist restores a good file).
|
|
171
|
+
*
|
|
172
|
+
* Must be called while holding `this.mutex`.
|
|
173
|
+
*/
|
|
174
|
+
async syncFromDisk() {
|
|
175
|
+
if (!this.file)
|
|
176
|
+
return;
|
|
177
|
+
let st;
|
|
178
|
+
try {
|
|
179
|
+
st = await fs.stat(this.filePath);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
if (isEnoent(err))
|
|
183
|
+
return; // deleted out from under us — keep memory
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
// Fast path: skip the read+merge only when the stat fingerprint is
|
|
187
|
+
// unchanged AND it has aged past the filesystem's mtime granularity. On
|
|
188
|
+
// coarse-mtime filesystems (HFS+, older ext) two writes inside the same
|
|
189
|
+
// ~1-2s tick that also produce an identical byte length share a
|
|
190
|
+
// (mtime,size) fingerprint, so a recent match might hide a sibling write.
|
|
191
|
+
// The merge is idempotent and cheap, so when in doubt we re-read.
|
|
192
|
+
if (this.lastSynced &&
|
|
193
|
+
st.mtimeMs === this.lastSynced.mtimeMs &&
|
|
194
|
+
st.size === this.lastSynced.size &&
|
|
195
|
+
Date.now() - st.mtimeMs > MTIME_GRANULARITY_MS) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
let parsedRaw;
|
|
199
|
+
try {
|
|
200
|
+
parsedRaw = JSON.parse(await fs.readFile(this.filePath, 'utf8'));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!isPlainObject(parsedRaw))
|
|
206
|
+
return;
|
|
207
|
+
const parsed = parsedRaw;
|
|
208
|
+
if (parsed.version !== 1 ||
|
|
209
|
+
parsed.kdf !== 'scrypt' ||
|
|
210
|
+
parsed.salt !== this.file.salt ||
|
|
211
|
+
!validateVaultFile(parsed)) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.file = { ...this.file, entries: mergeEntries(this.file.entries, parsed.entries) };
|
|
215
|
+
// Fingerprint from BEFORE the read: if a write landed in between, the
|
|
216
|
+
// next sync simply re-reads — never the other way around.
|
|
217
|
+
this.lastSynced = { mtimeMs: st.mtimeMs, size: st.size };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Crash-atomic write: serialize to a sibling tmp file, then rename. POSIX
|
|
221
|
+
* rename is atomic, so a crash mid-write leaves the previous vault intact
|
|
222
|
+
* rather than truncated. mode 0o600 keeps the secret store owner-only.
|
|
223
|
+
*/
|
|
224
|
+
async persist() {
|
|
225
|
+
if (!this.file)
|
|
226
|
+
return;
|
|
227
|
+
await writeFileAtomic(this.filePath, JSON.stringify(this.file, null, 2), { mode: 0o600 });
|
|
228
|
+
// Force the next syncFromDisk() to re-read rather than recording a stat
|
|
229
|
+
// here: between our rename and a post-write fs.stat another process may
|
|
230
|
+
// have renamed ITS version in, so the fingerprint we'd capture could be
|
|
231
|
+
// a foreign file's — which would then make us skip reading the very
|
|
232
|
+
// update we're out of sync with. Dropping the fingerprint costs one
|
|
233
|
+
// extra (idempotent, merge-only) read on the next access.
|
|
234
|
+
this.lastSynced = null;
|
|
235
|
+
}
|
|
236
|
+
async set(name, value, tags) {
|
|
237
|
+
await this.open();
|
|
238
|
+
return this.mutex.run(async () => {
|
|
239
|
+
if (!this.file || !this.masterKey)
|
|
240
|
+
throw new Error('vault not open');
|
|
241
|
+
await this.syncFromDisk();
|
|
242
|
+
const now = new Date().toISOString();
|
|
243
|
+
const existing = this.file.entries[name];
|
|
244
|
+
const blob = encrypt(value, this.masterKey);
|
|
245
|
+
this.file = {
|
|
246
|
+
...this.file,
|
|
247
|
+
entries: {
|
|
248
|
+
...this.file.entries,
|
|
249
|
+
[name]: {
|
|
250
|
+
...blob,
|
|
251
|
+
createdAt: existing?.createdAt ?? now,
|
|
252
|
+
updatedAt: now,
|
|
253
|
+
tags: tags ?? existing?.tags,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
await this.persist();
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
async get(name) {
|
|
261
|
+
await this.open();
|
|
262
|
+
// Read the entry inside the same mutex turn as syncFromDisk so a
|
|
263
|
+
// concurrent set()/delete() (which replaces this.file wholesale) can't
|
|
264
|
+
// swap the snapshot out between the sync and the decrypt.
|
|
265
|
+
return this.mutex.run(async () => {
|
|
266
|
+
await this.syncFromDisk();
|
|
267
|
+
if (!this.file || !this.masterKey)
|
|
268
|
+
throw new Error('vault not open');
|
|
269
|
+
const entry = this.file.entries[name];
|
|
270
|
+
if (!entry)
|
|
271
|
+
return null;
|
|
272
|
+
return this.decryptEntry(name, entry);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Decrypt a single stored blob, converting any low-level crypto failure into
|
|
277
|
+
* a friendly `VAULT_CORRUPT` error. `validateVaultFile` only guarantees the
|
|
278
|
+
* blob's iv/tag/data are *strings*, not that they base64-decode to the right
|
|
279
|
+
* lengths — a partially-corrupted entry (e.g. a truncated `iv` or `tag`, or a
|
|
280
|
+
* value re-keyed out-of-band) passes structural validation yet makes Node's
|
|
281
|
+
* AES-GCM throw a raw `ERR_CRYPTO_INVALID_IV` / `ERR_CRYPTO_INVALID_AUTH_TAG`
|
|
282
|
+
* / auth-tag-mismatch error. The vault's passphrase is already verified
|
|
283
|
+
* against the canary on open(), so a per-entry decrypt failure means THAT
|
|
284
|
+
* entry is corrupt, not that the whole vault is locked — surface a targeted
|
|
285
|
+
* recovery hint instead of crashing the caller with a cryptic crypto error.
|
|
286
|
+
*/
|
|
287
|
+
decryptEntry(name, entry) {
|
|
288
|
+
try {
|
|
289
|
+
return decrypt(entry, this.masterKey);
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
throw new MoxxyError({
|
|
293
|
+
code: 'VAULT_CORRUPT',
|
|
294
|
+
message: `vault: entry '${name}' could not be decrypted (corrupt or re-keyed)`,
|
|
295
|
+
hint: `Re-store it with \`/vault set ${name} <value>\`, or back up and remove ${this.filePath} to re-initialize.`,
|
|
296
|
+
context: { filePath: this.filePath, name },
|
|
297
|
+
cause: err,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async has(name) {
|
|
302
|
+
await this.open();
|
|
303
|
+
return this.mutex.run(async () => {
|
|
304
|
+
await this.syncFromDisk();
|
|
305
|
+
return Boolean(this.file?.entries[name]);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
async delete(name) {
|
|
309
|
+
await this.open();
|
|
310
|
+
return this.mutex.run(async () => {
|
|
311
|
+
if (!this.file)
|
|
312
|
+
return false;
|
|
313
|
+
await this.syncFromDisk();
|
|
314
|
+
if (!(name in this.file.entries))
|
|
315
|
+
return false;
|
|
316
|
+
const { [name]: _removed, ...rest } = this.file.entries;
|
|
317
|
+
void _removed;
|
|
318
|
+
this.file = { ...this.file, entries: rest };
|
|
319
|
+
await this.persist();
|
|
320
|
+
return true;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
async list() {
|
|
324
|
+
await this.open();
|
|
325
|
+
return this.mutex.run(async () => {
|
|
326
|
+
await this.syncFromDisk();
|
|
327
|
+
if (!this.file)
|
|
328
|
+
return [];
|
|
329
|
+
return Object.entries(this.file.entries).map(([name, e]) => ({
|
|
330
|
+
name,
|
|
331
|
+
createdAt: e.createdAt,
|
|
332
|
+
updatedAt: e.updatedAt,
|
|
333
|
+
tags: e.tags,
|
|
334
|
+
}));
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Wipe the in-memory master key and cached file so the AES key is no longer
|
|
339
|
+
* recoverable from a heap/core dump or swap after the store is done. The
|
|
340
|
+
* store can be reopened — `open()` re-derives the key. Returned plaintext
|
|
341
|
+
* strings from `get()` cannot be wiped (JS strings are immutable); only the
|
|
342
|
+
* long-lived key Buffer is zeroed here.
|
|
343
|
+
*/
|
|
344
|
+
close() {
|
|
345
|
+
this.masterKey?.fill(0);
|
|
346
|
+
this.masterKey = null;
|
|
347
|
+
this.file = null;
|
|
348
|
+
this.lastSynced = null;
|
|
349
|
+
}
|
|
350
|
+
[Symbol.dispose]() {
|
|
351
|
+
this.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Merge our in-memory entries with another writer's on-disk entries. Disk is
|
|
356
|
+
* the base (all current writers merge-before-persist, so disk is the union of
|
|
357
|
+
* everyone's persisted state); when both sides hold a key, the newer
|
|
358
|
+
* `updatedAt` wins so a stale whole-file write from an older moxxy can't roll
|
|
359
|
+
* back a fresher value (e.g. a just-rotated OAuth refresh token).
|
|
360
|
+
*/
|
|
361
|
+
function mergeEntries(memory, disk) {
|
|
362
|
+
const merged = { ...disk };
|
|
363
|
+
for (const [name, mine] of Object.entries(memory)) {
|
|
364
|
+
const theirs = merged[name];
|
|
365
|
+
if (theirs && theirs.updatedAt >= mine.updatedAt)
|
|
366
|
+
continue;
|
|
367
|
+
if (!theirs)
|
|
368
|
+
continue; // absent on disk → deleted by another writer
|
|
369
|
+
merged[name] = mine;
|
|
370
|
+
}
|
|
371
|
+
return merged;
|
|
372
|
+
}
|
|
373
|
+
function isEnoent(err) {
|
|
374
|
+
return err instanceof Error && 'code' in err && err.code === 'ENOENT';
|
|
375
|
+
}
|
|
376
|
+
function isPlainObject(v) {
|
|
377
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
378
|
+
}
|
|
379
|
+
function isEncryptedBlob(v) {
|
|
380
|
+
return (isPlainObject(v) &&
|
|
381
|
+
typeof v.iv === 'string' &&
|
|
382
|
+
typeof v.tag === 'string' &&
|
|
383
|
+
typeof v.data === 'string');
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Structural validation of a parsed vault file beyond the version/kdf gate:
|
|
387
|
+
* `salt` is a string, `entries` is a plain object whose every value is a
|
|
388
|
+
* well-formed encrypted blob (with createdAt/updatedAt strings), and `canary`
|
|
389
|
+
* (when present) is a blob. Guards against partial writes / manual edits that
|
|
390
|
+
* are valid JSON but would otherwise throw a raw TypeError in
|
|
391
|
+
* verifyPassphrase()/list() instead of a friendly VAULT_CORRUPT error.
|
|
392
|
+
*/
|
|
393
|
+
function validateVaultFile(parsed) {
|
|
394
|
+
if (typeof parsed.salt !== 'string')
|
|
395
|
+
return false;
|
|
396
|
+
if (!isPlainObject(parsed.entries))
|
|
397
|
+
return false;
|
|
398
|
+
for (const entry of Object.values(parsed.entries)) {
|
|
399
|
+
if (!isEncryptedBlob(entry))
|
|
400
|
+
return false;
|
|
401
|
+
const e = entry;
|
|
402
|
+
if (typeof e.createdAt !== 'string' || typeof e.updatedAt !== 'string')
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
if (parsed.canary !== undefined && !isEncryptedBlob(parsed.canary))
|
|
406
|
+
return false;
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
function firstEntry(entries) {
|
|
410
|
+
for (const key of Object.keys(entries)) {
|
|
411
|
+
return entries[key];
|
|
412
|
+
}
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|