@andrewheberle/serverless-ssh-ca 0.19.1 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +60 -3
  2. package/dist/db/index.d.ts.map +1 -1
  3. package/dist/db/index.js +3 -4
  4. package/dist/db/index.js.map +1 -1
  5. package/dist/db/migrate.d.ts +41 -0
  6. package/dist/db/migrate.d.ts.map +1 -0
  7. package/dist/db/migrate.js +84 -0
  8. package/dist/db/migrate.js.map +1 -0
  9. package/dist/db/migrations/0001_initial_schema.d.ts +1 -1
  10. package/dist/db/migrations/0001_initial_schema.d.ts.map +1 -1
  11. package/dist/db/migrations/0001_initial_schema.js +9 -10
  12. package/dist/db/migrations/0001_initial_schema.js.map +1 -1
  13. package/dist/db/migrations/index.d.ts +1 -1
  14. package/dist/db/migrations/index.d.ts.map +1 -1
  15. package/dist/index.d.ts +28 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +17 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/node/env.d.ts +72 -0
  20. package/dist/node/env.d.ts.map +1 -0
  21. package/dist/node/env.js +74 -0
  22. package/dist/node/env.js.map +1 -0
  23. package/dist/node/index.d.ts +23 -0
  24. package/dist/node/index.d.ts.map +1 -0
  25. package/dist/node/index.js +22 -0
  26. package/dist/node/index.js.map +1 -0
  27. package/dist/node/secret.d.ts +24 -0
  28. package/dist/node/secret.d.ts.map +1 -0
  29. package/dist/node/secret.js +28 -0
  30. package/dist/node/secret.js.map +1 -0
  31. package/dist/node/sqlite.d.ts +47 -0
  32. package/dist/node/sqlite.d.ts.map +1 -0
  33. package/dist/node/sqlite.js +273 -0
  34. package/dist/node/sqlite.js.map +1 -0
  35. package/dist/types.d.ts +47 -3
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +6 -2
  38. package/src/db/index.ts +3 -4
  39. package/src/db/migrate.ts +107 -0
  40. package/src/db/migrations/0001_initial_schema.ts +10 -11
  41. package/src/db/migrations/index.ts +1 -1
  42. package/src/index.ts +37 -1
  43. package/src/node/env.ts +90 -0
  44. package/src/node/index.ts +22 -0
  45. package/src/node/secret.ts +30 -0
  46. package/src/node/sqlite.ts +315 -0
  47. package/src/types.ts +50 -3
