@andrewheberle/serverless-ssh-ca 0.19.1 → 0.20.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.
@@ -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":"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"}
package/dist/types.d.ts CHANGED
@@ -19,15 +19,58 @@ 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 (as used
66
+ * when applying schema migrations).
27
67
  */
28
68
  export interface CaDatabase {
29
- prepare(query: string): unknown;
30
- batch(statements: never[]): Promise<unknown>;
69
+ /** Prepare a SQL query for execution, see {@link CaPreparedStatement} */
70
+ prepare(query: string): CaPreparedStatement;
71
+ /** Execute several prepared statements atomically, returning a result for each */
72
+ batch(statements: CaPreparedStatement[]): Promise<CaDatabaseResult[]>;
73
+ /** Execute one or more SQL statements without parameters */
31
74
  exec(query: string): Promise<unknown>;
32
75
  }
33
76
  /**
@@ -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.0",
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/index.ts CHANGED
@@ -2,7 +2,7 @@ import type { ExecutionContext } from "hono"
2
2
  import { dbCleanup } from "./db/index.js"
3
3
  import { createApp } from "./router.js"
4
4
  import type { SshCaBindings } from "./types.js"
5
- export type { CaDatabase, CaSecret, SshCaBindings } from "./types.js"
5
+ export type { CaDatabase, CaDatabaseQueryResult, CaDatabaseResult, CaPreparedStatement, CaSecret, SshCaBindings } from "./types.js"
6
6
 
7
7
  // the handlers the CA exposes, compatible with a Cloudflare Workers ExportedHandler
8
8
  type SshCaHandler = {
@@ -10,6 +10,42 @@ type SshCaHandler = {
10
10
  scheduled(controller: unknown, env: SshCaBindings, ctx: ExecutionContext): Promise<void>
11
11
  }
12
12
 
13
+ /**
14
+ * A runtime neutral instance of the CA, returned by {@link createSshCa}.
15
+ */
16
+ export interface SshCa {
17
+ /**
18
+ * Handle a request to the CA's API, suitable for any server that speaks
19
+ * the Fetch API `Request` and `Response` types.
20
+ */
21
+ fetch(req: Request): Promise<Response>
22
+ /**
23
+ * Remove expired certificates from the database according to
24
+ * `DB_CERTIFICATE_RETENTION`. This should be run periodically (for example
25
+ * once a day), as the `scheduled` handler does on Cloudflare Workers.
26
+ */
27
+ cleanup(): Promise<void>
28
+ }
29
+
30
+ /**
31
+ * Create an instance of the CA bound to a fixed set of bindings, for use on
32
+ * runtimes other than Cloudflare Workers.
33
+ *
34
+ * On Workers use the default export instead, which receives its bindings from
35
+ * the runtime on each request.
36
+ *
37
+ * @param env The bindings (configuration, database and private key) for the CA, see {@link SshCaBindings}
38
+ * @returns The CA's request handler and database cleanup task, see {@link SshCa}
39
+ */
40
+ export const createSshCa = (env: SshCaBindings): SshCa => {
41
+ const app = createApp(env)
42
+
43
+ return {
44
+ fetch: async (req: Request): Promise<Response> => await app.fetch(req, env),
45
+ cleanup: async (): Promise<void> => await dbCleanup(env),
46
+ }
47
+ }
48
+
13
49
  let app: ReturnType<typeof createApp> | undefined
14
50
 
