@mstone6969/vault 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -6
- package/dist/crypto.d.ts +99 -2
- package/dist/crypto.d.ts.map +1 -1
- package/dist/errors.d.ts +122 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +447 -33
- package/dist/index.js.map +9 -7
- package/dist/providers.d.ts +131 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/stores/file.d.ts +190 -0
- package/dist/stores/file.d.ts.map +1 -0
- package/dist/stores/file.js +1 -0
- package/dist/stores/memory.d.ts +98 -7
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/sqlite.d.ts +140 -9
- package/dist/stores/sqlite.d.ts.map +1 -1
- package/dist/stores/sqlite.js +47 -12
- package/dist/stores/sqlite.js.map +3 -3
- package/dist/types.d.ts +408 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/vault.d.ts +554 -19
- package/dist/vault.d.ts.map +1 -1
- package/docs/README.md +10 -0
- package/docs/index/README.md +48 -0
- package/docs/index/classes/FileStore.md +341 -0
- package/docs/index/classes/MemoryStore.md +240 -0
- package/docs/index/classes/Vault.md +805 -0
- package/docs/index/classes/VaultError.md +371 -0
- package/docs/index/classes/VaultKeyError.md +370 -0
- package/docs/index/functions/envKey.md +43 -0
- package/docs/index/functions/fileKey.md +46 -0
- package/docs/index/functions/generateKey.md +39 -0
- package/docs/index/functions/importKey.md +49 -0
- package/docs/index/functions/isKeyProvider.md +43 -0
- package/docs/index/functions/open.md +67 -0
- package/docs/index/functions/randomValue.md +53 -0
- package/docs/index/functions/seal.md +56 -0
- package/docs/index/functions/staticKey.md +39 -0
- package/docs/index/type-aliases/Generator.md +58 -0
- package/docs/index/type-aliases/HistoryEntry.md +65 -0
- package/docs/index/type-aliases/KeyProvider.md +74 -0
- package/docs/index/type-aliases/PutOptions.md +141 -0
- package/docs/index/type-aliases/RekeyReport.md +37 -0
- package/docs/index/type-aliases/RotationContext.md +49 -0
- package/docs/index/type-aliases/RotationPolicy.md +142 -0
- package/docs/index/type-aliases/SecretRecord.md +214 -0
- package/docs/index/type-aliases/SecretSummary.md +56 -0
- package/docs/index/type-aliases/VaultEvent.md +95 -0
- package/docs/index/type-aliases/VaultOptions.md +142 -0
- package/docs/index/type-aliases/VaultStore.md +156 -0
- package/docs/index/variables/DEFAULT_ALPHABET.md +29 -0
- package/docs/index/variables/DEFAULT_HISTORY_LIMIT.md +28 -0
- package/docs/index/variables/DEFAULT_PREFIX.md +23 -0
- package/docs/stores/sqlite/README.md +11 -0
- package/docs/stores/sqlite/classes/SqliteStore.md +307 -0
- package/package.json +15 -5
package/dist/stores/memory.d.ts
CHANGED
|
@@ -1,16 +1,107 @@
|
|
|
1
1
|
import type { SecretRecord, VaultStore } from "../types";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Keeps sealed values in a Map. Handy for tests and short-lived processes.
|
|
4
|
+
*
|
|
5
|
+
* Nothing is written anywhere: when the process ends every record goes with it.
|
|
6
|
+
* That is the point — a test gets a clean vault per run without a file to
|
|
7
|
+
* create and delete, and a short-lived worker holding a few credentials in
|
|
8
|
+
* memory leaves nothing behind on disk.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* Like every {@link VaultStore}, this persists records exactly as given and
|
|
12
|
+
* enforces nothing: finality, expiry and history are rules the {@link Vault}
|
|
13
|
+
* applies before it calls {@link MemoryStore.put}.
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { generateKey, MemoryStore, Vault } from "@mstone6969/vault"
|
|
17
|
+
*
|
|
18
|
+
* const vault = new Vault({ key: generateKey(), store: new MemoryStore() })
|
|
19
|
+
* await vault.put("alice", "db-password", "hunter2")
|
|
20
|
+
* ```
|
|
21
|
+
* @see {@link FileStore} for a store that survives the process.
|
|
22
|
+
*/
|
|
3
23
|
export declare class MemoryStore implements VaultStore {
|
|
24
|
+
/** Records by `owner name`, the key {@link MemoryStore.key} builds. */
|
|
4
25
|
private readonly records;
|
|
26
|
+
/**
|
|
27
|
+
* Makes an empty store.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Takes nothing: there is no file to name and no key to hand it, since
|
|
31
|
+
* records never leave the process.
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { MemoryStore } from "@mstone6969/vault"
|
|
35
|
+
*
|
|
36
|
+
* const store = new MemoryStore()
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
constructor();
|
|
40
|
+
/**
|
|
41
|
+
* The Map key for one entry: owners are separate namespaces, so the name
|
|
42
|
+
* alone is not unique.
|
|
43
|
+
*
|
|
44
|
+
* @param owner Whose entry it is.
|
|
45
|
+
* @param name What the entry is called.
|
|
46
|
+
* @returns The two joined by a space.
|
|
47
|
+
*/
|
|
5
48
|
private static key;
|
|
49
|
+
/**
|
|
50
|
+
* One record, or null when there is none under that name.
|
|
51
|
+
*
|
|
52
|
+
* @param owner Whose entry to look for.
|
|
53
|
+
* @param name What the entry is called.
|
|
54
|
+
* @returns The stored record, or null if this owner has nothing by that
|
|
55
|
+
* name. The record itself is returned, not a copy, so callers must not
|
|
56
|
+
* mutate it.
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { MemoryStore } from "@mstone6969/vault"
|
|
60
|
+
*
|
|
61
|
+
* const store = new MemoryStore()
|
|
62
|
+
* const record = await store.get("alice", "db-password") // null
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
6
65
|
get(owner: string, name: string): Promise<SecretRecord | null>;
|
|
66
|
+
/**
|
|
67
|
+
* Every record one owner holds.
|
|
68
|
+
*
|
|
69
|
+
* @param owner Whose entries to return.
|
|
70
|
+
* @returns That owner's records, in no particular order — the vault's
|
|
71
|
+
* `list` sorts them. Empty for an owner the store has never heard of.
|
|
72
|
+
* @see {@link MemoryStore.all} when the caller needs every owner's records.
|
|
73
|
+
*/
|
|
7
74
|
list(owner: string): Promise<SecretRecord[]>;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Every record, whoever owns it.
|
|
77
|
+
*
|
|
78
|
+
* @returns All records the store holds, in no particular order.
|
|
79
|
+
* @remarks
|
|
80
|
+
* This is the only place anything reaches across owners, and it exists for
|
|
81
|
+
* the vault-wide operations — `rekey`, `reseal` and `purgeExpired` — which
|
|
82
|
+
* have to touch every entry. Ordinary reads go through
|
|
83
|
+
* {@link MemoryStore.list}.
|
|
84
|
+
*/
|
|
85
|
+
all(): Promise<SecretRecord[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Writes a record, replacing any under the same owner and name.
|
|
88
|
+
*
|
|
89
|
+
* @param record The record to keep, stored as given.
|
|
90
|
+
* @returns The same record, so a caller can write and use the result in one
|
|
91
|
+
* step.
|
|
92
|
+
* @remarks
|
|
93
|
+
* An `isFinal` record is replaced here without complaint: refusing that is
|
|
94
|
+
* the vault's job, and it checks before it calls this.
|
|
95
|
+
*/
|
|
96
|
+
put(record: SecretRecord): Promise<SecretRecord>;
|
|
97
|
+
/**
|
|
98
|
+
* Deletes a record, returning false when there was nothing to delete.
|
|
99
|
+
*
|
|
100
|
+
* @param owner Whose entry to delete.
|
|
101
|
+
* @param name What the entry is called.
|
|
102
|
+
* @returns True if a record was removed, false if there was none — so a
|
|
103
|
+
* caller can tell a delete from a no-op.
|
|
104
|
+
*/
|
|
14
105
|
remove(owner: string, name: string): Promise<boolean>;
|
|
15
106
|
}
|
|
16
107
|
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAExD
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAExD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,WAAY,YAAW,UAAU;IAC1C,uEAAuE;IACvE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IAEnD;;;;;;;;;;;;OAYG;;IAKH;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,GAAG;IAIlB;;;;;;;;;;;;;;;OAeG;IACG,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAIpE;;;;;;;OAOG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAIlD;;;;;;;;;OASG;IACG,GAAG,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAIpC;;;;;;;;;OASG;IACG,GAAG,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAKtD;;;;;;;OAOG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAG9D"}
|
package/dist/stores/sqlite.d.ts
CHANGED
|
@@ -1,24 +1,155 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import type { SecretRecord, VaultStore } from "../types";
|
|
3
3
|
/**
|
|
4
|
-
* Keeps
|
|
5
|
-
*
|
|
4
|
+
* Keeps records in SQLite through `bun:sqlite`. Pass a file path, ":memory:",
|
|
5
|
+
* or a Database you already opened.
|
|
6
|
+
*
|
|
7
|
+
* Values are sealed, but owners, names and metadata are ordinary columns —
|
|
8
|
+
* anyone who can read the file learns what you keep, if not what it says. Use
|
|
9
|
+
* `FileStore` when even that should be hidden.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* `bun:sqlite` makes this store Bun-only: importing it on Node fails at the
|
|
13
|
+
* import, not at the first query. That is why it is reached through
|
|
14
|
+
* `@mstone6969/vault/stores/sqlite` instead of the package's main entry —
|
|
15
|
+
* a Node program that imports the main entry never loads this file, and never
|
|
16
|
+
* sees the Bun-only import.
|
|
17
|
+
*
|
|
18
|
+
* Every method is `async` only to satisfy {@link VaultStore}; `bun:sqlite` is
|
|
19
|
+
* synchronous, so nothing here waits on I/O.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { Vault } from "@mstone6969/vault"
|
|
24
|
+
* import { SqliteStore } from "@mstone6969/vault/stores/sqlite"
|
|
25
|
+
*
|
|
26
|
+
* const store = new SqliteStore("./vault.sqlite")
|
|
27
|
+
* const vault = new Vault({ key: process.env.VAULT_KEY!, store })
|
|
28
|
+
*
|
|
29
|
+
* await vault.put("alice", "db-password", "hunter2", {
|
|
30
|
+
* metadata: { kind: "postgres" },
|
|
31
|
+
* })
|
|
32
|
+
* console.log(await vault.open("alice", "db-password")) // "hunter2"
|
|
33
|
+
* store.close()
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @see {@link VaultStore} for what a store owes the vault.
|
|
6
37
|
*/
|
|
7
38
|
export declare class SqliteStore implements VaultStore {
|
|
8
39
|
private readonly db;
|
|
9
40
|
private readonly table;
|
|
41
|
+
/**
|
|
42
|
+
* Opens the database if it was given a path, and makes sure the table is
|
|
43
|
+
* there and current.
|
|
44
|
+
*
|
|
45
|
+
* The table is created in its 0.1.0 shape if it is missing, and then every
|
|
46
|
+
* column added since — `sealed_key`, `plain`, `is_sealed`, `is_final`,
|
|
47
|
+
* `expires_at`, `history`, `rotation`, `rotated_at`, `metadata` — is
|
|
48
|
+
* ALTERed in if it is absent. So a table written by an older version of
|
|
49
|
+
* the package is upgraded the moment a newer one opens it, in place and
|
|
50
|
+
* without a migration step of your own, and rows that predate a column
|
|
51
|
+
* read back with sensible defaults: no history, no metadata, sealed, not
|
|
52
|
+
* final, no expiry.
|
|
53
|
+
*
|
|
54
|
+
* @param database A file path to open, ":memory:" for a database that
|
|
55
|
+
* lasts as long as the process, or a `Database` you already opened —
|
|
56
|
+
* useful for keeping the vault's table beside your own in one file, and
|
|
57
|
+
* inside your own transactions.
|
|
58
|
+
* @defaultValue `":memory:"`
|
|
59
|
+
* @param table Which table to keep records in. Give it a name of its own
|
|
60
|
+
* when one database holds several vaults.
|
|
61
|
+
* @defaultValue `"vault_secrets"`
|
|
62
|
+
* @throws Whatever `bun:sqlite` throws when the path cannot be opened, or
|
|
63
|
+
* when `table` already exists with an incompatible shape.
|
|
64
|
+
*
|
|
65
|
+
* @example Sharing a database you already opened
|
|
66
|
+
* ```ts
|
|
67
|
+
* import { Database } from "bun:sqlite"
|
|
68
|
+
* import { SqliteStore } from "@mstone6969/vault/stores/sqlite"
|
|
69
|
+
*
|
|
70
|
+
* const db = new Database("./app.sqlite")
|
|
71
|
+
* const store = new SqliteStore(db, "shared_secrets")
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
10
74
|
constructor(database?: string | Database, table?: string);
|
|
75
|
+
/**
|
|
76
|
+
* Turns a row back into a record: JSON columns parsed, epoch milliseconds
|
|
77
|
+
* back into `Date`, integers back into booleans.
|
|
78
|
+
*
|
|
79
|
+
* Every column added after 0.1.0 is read defensively, because a row
|
|
80
|
+
* written before that column existed reads back as null.
|
|
81
|
+
*
|
|
82
|
+
* @param row One row of {@link SqliteStore}'s table, as SQLite returns it.
|
|
83
|
+
* @returns The record that row stands for.
|
|
84
|
+
*/
|
|
11
85
|
private static toRecord;
|
|
86
|
+
/**
|
|
87
|
+
* One record, or null when there is none under that name.
|
|
88
|
+
*
|
|
89
|
+
* The record carries the sealed value, not the plaintext: use
|
|
90
|
+
* `Vault.open` to get a value back out.
|
|
91
|
+
*
|
|
92
|
+
* @param owner Whose entry to look for.
|
|
93
|
+
* @param name The entry's name.
|
|
94
|
+
* @returns The stored record, or null when that owner has nothing under
|
|
95
|
+
* that name.
|
|
96
|
+
*/
|
|
12
97
|
get(owner: string, name: string): Promise<SecretRecord | null>;
|
|
98
|
+
/**
|
|
99
|
+
* Every record one owner holds, in whatever order SQLite returns them —
|
|
100
|
+
* the vault sorts what it shows.
|
|
101
|
+
*
|
|
102
|
+
* @param owner Whose entries to return.
|
|
103
|
+
* @returns That owner's records, empty when they hold none.
|
|
104
|
+
*/
|
|
13
105
|
list(owner: string): Promise<SecretRecord[]>;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Every record, whoever owns it.
|
|
108
|
+
*
|
|
109
|
+
* Only `Vault.rekey` and `Vault.purgeExpired` need this;
|
|
110
|
+
* everything else stays inside one owner. It loads the whole table, so it
|
|
111
|
+
* is the one method that costs more as the vault grows.
|
|
112
|
+
*
|
|
113
|
+
* @returns Every record in the table.
|
|
114
|
+
*/
|
|
115
|
+
all(): Promise<SecretRecord[]>;
|
|
116
|
+
/**
|
|
117
|
+
* Writes a record, replacing any under the same owner and name.
|
|
118
|
+
*
|
|
119
|
+
* `created_at` is deliberately left out of the conflict update: a
|
|
120
|
+
* replacement keeps the moment the entry first appeared. Nothing here
|
|
121
|
+
* enforces finality or expiry — those are the vault's rules, and the store
|
|
122
|
+
* writes what it is given.
|
|
123
|
+
*
|
|
124
|
+
* @param record The record to write. `owner` and `name` are its key.
|
|
125
|
+
* @returns The record as it reads back out of SQLite, which is what
|
|
126
|
+
* callers should keep — dates have been through epoch milliseconds and
|
|
127
|
+
* JSON columns through a round trip.
|
|
128
|
+
* @throws Error if the row cannot be read back immediately after writing
|
|
129
|
+
* it, which means the database is not behaving as SQLite.
|
|
130
|
+
*/
|
|
131
|
+
put(record: SecretRecord): Promise<SecretRecord>;
|
|
132
|
+
/**
|
|
133
|
+
* Deletes a record, returning false when there was nothing to delete.
|
|
134
|
+
*
|
|
135
|
+
* History goes with it: the row holds the previous values, so deleting an
|
|
136
|
+
* entry deletes every version of it.
|
|
137
|
+
*
|
|
138
|
+
* @param owner Whose entry to delete.
|
|
139
|
+
* @param name The entry's name.
|
|
140
|
+
* @returns True when a record was there and is now gone, false when there
|
|
141
|
+
* was nothing under that name.
|
|
142
|
+
*/
|
|
20
143
|
remove(owner: string, name: string): Promise<boolean>;
|
|
21
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Closes the underlying database.
|
|
146
|
+
*
|
|
147
|
+
* This closes a `Database` you passed to the constructor as well — the
|
|
148
|
+
* store does not track who opened it, so anything else using that same
|
|
149
|
+
* handle stops working too. Call it on the owner of the connection only.
|
|
150
|
+
*
|
|
151
|
+
* @returns Nothing; the store is unusable afterwards.
|
|
152
|
+
*/
|
|
22
153
|
close(): void;
|
|
23
154
|
}
|
|
24
155
|
//# sourceMappingURL=sqlite.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/stores/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/stores/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,OAAO,KAAK,EAAgB,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAgCtE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,WAAY,YAAW,UAAU;IAC1C,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAU;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;gBACS,QAAQ,GAAE,MAAM,GAAG,QAAqB,EAAE,KAAK,SAAkB;IA2B7E;;;;;;;;;OASG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ;IA2BvB;;;;;;;;;;OAUG;IACG,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IASpE;;;;;;OAMG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAOlD;;;;;;;;OAQG;IACG,GAAG,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAOpC;;;;;;;;;;;;;;OAcG;IACG,GAAG,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IA2CtD;;;;;;;;;;OAUG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAO3D;;;;;;;;OAQG;IACH,KAAK,IAAI,IAAI;CAGhB"}
|
package/dist/stores/sqlite.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/stores/sqlite.ts
|
|
3
3
|
import { Database } from "bun:sqlite";
|
|
4
|
+
var LATER_COLUMNS = [
|
|
5
|
+
["sealed_key", "TEXT"],
|
|
6
|
+
["plain", "TEXT"],
|
|
7
|
+
["is_sealed", "INTEGER NOT NULL DEFAULT 1"],
|
|
8
|
+
["is_final", "INTEGER NOT NULL DEFAULT 0"],
|
|
9
|
+
["expires_at", "INTEGER"],
|
|
10
|
+
["history", "TEXT"],
|
|
11
|
+
["rotation", "TEXT"],
|
|
12
|
+
["rotated_at", "INTEGER"],
|
|
13
|
+
["metadata", "TEXT"]
|
|
14
|
+
];
|
|
4
15
|
|
|
5
16
|
class SqliteStore {
|
|
6
17
|
db;
|
|
@@ -12,21 +23,31 @@ class SqliteStore {
|
|
|
12
23
|
owner TEXT NOT NULL,
|
|
13
24
|
name TEXT NOT NULL,
|
|
14
25
|
sealed TEXT NOT NULL,
|
|
15
|
-
metadata TEXT,
|
|
16
26
|
created_at INTEGER NOT NULL,
|
|
17
27
|
updated_at INTEGER NOT NULL,
|
|
18
28
|
PRIMARY KEY (owner, name)
|
|
19
29
|
)`);
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
const present = new Set(this.db.query(`PRAGMA table_info(${this.table})`).all().map((column) => column.name));
|
|
31
|
+
for (const [column, type] of LATER_COLUMNS) {
|
|
32
|
+
if (!present.has(column)) {
|
|
33
|
+
this.db.run(`ALTER TABLE ${this.table} ADD COLUMN ${column} ${type}`);
|
|
34
|
+
}
|
|
23
35
|
}
|
|
24
36
|
}
|
|
25
37
|
static toRecord(row) {
|
|
38
|
+
const history = row.history ? JSON.parse(row.history).map((entry) => ({ ...entry, createdAt: new Date(entry.createdAt) })) : [];
|
|
26
39
|
return {
|
|
27
40
|
owner: row.owner,
|
|
28
41
|
name: row.name,
|
|
29
42
|
sealed: row.sealed,
|
|
43
|
+
sealedKey: row.sealed_key,
|
|
44
|
+
plain: row.plain,
|
|
45
|
+
isSealed: row.is_sealed !== 0,
|
|
46
|
+
isFinal: row.is_final !== 0,
|
|
47
|
+
expiresAt: row.expires_at === null ? null : new Date(row.expires_at),
|
|
48
|
+
history,
|
|
49
|
+
rotation: row.rotation ? JSON.parse(row.rotation) : null,
|
|
50
|
+
rotatedAt: row.rotated_at === null ? null : new Date(row.rotated_at),
|
|
30
51
|
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
31
52
|
createdAt: new Date(row.created_at),
|
|
32
53
|
updatedAt: new Date(row.updated_at)
|
|
@@ -39,16 +60,30 @@ class SqliteStore {
|
|
|
39
60
|
async list(owner) {
|
|
40
61
|
return this.db.query(`SELECT * FROM ${this.table} WHERE owner = ?`).all(owner).map(SqliteStore.toRecord);
|
|
41
62
|
}
|
|
63
|
+
async all() {
|
|
64
|
+
return this.db.query(`SELECT * FROM ${this.table}`).all().map(SqliteStore.toRecord);
|
|
65
|
+
}
|
|
42
66
|
async put(record) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
this.db.query(`INSERT INTO ${this.table}
|
|
68
|
+
(owner, name, sealed, sealed_key, plain, is_sealed, is_final,
|
|
69
|
+
expires_at, history, rotation, rotated_at, metadata,
|
|
70
|
+
created_at, updated_at)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
72
|
+
ON CONFLICT(owner, name) DO UPDATE SET
|
|
73
|
+
sealed = excluded.sealed,
|
|
74
|
+
sealed_key = excluded.sealed_key,
|
|
75
|
+
plain = excluded.plain,
|
|
76
|
+
is_sealed = excluded.is_sealed,
|
|
77
|
+
is_final = excluded.is_final,
|
|
78
|
+
expires_at = excluded.expires_at,
|
|
79
|
+
history = excluded.history,
|
|
80
|
+
rotation = excluded.rotation,
|
|
81
|
+
rotated_at = excluded.rotated_at,
|
|
82
|
+
metadata = excluded.metadata,
|
|
83
|
+
updated_at = excluded.updated_at`).run(record.owner, record.name, record.sealed, record.sealedKey, record.plain, record.isSealed ? 1 : 0, record.isFinal ? 1 : 0, record.expiresAt?.getTime() ?? null, JSON.stringify(record.history), record.rotation ? JSON.stringify(record.rotation) : null, record.rotatedAt?.getTime() ?? null, JSON.stringify(record.metadata), record.createdAt.getTime(), record.updatedAt.getTime());
|
|
49
84
|
const stored = await this.get(record.owner, record.name);
|
|
50
85
|
if (!stored)
|
|
51
|
-
throw new Error("The
|
|
86
|
+
throw new Error("The record vanished immediately after writing it.");
|
|
52
87
|
return stored;
|
|
53
88
|
}
|
|
54
89
|
async remove(owner, name) {
|
|
@@ -66,5 +101,5 @@ export {
|
|
|
66
101
|
SqliteStore
|
|
67
102
|
};
|
|
68
103
|
|
|
69
|
-
//# debugId=
|
|
104
|
+
//# debugId=850E2DEC0A204F1364756E2164756E21
|
|
70
105
|
//# sourceMappingURL=sqlite.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/stores/sqlite.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { Database } from \"bun:sqlite\"\nimport type { SecretRecord, VaultStore } from \"../types\"\n\ntype Row = {\n owner: string\n name: string\n sealed: string\n metadata: string | null\n created_at: number\n updated_at: number\n}\n\n/**\n * Keeps sealed values in SQLite through `bun:sqlite`. Pass a file path,\n * \":memory:\", or a Database you already opened.\n */\nexport class SqliteStore implements VaultStore {\n private readonly db: Database\n private readonly table: string\n\n constructor(database: string | Database = \":memory:\", table = \"vault_secrets\") {\n this.db = typeof database === \"string\" ? new Database(database) : database\n this.table = table\n this.db.run(\n `CREATE TABLE IF NOT EXISTS ${this.table} (\n owner TEXT NOT NULL,\n name TEXT NOT NULL,\n sealed TEXT NOT NULL,\n metadata TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n PRIMARY KEY (owner, name)\n )`\n )\n // A table written by an earlier version has no metadata column.\n const columns = this.db\n .query<{ name: string }, []>(`PRAGMA table_info(${this.table})`)\n .all()\n if (!columns.some((column) => column.name === \"metadata\")) {\n this.db.run(`ALTER TABLE ${this.table} ADD COLUMN metadata TEXT`)\n }\n }\n\n private static toRecord(row: Row): SecretRecord {\n return {\n owner: row.owner,\n name: row.name,\n sealed: row.sealed,\n metadata: row.metadata ? (JSON.parse(row.metadata) as Record<string, string>) : {},\n createdAt: new Date(row.created_at),\n updatedAt: new Date(row.updated_at),\n }\n }\n\n async get(owner: string, name: string): Promise<SecretRecord | null> {\n const row = this.db\n .query<Row, [string, string]>(\n `SELECT * FROM ${this.table} WHERE owner = ? AND name = ?`\n )\n .get(owner, name)\n return row ? SqliteStore.toRecord(row) : null\n }\n\n async list(owner: string): Promise<SecretRecord[]> {\n return this.db\n .query<Row, [string]>(`SELECT * FROM ${this.table} WHERE owner = ?`)\n .all(owner)\n .map(SqliteStore.toRecord)\n }\n\n async put(record: {\n owner: string\n name: string\n sealed: string\n metadata: Record<string, string>\n }): Promise<SecretRecord> {\n const now = Date.now()\n // Replacing a value keeps the row's original created_at.\n this.db\n .query(\n `INSERT INTO ${this.table} (owner, name, sealed, metadata, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(owner, name)\n DO UPDATE SET sealed = excluded.sealed, metadata = excluded.metadata,\n updated_at = excluded.updated_at`\n )\n .run(\n record.owner,\n record.name,\n record.sealed,\n JSON.stringify(record.metadata),\n now,\n now\n )\n\n const stored = await this.get(record.owner, record.name)\n if (!stored) throw new Error(\"The secret vanished immediately after writing it.\")\n return stored\n }\n\n async remove(owner: string, name: string): Promise<boolean> {\n const existing = await this.get(owner, name)\n if (!existing) return false\n this.db\n .query(`DELETE FROM ${this.table} WHERE owner = ? AND name = ?`)\n .run(owner, name)\n return true\n }\n\n /** Closes the underlying database. */\n close(): void {\n this.db.close()\n }\n}\n"
|
|
5
|
+
"import { Database } from \"bun:sqlite\"\nimport type { HistoryEntry, SecretRecord, VaultStore } from \"../types\"\n\ntype Row = {\n owner: string\n name: string\n sealed: string\n sealed_key: string | null\n plain: string | null\n is_sealed: number\n is_final: number\n expires_at: number | null\n history: string | null\n rotation: string | null\n rotated_at: number | null\n metadata: string | null\n created_at: number\n updated_at: number\n}\n\n/** Columns added after 0.1.0, applied to tables that predate them. */\nconst LATER_COLUMNS: [string, string][] = [\n [\"sealed_key\", \"TEXT\"],\n [\"plain\", \"TEXT\"],\n [\"is_sealed\", \"INTEGER NOT NULL DEFAULT 1\"],\n [\"is_final\", \"INTEGER NOT NULL DEFAULT 0\"],\n [\"expires_at\", \"INTEGER\"],\n [\"history\", \"TEXT\"],\n [\"rotation\", \"TEXT\"],\n [\"rotated_at\", \"INTEGER\"],\n [\"metadata\", \"TEXT\"],\n]\n\n/**\n * Keeps records in SQLite through `bun:sqlite`. Pass a file path, \":memory:\",\n * or a Database you already opened.\n *\n * Values are sealed, but owners, names and metadata are ordinary columns —\n * anyone who can read the file learns what you keep, if not what it says. Use\n * `FileStore` when even that should be hidden.\n *\n * @remarks\n * `bun:sqlite` makes this store Bun-only: importing it on Node fails at the\n * import, not at the first query. That is why it is reached through\n * `@mstone6969/vault/stores/sqlite` instead of the package's main entry —\n * a Node program that imports the main entry never loads this file, and never\n * sees the Bun-only import.\n *\n * Every method is `async` only to satisfy {@link VaultStore}; `bun:sqlite` is\n * synchronous, so nothing here waits on I/O.\n *\n * @example\n * ```ts\n * import { Vault } from \"@mstone6969/vault\"\n * import { SqliteStore } from \"@mstone6969/vault/stores/sqlite\"\n *\n * const store = new SqliteStore(\"./vault.sqlite\")\n * const vault = new Vault({ key: process.env.VAULT_KEY!, store })\n *\n * await vault.put(\"alice\", \"db-password\", \"hunter2\", {\n * metadata: { kind: \"postgres\" },\n * })\n * console.log(await vault.open(\"alice\", \"db-password\")) // \"hunter2\"\n * store.close()\n * ```\n *\n * @see {@link VaultStore} for what a store owes the vault.\n */\nexport class SqliteStore implements VaultStore {\n private readonly db: Database\n private readonly table: string\n\n /**\n * Opens the database if it was given a path, and makes sure the table is\n * there and current.\n *\n * The table is created in its 0.1.0 shape if it is missing, and then every\n * column added since — `sealed_key`, `plain`, `is_sealed`, `is_final`,\n * `expires_at`, `history`, `rotation`, `rotated_at`, `metadata` — is\n * ALTERed in if it is absent. So a table written by an older version of\n * the package is upgraded the moment a newer one opens it, in place and\n * without a migration step of your own, and rows that predate a column\n * read back with sensible defaults: no history, no metadata, sealed, not\n * final, no expiry.\n *\n * @param database A file path to open, \":memory:\" for a database that\n * lasts as long as the process, or a `Database` you already opened —\n * useful for keeping the vault's table beside your own in one file, and\n * inside your own transactions.\n * @defaultValue `\":memory:\"`\n * @param table Which table to keep records in. Give it a name of its own\n * when one database holds several vaults.\n * @defaultValue `\"vault_secrets\"`\n * @throws Whatever `bun:sqlite` throws when the path cannot be opened, or\n * when `table` already exists with an incompatible shape.\n *\n * @example Sharing a database you already opened\n * ```ts\n * import { Database } from \"bun:sqlite\"\n * import { SqliteStore } from \"@mstone6969/vault/stores/sqlite\"\n *\n * const db = new Database(\"./app.sqlite\")\n * const store = new SqliteStore(db, \"shared_secrets\")\n * ```\n */\n constructor(database: string | Database = \":memory:\", table = \"vault_secrets\") {\n this.db = typeof database === \"string\" ? new Database(database) : database\n this.table = table\n this.db.run(\n `CREATE TABLE IF NOT EXISTS ${this.table} (\n owner TEXT NOT NULL,\n name TEXT NOT NULL,\n sealed TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n PRIMARY KEY (owner, name)\n )`\n )\n\n const present = new Set(\n this.db\n .query<{ name: string }, []>(`PRAGMA table_info(${this.table})`)\n .all()\n .map((column) => column.name)\n )\n for (const [column, type] of LATER_COLUMNS) {\n if (!present.has(column)) {\n this.db.run(`ALTER TABLE ${this.table} ADD COLUMN ${column} ${type}`)\n }\n }\n }\n\n /**\n * Turns a row back into a record: JSON columns parsed, epoch milliseconds\n * back into `Date`, integers back into booleans.\n *\n * Every column added after 0.1.0 is read defensively, because a row\n * written before that column existed reads back as null.\n *\n * @param row One row of {@link SqliteStore}'s table, as SQLite returns it.\n * @returns The record that row stands for.\n */\n private static toRecord(row: Row): SecretRecord {\n const history: HistoryEntry[] = row.history\n ? (JSON.parse(row.history) as { sealed: string; sealedKey: string | null; createdAt: string }[]).map(\n (entry) => ({ ...entry, createdAt: new Date(entry.createdAt) })\n )\n : []\n\n return {\n owner: row.owner,\n name: row.name,\n sealed: row.sealed,\n sealedKey: row.sealed_key,\n plain: row.plain,\n isSealed: row.is_sealed !== 0,\n isFinal: row.is_final !== 0,\n expiresAt: row.expires_at === null ? null : new Date(row.expires_at),\n history,\n rotation: row.rotation\n ? (JSON.parse(row.rotation) as SecretRecord[\"rotation\"])\n : null,\n rotatedAt: row.rotated_at === null ? null : new Date(row.rotated_at),\n metadata: row.metadata ? (JSON.parse(row.metadata) as Record<string, string>) : {},\n createdAt: new Date(row.created_at),\n updatedAt: new Date(row.updated_at),\n }\n }\n\n /**\n * One record, or null when there is none under that name.\n *\n * The record carries the sealed value, not the plaintext: use\n * `Vault.open` to get a value back out.\n *\n * @param owner Whose entry to look for.\n * @param name The entry's name.\n * @returns The stored record, or null when that owner has nothing under\n * that name.\n */\n async get(owner: string, name: string): Promise<SecretRecord | null> {\n const row = this.db\n .query<Row, [string, string]>(\n `SELECT * FROM ${this.table} WHERE owner = ? AND name = ?`\n )\n .get(owner, name)\n return row ? SqliteStore.toRecord(row) : null\n }\n\n /**\n * Every record one owner holds, in whatever order SQLite returns them —\n * the vault sorts what it shows.\n *\n * @param owner Whose entries to return.\n * @returns That owner's records, empty when they hold none.\n */\n async list(owner: string): Promise<SecretRecord[]> {\n return this.db\n .query<Row, [string]>(`SELECT * FROM ${this.table} WHERE owner = ?`)\n .all(owner)\n .map(SqliteStore.toRecord)\n }\n\n /**\n * Every record, whoever owns it.\n *\n * Only `Vault.rekey` and `Vault.purgeExpired` need this;\n * everything else stays inside one owner. It loads the whole table, so it\n * is the one method that costs more as the vault grows.\n *\n * @returns Every record in the table.\n */\n async all(): Promise<SecretRecord[]> {\n return this.db\n .query<Row, []>(`SELECT * FROM ${this.table}`)\n .all()\n .map(SqliteStore.toRecord)\n }\n\n /**\n * Writes a record, replacing any under the same owner and name.\n *\n * `created_at` is deliberately left out of the conflict update: a\n * replacement keeps the moment the entry first appeared. Nothing here\n * enforces finality or expiry — those are the vault's rules, and the store\n * writes what it is given.\n *\n * @param record The record to write. `owner` and `name` are its key.\n * @returns The record as it reads back out of SQLite, which is what\n * callers should keep — dates have been through epoch milliseconds and\n * JSON columns through a round trip.\n * @throws Error if the row cannot be read back immediately after writing\n * it, which means the database is not behaving as SQLite.\n */\n async put(record: SecretRecord): Promise<SecretRecord> {\n this.db\n .query(\n `INSERT INTO ${this.table}\n (owner, name, sealed, sealed_key, plain, is_sealed, is_final,\n expires_at, history, rotation, rotated_at, metadata,\n created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(owner, name) DO UPDATE SET\n sealed = excluded.sealed,\n sealed_key = excluded.sealed_key,\n plain = excluded.plain,\n is_sealed = excluded.is_sealed,\n is_final = excluded.is_final,\n expires_at = excluded.expires_at,\n history = excluded.history,\n rotation = excluded.rotation,\n rotated_at = excluded.rotated_at,\n metadata = excluded.metadata,\n updated_at = excluded.updated_at`\n )\n .run(\n record.owner,\n record.name,\n record.sealed,\n record.sealedKey,\n record.plain,\n record.isSealed ? 1 : 0,\n record.isFinal ? 1 : 0,\n record.expiresAt?.getTime() ?? null,\n JSON.stringify(record.history),\n record.rotation ? JSON.stringify(record.rotation) : null,\n record.rotatedAt?.getTime() ?? null,\n JSON.stringify(record.metadata),\n record.createdAt.getTime(),\n record.updatedAt.getTime()\n )\n\n const stored = await this.get(record.owner, record.name)\n if (!stored) throw new Error(\"The record vanished immediately after writing it.\")\n return stored\n }\n\n /**\n * Deletes a record, returning false when there was nothing to delete.\n *\n * History goes with it: the row holds the previous values, so deleting an\n * entry deletes every version of it.\n *\n * @param owner Whose entry to delete.\n * @param name The entry's name.\n * @returns True when a record was there and is now gone, false when there\n * was nothing under that name.\n */\n async remove(owner: string, name: string): Promise<boolean> {\n const existing = await this.get(owner, name)\n if (!existing) return false\n this.db.query(`DELETE FROM ${this.table} WHERE owner = ? AND name = ?`).run(owner, name)\n return true\n }\n\n /**\n * Closes the underlying database.\n *\n * This closes a `Database` you passed to the constructor as well — the\n * store does not track who opened it, so anything else using that same\n * handle stops working too. Call it on the owner of the connection only.\n *\n * @returns Nothing; the store is unusable afterwards.\n */\n close(): void {\n this.db.close()\n }\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;AAAA;AAAA;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "850E2DEC0A204F1364756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|