@@ -0,0 +1,24 @@
1
+ import type { CaSecret } from "../types.js";
2
+ /**
3
+ * Use a string as the CA's private key secret.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const PRIVATE_KEY = secretFromString(process.env.SSH_CA_PRIVATE_KEY ?? "")
8
+ * ```
9
+ *
10
+ * @param value The CA private key in OpenSSH format
11
+ * @returns A secret for the `PRIVATE_KEY` binding, see {@link CaSecret}
12
+ */
13
+ export declare const secretFromString: (value: string) => CaSecret;
14
+ /**
15
+ * Use the contents of a file as the CA's private key secret.
16
+ *
17
+ * The file is read each time the key is needed, so replacing the file takes
18
+ * effect without a restart.
19
+ *
20
+ * @param path The path of a file containing the CA private key in OpenSSH format
21
+ * @returns A secret for the `PRIVATE_KEY` binding, see {@link CaSecret}
22
+ */
23
+ export declare const secretFromFile: (path: string) => CaSecret;
24
+ //# sourceMappingURL=secret.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../../src/node/secret.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,UAAW,MAAM,KAAG,QAE/C,CAAA;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,SAAU,MAAM,KAAG,QAE5C,CAAA"}
@@ -0,0 +1,28 @@
1
+ import { readFile } from "node:fs/promises";
2
+ /**
3
+ * Use a string as the CA's private key secret.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const PRIVATE_KEY = secretFromString(process.env.SSH_CA_PRIVATE_KEY ?? "")
8
+ * ```
9
+ *
10
+ * @param value The CA private key in OpenSSH format
11
+ * @returns A secret for the `PRIVATE_KEY` binding, see {@link CaSecret}
12
+ */
13
+ export const secretFromString = (value) => ({
14
+ get: async () => value,
15
+ });
16
+ /**
17
+ * Use the contents of a file as the CA's private key secret.
18
+ *
19
+ * The file is read each time the key is needed, so replacing the file takes
20
+ * effect without a restart.
21
+ *
22
+ * @param path The path of a file containing the CA private key in OpenSSH format
23
+ * @returns A secret for the `PRIVATE_KEY` binding, see {@link CaSecret}
24
+ */
25
+ export const secretFromFile = (path) => ({
26
+ get: async () => await readFile(path, { encoding: "utf-8" }),
27
+ });
28
+ //# sourceMappingURL=secret.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret.js","sourceRoot":"","sources":["../../src/node/secret.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAG3C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAAa,EAAY,EAAE,CAAC,CAAC;IAC7D,GAAG,EAAE,KAAK,IAAqB,EAAE,CAAC,KAAK;CACvC,CAAC,CAAA;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,IAAY,EAAY,EAAE,CAAC,CAAC;IAC1D,GAAG,EAAE,KAAK,IAAqB,EAAE,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;CAC7E,CAAC,CAAA"}
@@ -0,0 +1,47 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ import type { CaDatabase } from "../types.js";
3
+ /**
4
+ * A single SQL statement split from a larger query by {@link splitStatements}.
5
+ */
6
+ export type SqlStatement = {
7
+ /** The SQL of the statement, without its trailing `;` */
8
+ sql: string;
9
+ /** The number of anonymous (`?`) parameters in the statement */
10
+ parameters: number;
11
+ /** Whether the statement uses numbered or named parameters (`?1`, `:name`, `@name` or `$name`) */
12
+ named: boolean;
13
+ };
14
+ /**
15
+ * Split SQL that may contain several statements into individual statements.
16
+ *
17
+ * `node:sqlite` only compiles the first statement passed to `prepare()`,
18
+ * whereas Cloudflare D1 runs them all, so queries are split here first. Quoted
19
+ * strings and identifiers, comments and `CREATE TRIGGER ... BEGIN ... END`
20
+ * bodies are not split. Statements containing nothing but whitespace or
21
+ * comments are dropped.
22
+ *
23
+ * @param sql The SQL to split
24
+ * @returns The statements found in `sql`, see {@link SqlStatement}
25
+ */
26
+ export declare const splitStatements: (sql: string) => SqlStatement[];
27
+ /**
28
+ * Use a Node.js `node:sqlite` database as the CA's database.
29
+ *
30
+ * The returned object behaves like a Cloudflare D1 binding: queries containing
31
+ * several statements are run in full (see {@link splitStatements}) and
32
+ * {@link CaDatabase.batch} is atomic. The database's schema is created and
33
+ * migrated automatically when the CA first uses it.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { DatabaseSync } from "node:sqlite"
38
+ * import { fromNodeSqlite } from "@andrewheberle/serverless-ssh-ca/node"
39
+ *
40
+ * const DB = fromNodeSqlite(new DatabaseSync("ssh-ca.sqlite"))
41
+ * ```
42
+ *
43
+ * @param db An open `node:sqlite` database
44
+ * @returns A database for the `DB` binding, see {@link CaDatabase}
45
+ */
46
+ export declare const fromNodeSqlite: (db: DatabaseSync) => CaDatabase;
47
+ //# sourceMappingURL=sqlite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/node/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,aAAa,CAAA;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAgE,MAAM,aAAa,CAAA;AAE3G;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG;IAC1B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAA;IACX,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAA;IAClB,kGAAkG;IAClG,KAAK,EAAE,OAAO,CAAA;CACd,CAAA;AAID;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,QAAS,MAAM,KAAG,YAAY,EA6HzD,CAAA;AA6HD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,cAAc,OAAQ,YAAY,KAAG,UAgBhD,CAAA"}
@@ -0,0 +1,273 @@
1
+ const isWordChar = (ch) => ch !== undefined && /[A-Za-z0-9_$]/.test(ch);
2
+ /**
3
+ * Split SQL that may contain several statements into individual statements.
4
+ *
5
+ * `node:sqlite` only compiles the first statement passed to `prepare()`,
6
+ * whereas Cloudflare D1 runs them all, so queries are split here first. Quoted
7
+ * strings and identifiers, comments and `CREATE TRIGGER ... BEGIN ... END`
8
+ * bodies are not split. Statements containing nothing but whitespace or
9
+ * comments are dropped.
10
+ *
11
+ * @param sql The SQL to split
12
+ * @returns The statements found in `sql`, see {@link SqlStatement}
13
+ */
14
+ export const splitStatements = (sql) => {
15
+ const statements = [];
16
+ let start = 0;
17
+ let parameters = 0;
18
+ let named = false;
19
+ let hasTokens = false;
20
+ let words = [];
21
+ let depth = 0;
22
+ const push = (end) => {
23
+ if (hasTokens) {
24
+ statements.push({ sql: sql.slice(start, end).trim(), parameters, named });
25
+ }
26
+ start = end + 1;
27
+ parameters = 0;
28
+ named = false;
29
+ hasTokens = false;
30
+ words = [];
31
+ depth = 0;
32
+ };
33
+ // true if the current statement is CREATE [TEMP|TEMPORARY] TRIGGER, whose body may contain ";"
34
+ const inTrigger = () => words[0] === "CREATE" && (words[1] === "TRIGGER" || words[2] === "TRIGGER");
35
+ let i = 0;
36
+ while (i < sql.length) {
37
+ const ch = sql[i];
38
+ const next = sql[i + 1];
39
+ // comments
40
+ if (ch === "-" && next === "-") {
41
+ const end = sql.indexOf("\n", i);
42
+ i = end === -1 ? sql.length : end + 1;
43
+ continue;
44
+ }
45
+ if (ch === "/" && next === "*") {
46
+ const end = sql.indexOf("*/", i + 2);
47
+ i = end === -1 ? sql.length : end + 2;
48
+ continue;
49
+ }
50
+ // whitespace
51
+ if (ch === undefined || /\s/.test(ch)) {
52
+ i++;
53
+ continue;
54
+ }
55
+ // end of statement
56
+ if (ch === ";" && depth === 0) {
57
+ push(i);
58
+ i++;
59
+ continue;
60
+ }
61
+ hasTokens = true;
62
+ // quoted strings and identifiers, where a doubled quote is an escaped quote
63
+ if (ch === "'" || ch === "\"" || ch === "`" || ch === "[") {
64
+ const close = ch === "[" ? "]" : ch;
65
+ let j = i + 1;
66
+ while (j < sql.length) {
67
+ if (sql[j] === close) {
68
+ if (close !== "]" && sql[j + 1] === close) {
69
+ j += 2;
70
+ continue;
71
+ }
72
+ break;
73
+ }
74
+ j++;
75
+ }
76
+ i = j + 1;
77
+ continue;
78
+ }
79
+ // parameters
80
+ if (ch === "?") {
81
+ let j = i + 1;
82
+ while (j < sql.length && /[0-9]/.test(sql[j] ?? "")) {
83
+ j++;
84
+ }
85
+ if (j === i + 1) {
86
+ parameters++;
87
+ }
88
+ else {
89
+ named = true;
90
+ }
91
+ i = j;
92
+ continue;
93
+ }
94
+ if ((ch === ":" || ch === "@" || ch === "$") && isWordChar(next)) {
95
+ named = true;
96
+ i++;
97
+ while (isWordChar(sql[i])) {
98
+ i++;
99
+ }
100
+ continue;
101
+ }
102
+ // keywords and identifiers
103
+ if (isWordChar(ch)) {
104
+ let j = i;
105
+ while (isWordChar(sql[j])) {
106
+ j++;
107
+ }
108
+ const word = sql.slice(i, j).toUpperCase();
109
+ if (words.length < 3) {
110
+ words.push(word);
111
+ }
112
+ if (inTrigger()) {
113
+ if (word === "BEGIN" || word === "CASE") {
114
+ depth++;
115
+ }
116
+ else if (word === "END" && depth > 0) {
117
+ depth--;
118
+ }
119
+ }
120
+ i = j;
121
+ continue;
122
+ }
123
+ i++;
124
+ }
125
+ push(sql.length);
126
+ return statements;
127
+ };
128
+ /**
129
+ * Convert a value bound to a statement into one `node:sqlite` accepts, using
130
+ * the same conversions as Cloudflare D1.
131
+ */
132
+ const toSqlValue = (value) => {
133
+ if (value === null || typeof value === "number" || typeof value === "bigint" || typeof value === "string") {
134
+ return value;
135
+ }
136
+ if (typeof value === "boolean") {
137
+ return value ? 1 : 0;
138
+ }
139
+ if (value instanceof ArrayBuffer) {
140
+ return new Uint8Array(value);
141
+ }
142
+ if (ArrayBuffer.isView(value)) {
143
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
144
+ }
145
+ throw new TypeError(`unsupported type for SQL parameter: ${value === undefined ? "undefined" : typeof value}`);
146
+ };
147
+ const toNumber = (value) => {
148
+ if (typeof value === "number") {
149
+ return value;
150
+ }
151
+ if (typeof value === "bigint") {
152
+ return Number(value);
153
+ }
154
+ throw new TypeError(`expected a number from SQLite but got ${typeof value}`);
155
+ };
156
+ // counters used to report D1 style metadata for any statement
157
+ const counters = (db) => {
158
+ const row = db.prepare("SELECT total_changes() AS changes, last_insert_rowid() AS last_row_id").get();
159
+ return { changes: toNumber(row?.changes), lastRowId: toNumber(row?.last_row_id) };
160
+ };
161
+ let savepointId = 0;
162
+ // run fn atomically, using a savepoint so it also works inside an existing transaction
163
+ const atomically = (db, fn) => {
164
+ const name = `serverless_ssh_ca_${savepointId++}`;
165
+ db.exec(`SAVEPOINT ${name}`);
166
+ try {
167
+ const result = fn();
168
+ db.exec(`RELEASE ${name}`);
169
+ return result;
170
+ }
171
+ catch (err) {
172
+ db.exec(`ROLLBACK TO ${name}`);
173
+ db.exec(`RELEASE ${name}`);
174
+ throw err;
175
+ }
176
+ };
177
+ class NodeSqliteStatement {
178
+ db;
179
+ sql;
180
+ values;
181
+ constructor(db, sql, values = []) {
182
+ this.db = db;
183
+ this.sql = sql;
184
+ this.values = values;
185
+ }
186
+ bind(...values) {
187
+ return new NodeSqliteStatement(this.db, this.sql, values.map(toSqlValue));
188
+ }
189
+ async all() {
190
+ return this.execute();
191
+ }
192
+ async run() {
193
+ const { success, meta } = this.execute();
194
+ return { success, meta };
195
+ }
196
+ // executes synchronously so no other query can run part way through
197
+ execute() {
198
+ const started = performance.now();
199
+ const before = counters(this.db);
200
+ const results = this.query();
201
+ const after = counters(this.db);
202
+ return {
203
+ success: true,
204
+ results,
205
+ meta: {
206
+ changes: after.changes - before.changes,
207
+ last_row_id: after.lastRowId,
208
+ duration: performance.now() - started,
209
+ },
210
+ };
211
+ }
212
+ query() {
213
+ const statements = splitStatements(this.sql);
214
+ if (statements.length <= 1) {
215
+ return this.db.prepare(this.sql).all(...this.values);
216
+ }
217
+ if (statements.some(s => s.named)) {
218
+ throw new Error("queries containing more than one statement only support anonymous (?) parameters");
219
+ }
220
+ const expected = statements.reduce((total, s) => total + s.parameters, 0);
221
+ if (expected !== this.values.length) {
222
+ throw new RangeError(`query expects ${expected} parameters but ${this.values.length} were bound`);
223
+ }
224
+ // give each statement its share of the bound values, returning the rows of the last statement
225
+ return atomically(this.db, () => {
226
+ let offset = 0;
227
+ let results = [];
228
+ for (const statement of statements) {
229
+ const values = this.values.slice(offset, offset + statement.parameters);
230
+ offset += statement.parameters;
231
+ results = this.db.prepare(statement.sql).all(...values);
232
+ }
233
+ return results;
234
+ });
235
+ }
236
+ }
237
+ /**
238
+ * Use a Node.js `node:sqlite` database as the CA's database.
239
+ *
240
+ * The returned object behaves like a Cloudflare D1 binding: queries containing
241
+ * several statements are run in full (see {@link splitStatements}) and
242
+ * {@link CaDatabase.batch} is atomic. The database's schema is created and
243
+ * migrated automatically when the CA first uses it.
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * import { DatabaseSync } from "node:sqlite"
248
+ * import { fromNodeSqlite } from "@andrewheberle/serverless-ssh-ca/node"
249
+ *
250
+ * const DB = fromNodeSqlite(new DatabaseSync("ssh-ca.sqlite"))
251
+ * ```
252
+ *
253
+ * @param db An open `node:sqlite` database
254
+ * @returns A database for the `DB` binding, see {@link CaDatabase}
255
+ */
256
+ export const fromNodeSqlite = (db) => ({
257
+ prepare: (query) => new NodeSqliteStatement(db, query),
258
+ batch: async (statements) => {
259
+ const owned = statements.map(s => {
260
+ if (!(s instanceof NodeSqliteStatement) || s.db !== db) {
261
+ throw new TypeError("batch statements must be prepared by the same database");
262
+ }
263
+ return s;
264
+ });
265
+ return atomically(db, () => owned.map(s => s.execute()));
266
+ },
267
+ exec: async (query) => {
268
+ const started = performance.now();
269
+ db.exec(query);
270
+ return { count: splitStatements(query).length, duration: performance.now() - started };
271
+ },
272
+ });
273
+ //# sourceMappingURL=sqlite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/node/sqlite.ts"],"names":[],"mappings":"AAeA,MAAM,UAAU,GAAG,CAAC,EAAsB,EAAW,EAAE,CAAC,EAAE,KAAK,SAAS,IAAI,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAEpG;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,GAAW,EAAkB,EAAE;IAC9D,MAAM,UAAU,GAAmB,EAAE,CAAA;IAErC,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,UAAU,GAAG,CAAC,CAAA;IAClB,IAAI,KAAK,GAAG,KAAK,CAAA;IACjB,IAAI,SAAS,GAAG,KAAK,CAAA;IACrB,IAAI,KAAK,GAAa,EAAE,CAAA;IACxB,IAAI,KAAK,GAAG,CAAC,CAAA;IAEb,MAAM,IAAI,GAAG,CAAC,GAAW,EAAQ,EAAE;QAClC,IAAI,SAAS,EAAE,CAAC;YACf,UAAU,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;QAC1E,CAAC;QACD,KAAK,GAAG,GAAG,GAAG,CAAC,CAAA;QACf,UAAU,GAAG,CAAC,CAAA;QACd,KAAK,GAAG,KAAK,CAAA;QACb,SAAS,GAAG,KAAK,CAAA;QACjB,KAAK,GAAG,EAAE,CAAA;QACV,KAAK,GAAG,CAAC,CAAA;IACV,CAAC,CAAA;IAED,+FAA+F;IAC/F,MAAM,SAAS,GAAG,GAAY,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAA;IAE5G,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;QACjB,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QAEvB,WAAW;QACX,IAAI,EAAE,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;YAChC,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAA;YACrC,SAAQ;QACT,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;YACpC,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAA;YACrC,SAAQ;QACT,CAAC;QAED,aAAa;QACb,IAAI,EAAE,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YACvC,CAAC,EAAE,CAAA;YACH,SAAQ;QACT,CAAC;QAED,mBAAmB;QACnB,IAAI,EAAE,KAAK,GAAG,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,CAAC,CAAC,CAAA;YACP,CAAC,EAAE,CAAA;YACH,SAAQ;QACT,CAAC;QAED,SAAS,GAAG,IAAI,CAAA;QAEhB,4EAA4E;QAC5E,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YAC3D,MAAM,KAAK,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;YACnC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACb,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;gBACvB,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;oBACtB,IAAI,KAAK,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;wBAC3C,CAAC,IAAI,CAAC,CAAA;wBACN,SAAQ;oBACT,CAAC;oBACD,MAAK;gBACN,CAAC;gBACD,CAAC,EAAE,CAAA;YACJ,CAAC;YACD,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACT,SAAQ;QACT,CAAC;QAED,aAAa;QACb,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACb,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;gBACrD,CAAC,EAAE,CAAA;YACJ,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBACjB,UAAU,EAAE,CAAA;YACb,CAAC;iBAAM,CAAC;gBACP,KAAK,GAAG,IAAI,CAAA;YACb,CAAC;YACD,CAAC,GAAG,CAAC,CAAA;YACL,SAAQ;QACT,CAAC;QACD,IAAI,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAClE,KAAK,GAAG,IAAI,CAAA;YACZ,CAAC,EAAE,CAAA;YACH,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC3B,CAAC,EAAE,CAAA;YACJ,CAAC;YACD,SAAQ;QACT,CAAC;QAED,2BAA2B;QAC3B,IAAI,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC;YACpB,IAAI,CAAC,GAAG,CAAC,CAAA;YACT,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC3B,CAAC,EAAE,CAAA;YACJ,CAAC;YACD,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;YAC1C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACjB,CAAC;YACD,IAAI,SAAS,EAAE,EAAE,CAAC;gBACjB,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;oBACzC,KAAK,EAAE,CAAA;gBACR,CAAC;qBAAM,IAAI,IAAI,KAAK,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;oBACxC,KAAK,EAAE,CAAA;gBACR,CAAC;YACF,CAAC;YACD,CAAC,GAAG,CAAC,CAAA;YACL,SAAQ;QACT,CAAC;QAED,CAAC,EAAE,CAAA;IACJ,CAAC;IAED,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAEhB,OAAO,UAAU,CAAA;AAClB,CAAC,CAAA;AAED;;;GAGG;AACH,MAAM,UAAU,GAAG,CAAC,KAAc,EAAiB,EAAE;IACpD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC3G,OAAO,KAAK,CAAA;IACb,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACrB,CAAC;IACD,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;QAClC,OAAO,IAAI,UAAU,CAAC,KAAK,CAAC,CAAA;IAC7B,CAAC;IACD,IAAI,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,IAAI,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;IACxE,CAAC;IACD,MAAM,IAAI,SAAS,CAAC,uCAAuC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,KAAK,EAAE,CAAC,CAAA;AAC/G,CAAC,CAAA;AAED,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAU,EAAE;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAA;IACb,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IACrB,CAAC;IACD,MAAM,IAAI,SAAS,CAAC,yCAAyC,OAAO,KAAK,EAAE,CAAC,CAAA;AAC7E,CAAC,CAAA;AAED,8DAA8D;AAC9D,MAAM,QAAQ,GAAG,CAAC,EAAgB,EAA0C,EAAE;IAC7E,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,uEAAuE,CAAC,CAAC,GAAG,EAAE,CAAA;IACrG,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,CAAA;AAClF,CAAC,CAAA;AAED,IAAI,WAAW,GAAG,CAAC,CAAA;AAEnB,uFAAuF;AACvF,MAAM,UAAU,GAAG,CAAI,EAAgB,EAAE,EAAW,EAAK,EAAE;IAC1D,MAAM,IAAI,GAAG,qBAAqB,WAAW,EAAE,EAAE,CAAA;IACjD,EAAE,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAA;IAC5B,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,EAAE,EAAE,CAAA;QACnB,EAAE,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAA;QAC1B,OAAO,MAAM,CAAA;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,EAAE,CAAC,IAAI,CAAC,eAAe,IAAI,EAAE,CAAC,CAAA;QAC9B,EAAE,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAA;QAC1B,MAAM,GAAG,CAAA;IACV,CAAC;AACF,CAAC,CAAA;AAED,MAAM,mBAAmB;IACf,EAAE,CAAc;IACR,GAAG,CAAQ;IACX,MAAM,CAA0B;IAEjD,YAAY,EAAgB,EAAE,GAAW,EAAE,MAAM,GAA6B,EAAE;QAC/E,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;QACZ,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACrB,CAAC;IAED,IAAI,CAAC,GAAG,MAAiB;QACxB,OAAO,IAAI,mBAAmB,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAA;IAC1E,CAAC;IAED,KAAK,CAAC,GAAG;QACR,OAAO,IAAI,CAAC,OAAO,EAAE,CAAA;IACtB,CAAC;IAED,KAAK,CAAC,GAAG;QACR,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,CAAA;QACxC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;IACzB,CAAC;IAED,oEAAoE;IACpE,OAAO;QACN,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;QACjC,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,EAAE,CAAA;QAC5B,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAE/B,OAAO;YACN,OAAO,EAAE,IAAI;YACb,OAAO;YACP,IAAI,EAAE;gBACL,OAAO,EAAE,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO;gBACvC,WAAW,EAAE,KAAK,CAAC,SAAS;gBAC5B,QAAQ,EAAE,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO;aACrC;SACD,CAAA;IACF,CAAC;IAEO,KAAK;QACZ,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC5C,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YAC5B,OAAO,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAA;QACrD,CAAC;QAED,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,kFAAkF,CAAC,CAAA;QACpG,CAAC;QACD,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAA;QACzE,IAAI,QAAQ,KAAK,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACrC,MAAM,IAAI,UAAU,CAAC,iBAAiB,QAAQ,mBAAmB,IAAI,CAAC,MAAM,CAAC,MAAM,aAAa,CAAC,CAAA;QAClG,CAAC;QAED,8FAA8F;QAC9F,OAAO,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE;YAC/B,IAAI,MAAM,GAAG,CAAC,CAAA;YACd,IAAI,OAAO,GAAc,EAAE,CAAA;YAC3B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;gBACpC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,UAAU,CAAC,CAAA;gBACvE,MAAM,IAAI,SAAS,CAAC,UAAU,CAAA;gBAC9B,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAA;YACxD,CAAC;YACD,OAAO,OAAO,CAAA;QACf,CAAC,CAAC,CAAA;IACH,CAAC;CACD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,EAAgB,EAAc,EAAE,CAAC,CAAC;IAChE,OAAO,EAAE,CAAC,KAAa,EAAuB,EAAE,CAAC,IAAI,mBAAmB,CAAC,EAAE,EAAE,KAAK,CAAC;IACnF,KAAK,EAAE,KAAK,EAAE,UAAiC,EAAoC,EAAE;QACpF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;YAChC,IAAI,CAAC,CAAC,CAAC,YAAY,mBAAmB,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;gBACxD,MAAM,IAAI,SAAS,CAAC,wDAAwD,CAAC,CAAA;YAC9E,CAAC;YACD,OAAO,CAAC,CAAA;QACT,CAAC,CAAC,CAAA;QACF,OAAO,UAAU,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IACzD,CAAC;IACD,IAAI,EAAE,KAAK,EAAE,KAAa,EAAgD,EAAE;QAC3E,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;QACjC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACd,OAAO,EAAE,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,CAAA;IACvF,CAAC;CACD,CAAC,CAAA"}
package/dist/types.d.ts CHANGED
@@ -19,15 +19,59 @@ export type SSHExtension = {
19
19
  name: string;
20
20
  data: Buffer<ArrayBuffer>;
21
21
  };