15
51
  export default {
@@ -0,0 +1,90 @@
1
+ import { seconds } from "itty-time"
2
+ import z from "zod"
3
+ import type { CaDatabase, CaSecret, SshCaBindings } from "../types.js"
4
+
5
+ const required = z.string().trim().min(1, { message: "must be set" })
6
+
7
+ // a duration in the format accepted by itty-time, such as "24 hours"
8
+ const duration = (opts: { allowZero: boolean }) => required.refine(v => {
9
+ const s = seconds(v)
10
+ return Number.isFinite(s) && (opts.allowZero ? s >= 0 : s > 0)
11
+ }, { message: opts.allowZero ? "must be a duration such as \"90 seconds\"" : "must be a positive duration such as \"24 hours\"" })
12
+
13
+ const boolean = z.enum(["true", "false"], { message: "must be \"true\" or \"false\"" })
14
+
15
+ /**
16
+ * The environment variables read by {@link bindingsFromEnv}, which match the
17
+ * Workers vars documented for the CA.
18
+ */
19
+ export const EnvSchema = z.object({
20
+ DB_CERTIFICATE_RETENTION: z.union([
21
+ z.literal("infinite"),
22
+ // an SQLite date modifier such as "1 year" or "90 days"
23
+ z.string().regex(/^[+-]?\d+(\.\d+)? (second|minute|hour|day|month|year)s?$/),
24
+ ], { message: "must be \"infinite\" or a period such as \"1 year\"" }),
25
+ SSH_CERTIFICATE_EXTENSIONS: z.string(),
26
+ SSH_CERTIFICATE_LIFETIME: duration({ allowZero: false }),
27
+ SSH_CERTIFICATE_INCLUDE_SELF: boolean.optional(),
28
+ SSH_CERTIFICATE_INCLUDE_SELF_EMAIL: boolean.optional(),
29
+ SSH_CERTIFICATE_PRINCIPALS: z.string().optional(),
30
+ SSH_HOST_CERTIFICATE_LIFETIME: duration({ allowZero: false }),
31
+ SSH_HOST_CERTIFICATE_ALLOWED_EMAILS: z.string().optional(),
32
+ SSH_HOST_CERTIFICATE_ALLOWED_ROLES: z.string().optional(),
33
+ ISSUER_DN: required,
34
+ JWT_JWKS_URL: z.url({ protocol: /^https?$/, message: "must be an http or https URL" }),
35
+ JWT_AUD: z.string().optional(),
36
+ JWT_ISSUER: required,
37
+ JWT_ALGORITHMS: required,
38
+ JWT_SSH_CERTIFICATE_PRINCIPALS_CLAIM: required,
39
+ CERTIFICATE_REQUEST_TIME_SKEW_MAX: duration({ allowZero: true }),
40
+ LOG_LEVEL: z.enum(["none", "error", "warning", "info", "debug"]).optional(),
41
+ })
42
+
43
+ /**
44
+ * The CA's configuration as read from environment variables, see {@link EnvSchema}.
45
+ */
46
+ export type CaEnv = z.infer<typeof EnvSchema>
47
+
48
+ /**
49
+ * Thrown by {@link bindingsFromEnv} when the environment is not a valid configuration.
50
+ */
51
+ export class InvalidConfigurationError extends Error {
52
+ constructor(message: string) {
53
+ super(message)
54
+ this.name = "InvalidConfigurationError"
55
+
56
+ Object.setPrototypeOf(this, InvalidConfigurationError.prototype)
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Build the CA's bindings from environment variables, for runtimes where
62
+ * configuration is not supplied as Workers bindings.
63
+ *
64
+ * The variables have the same names and formats as the Workers vars, and are
65
+ * validated before use so that misconfiguration is reported at start up.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const env = bindingsFromEnv({
70
+ * DB: fromNodeSqlite(new DatabaseSync("ssh-ca.sqlite")),
71
+ * PRIVATE_KEY: secretFromFile("/etc/ssh-ca/ca_key"),
72
+ * })
73
+ * ```
74
+ *
75
+ * @param bindings The database and private key, see {@link CaDatabase} and {@link CaSecret}
76
+ * @param source The environment variables to read, `process.env` by default
77
+ * @returns The bindings to pass to `createSshCa`
78
+ * @throws {@link InvalidConfigurationError} if a variable is missing or invalid
79
+ */
80
+ export const bindingsFromEnv = (
81
+ bindings: { DB: CaDatabase, PRIVATE_KEY: CaSecret },
82
+ source: Record<string, string | undefined> = process.env,
83
+ ): SshCaBindings => {
84
+ const result = EnvSchema.safeParse(source)
85
+ if (!result.success) {
86
+ throw new InvalidConfigurationError(`invalid configuration:\n${z.prettifyError(result.error)}`)
87
+ }
88
+
89
+ return { ...result.data, DB: bindings.DB, PRIVATE_KEY: bindings.PRIVATE_KEY }
90
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Helpers for running the CA on Node.js, imported from
3
+ * `@andrewheberle/serverless-ssh-ca/node`.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { DatabaseSync } from "node:sqlite"
8
+ * import { createSshCa } from "@andrewheberle/serverless-ssh-ca"
9
+ * import { bindingsFromEnv, fromNodeSqlite, secretFromFile } from "@andrewheberle/serverless-ssh-ca/node"
10
+ *
11
+ * const ca = createSshCa(bindingsFromEnv({
12
+ * DB: fromNodeSqlite(new DatabaseSync("ssh-ca.sqlite")),
13
+ * PRIVATE_KEY: secretFromFile("/etc/ssh-ca/ca_key"),
14
+ * }))
15
+ * ```
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ export { bindingsFromEnv, EnvSchema, InvalidConfigurationError } from "./env.js"
20
+ export type { CaEnv } from "./env.js"
21
+ export { secretFromFile, secretFromString } from "./secret.js"
22
+ export { fromNodeSqlite } from "./sqlite.js"
@@ -0,0 +1,30 @@
1
+ import { readFile } from "node:fs/promises"
2
+ import type { CaSecret } from "../types.js"
3
+
4
+ /**
5
+ * Use a string as the CA's private key secret.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const PRIVATE_KEY = secretFromString(process.env.SSH_CA_PRIVATE_KEY ?? "")
10
+ * ```
11
+ *
12
+ * @param value The CA private key in OpenSSH format
13
+ * @returns A secret for the `PRIVATE_KEY` binding, see {@link CaSecret}
14
+ */
15
+ export const secretFromString = (value: string): CaSecret => ({
16
+ get: async (): Promise<string> => value,
17
+ })
18
+
19
+ /**
20
+ * Use the contents of a file as the CA's private key secret.
21
+ *
22
+ * The file is read each time the key is needed, so replacing the file takes
23
+ * effect without a restart.
24
+ *
25
+ * @param path The path of a file containing the CA private key in OpenSSH format
26
+ * @returns A secret for the `PRIVATE_KEY` binding, see {@link CaSecret}
27
+ */
28
+ export const secretFromFile = (path: string): CaSecret => ({
29
+ get: async (): Promise<string> => await readFile(path, { encoding: "utf-8" }),
30
+ })