22
+ /**
23
+ * The result of executing a statement via {@link CaPreparedStatement.run} or
24
+ * {@link CaDatabase.batch}, modelled on a Cloudflare D1 result.
25
+ */
26
+ export interface CaDatabaseResult {
27
+ /** Whether the statement executed successfully */
28
+ success: boolean;
29
+ meta?: {
30
+ /** The number of rows changed by the statement */
31
+ changes?: number;
32
+ /** The rowid of the last row inserted */
33
+ last_row_id?: number;
34
+ /** How long the statement took to execute in milliseconds */
35
+ duration?: number;
36
+ };
37
+ }
38
+ /**
39
+ * The result of executing a statement that returns rows via
40
+ * {@link CaPreparedStatement.all}.
41
+ */
42
+ export interface CaDatabaseQueryResult extends CaDatabaseResult {
43
+ /** The rows returned, one object per row keyed by column name */
44
+ results: unknown[];
45
+ }
46
+ /**
47
+ * The subset of a Cloudflare D1 prepared statement used by the CA.
48
+ */
49
+ export interface CaPreparedStatement {
50
+ /** Bind positional (`?`) parameters, returning a statement to execute */
51
+ bind(...values: unknown[]): CaPreparedStatement;
52
+ /** Execute the statement and return all rows */
53
+ all(): Promise<CaDatabaseQueryResult>;
54
+ /** Execute the statement without returning rows */
55
+ run(): Promise<CaDatabaseResult>;
56
+ }
22
57
  /**
23
58
  * The subset of a Cloudflare D1 database binding used by the CA.
24
59
  *
25
60
  * A Workers `D1Database` binding satisfies this structurally, so the package
26
- * does not need to depend on `@cloudflare/workers-types`.
61
+ * does not need to depend on `@cloudflare/workers-types`. Any other SQLite
62
+ * compatible database may be used by implementing this interface.
63
+ *
64
+ * The SQL the CA runs is SQLite dialect, and statements passed to
65
+ * {@link CaDatabase.prepare} may contain more than one SQL statement.
66
+ * Schema migrations rely on {@link CaDatabase.batch} being atomic so they can
67
+ * be applied safely by concurrent requests.
27
68
  */
28
69
  export interface CaDatabase {
29
- prepare(query: string): unknown;
30
- batch(statements: never[]): Promise<unknown>;
70
+ /** Prepare a SQL query for execution, see {@link CaPreparedStatement} */
71
+ prepare(query: string): CaPreparedStatement;
72
+ /** Execute several prepared statements atomically, returning a result for each */
73
+ batch(statements: CaPreparedStatement[]): Promise<CaDatabaseResult[]>;
74
+ /** Execute one or more SQL statements without parameters */
31
75
  exec(query: string): Promise<unknown>;
32
76
  }
33
77
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AAGjC,MAAM,MAAM,wBAAwB,GAAG;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACpC,WAAW,EAAE,MAAM,CAAA;CACtB,CAAA;AAGD,MAAM,MAAM,4BAA4B,GAAG;IACvC,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,CAAA;CACnC,GAAG,UAAU,CAAA;AAEd,MAAM,MAAM,YAAY,GAAG;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,WAAW,CAAC,CAAA;CAC5B,CAAA;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACvB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;IAC/B,KAAK,CAAC,UAAU,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5C,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACxC;AAED;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACrB,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,UAAU,CAAA;IACd,wBAAwB,EAAE,MAAM,CAAA;IAChC,WAAW,EAAE,QAAQ,CAAA;IACrB,0BAA0B,EAAE,MAAM,CAAA;IAClC,wBAAwB,EAAE,MAAM,CAAA;IAChC,4BAA4B,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC/C,kCAAkC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACrD,0BAA0B,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC9C,6BAA6B,EAAE,MAAM,CAAA;IACrC,mCAAmC,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACvD,kCAAkC,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACtD,SAAS,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACjC,oCAAoC,EAAE,MAAM,CAAA;IAC5C,iCAAiC,EAAE,MAAM,CAAA;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CAClB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@andrewheberle/serverless-ssh-ca",
3
- "version": "0.19.1",
4
- "description": "A serverless SSH CA built on Cloudflare Workers",
3
+ "version": "0.20.1",
4
+ "description": "A serverless SSH CA built for Cloudflare Workers that can also run on Node.js",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "files": [
@@ -14,6 +14,10 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "default": "./dist/index.js"
16
16
  },
17
+ "./node": {
18
+ "types": "./dist/node/index.d.ts",
19
+ "default": "./dist/node/index.js"
20
+ },
17
21
  "./openapi.json": "./openapi.json"
18
22
  },
19
23
  "scripts": {
package/src/db/index.ts CHANGED
@@ -2,6 +2,7 @@ import type { Certificate, Format, Identity } from "sshpk"
2
2
  import { logger } from "../logger.js"
3
3
  import type { SshCaBindings } from "../types.js"
4
4
  import { D1QB } from "workers-qb"
5
+ import { migrateOnce } from "./migrate.js"
5
6
  import { migrations } from "./migrations/index.js"
6
7
 
7
8
  export enum CertificateType {
@@ -32,13 +33,11 @@ const connect = async (env: SshCaBindings): Promise<D1QB<DatabaseSchema>> => {
32
33
  try {
33
34
  l.debug("connecting to database")
34
35
 
35
- const qb = new D1QB<DatabaseSchema>(env.DB)
36
- const migrationBuilder = qb.migrations({ migrations })
37
- const appliedMigrations = await migrationBuilder.apply()
36
+ const appliedMigrations = await migrateOnce(env.DB, migrations)
38
37
 
39
38
  l.debug("applied database migrations", "migrations", appliedMigrations.length, "migration_names", appliedMigrations.map(m => m.name))
40
39
 
41
- return qb
40
+ return new D1QB<DatabaseSchema>(env.DB)
42
41
  } catch (err) {
43
42
  l.error("error connecting to database", "error", err)
44
43
  throw err
@@ -0,0 +1,107 @@
1
+ import z from "zod"
2
+ import type { CaDatabase } from "../types.js"
3
+
4
+ /**
5
+ * A database schema migration.
6
+ *
7
+ * Migrations are applied in order and recorded by `name` in the `migrations`
8
+ * table, so a name must never change once released. Each statement must be a
9
+ * single SQL statement.
10
+ */
11
+ export type Migration = {
12
+ /** Unique name recorded once the migration has been applied */
13
+ name: string
14
+ /** The SQL statements to run, in order, as one transaction */
15
+ statements: string[]
16
+ }
17
+
18
+ // matches the table previously created by workers-qb so existing databases are not migrated again
19
+ const createMigrationsTable = `CREATE TABLE IF NOT EXISTS migrations (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ name TEXT UNIQUE,
22
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
23
+ )`
24
+
25
+ const MigrationRows = z.array(z.object({ name: z.string() }))
26
+
27
+ const appliedNames = async (db: CaDatabase): Promise<Set<string>> => {
28
+ const { results } = await db.prepare("SELECT name FROM migrations").all()
29
+ return new Set(MigrationRows.parse(results).map(row => row.name))
30
+ }
31
+
32
+ const isApplied = async (db: CaDatabase, name: string): Promise<boolean> => {
33
+ const { results } = await db.prepare("SELECT name FROM migrations WHERE name = ?").bind(name).all()
34
+ return results.length > 0
35
+ }
36
+
37
+ /**
38
+ * Apply any migrations that have not yet been applied to the database.
39
+ *
40
+ * Each migration is run with {@link CaDatabase.batch} so it applies atomically,
41
+ * and its row in the `migrations` table is inserted first. When several callers
42
+ * race to apply the same migration only one succeeds; the others fail on that
43
+ * insert before running any of the migration's statements and their batch rolls
44
+ * back, so migrations that are not idempotent are safe to apply concurrently.
45
+ *
46
+ * @param db The database to migrate, see {@link CaDatabase}
47
+ * @param migrations The migrations to apply in order, see {@link Migration}
48
+ * @returns The migrations applied by this call, excluding any applied concurrently by another caller
49
+ */
50
+ export const applyMigrations = async (db: CaDatabase, migrations: readonly Migration[]): Promise<Migration[]> => {
51
+ await db.prepare(createMigrationsTable).run()
52
+ const existing = await appliedNames(db)
53
+
54
+ const applied: Migration[] = []
55
+ for (const migration of migrations) {
56
+ if (existing.has(migration.name)) {
57
+ continue
58
+ }
59
+
60
+ try {
61
+ await db.batch([
62
+ db.prepare("INSERT INTO migrations (name) VALUES (?)").bind(migration.name),
63
+ ...migration.statements.map(sql => db.prepare(sql)),
64
+ ])
65
+ applied.push(migration)
66
+ } catch (err) {
67
+ // another caller applying the same migration is not an error
68
+ if (!(await isApplied(db, migration.name))) {
69
+ throw err
70
+ }
71
+ }
72
+ }
73
+
74
+ return applied
75
+ }
76
+
77
+ const migrated = new WeakMap<CaDatabase, Promise<Migration[]>>()
78
+
79
+ /**
80
+ * Apply migrations to a database at most once per database binding, see
81
+ * {@link applyMigrations}.
82
+ *
83
+ * Concurrent and later callers share the result of the first call, unless it
84
+ * fails, in which case the next call tries again.
85
+ *
86
+ * @param db The database to migrate, see {@link CaDatabase}
87
+ * @param migrations The migrations to apply in order, see {@link Migration}
88
+ * @returns The migrations applied, or an empty array if this database binding has already been migrated
89
+ */
90
+ export const migrateOnce = async (db: CaDatabase, migrations: readonly Migration[]): Promise<Migration[]> => {
91
+ const pending = migrated.get(db)
92
+ if (pending !== undefined) {
93
+ await pending
94
+ return []
95
+ }
96
+
97
+ const result = applyMigrations(db, migrations)
98
+ migrated.set(db, result)
99
+ try {
100
+ return await result
101
+ } catch (err) {
102
+ if (migrated.get(db) === result) {
103
+ migrated.delete(db)
104
+ }
105
+ throw err
106
+ }
107
+ }
@@ -1,9 +1,9 @@
1
- import { type Migration } from "workers-qb";
1
+ import type { Migration } from "../migrate.js"
2
2
 
3
3
  export const migration: Migration = {
4
4
  name: "0001_initial_schema",
5
- sql: `
6
- CREATE TABLE IF NOT EXISTS certificates (
5
+ statements: [
6
+ `CREATE TABLE IF NOT EXISTS certificates (
7
7
  id INTEGER PRIMARY KEY,
8
8
  serial TEXT UNIQUE NOT NULL,
9
9
  key_id TEXT NOT NULL,
@@ -14,12 +14,11 @@ export const migration: Migration = {
14
14
  revoked_at TIMESTAMP,
15
15
  certificate_type INTEGER NOT NULL DEFAULT 0,
16
16
  public_key TEXT
17
- );
18
-
19
- CREATE INDEX IF NOT EXISTS idx_certificates_serial ON certificates(serial);
20
- CREATE INDEX IF NOT EXISTS idx_certificates_valid_after ON certificates(valid_after);
21
- CREATE INDEX IF NOT EXISTS idx_certificates_valid_before ON certificates(valid_before);
22
- CREATE INDEX IF NOT EXISTS idx_certificates_revoked_at ON certificates(revoked_at);
23
- CREATE INDEX IF NOT EXISTS idx_certificates_certificate_type ON certificates(certificate_type);
24
- `
17
+ )`,
18
+ "CREATE INDEX IF NOT EXISTS idx_certificates_serial ON certificates(serial)",
19
+ "CREATE INDEX IF NOT EXISTS idx_certificates_valid_after ON certificates(valid_after)",
20
+ "CREATE INDEX IF NOT EXISTS idx_certificates_valid_before ON certificates(valid_before)",
21
+ "CREATE INDEX IF NOT EXISTS idx_certificates_revoked_at ON certificates(revoked_at)",
22
+ "CREATE INDEX IF NOT EXISTS idx_certificates_certificate_type ON certificates(certificate_type)",
23
+ ]
25
24
  }