@andrewheberle/serverless-ssh-ca 0.19.0 → 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.
- package/README.md +60 -3
- package/dist/index.d.ts +28 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/node/env.d.ts +72 -0
- package/dist/node/env.d.ts.map +1 -0
- package/dist/node/env.js +74 -0
- package/dist/node/env.js.map +1 -0
- package/dist/node/index.d.ts +23 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +22 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/secret.d.ts +24 -0
- package/dist/node/secret.d.ts.map +1 -0
- package/dist/node/secret.js +28 -0
- package/dist/node/secret.js.map +1 -0
- package/dist/node/sqlite.d.ts +47 -0
- package/dist/node/sqlite.d.ts.map +1 -0
- package/dist/node/sqlite.js +273 -0
- package/dist/node/sqlite.js.map +1 -0
- package/dist/types.d.ts +46 -3
- package/dist/types.d.ts.map +1 -1
- package/openapi.json +1391 -0
- package/package.json +11 -5
- package/src/index.ts +37 -1
- package/src/node/env.ts +90 -0
- package/src/node/index.ts +22 -0
- package/src/node/secret.ts +30 -0
- package/src/node/sqlite.ts +315 -0
- package/src/types.ts +49 -3
package/README.md
CHANGED
|
@@ -15,8 +15,9 @@ import app from "@andrewheberle/serverless-ssh-ca"
|
|
|
15
15
|
export default app
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
This package is built for Cloudflare Workers
|
|
19
|
-
|
|
18
|
+
This package is built for Cloudflare Workers and can also run on Node.js 24
|
|
19
|
+
(see [Running on Node.js](#running-on-nodejs)). These are the only runtimes it
|
|
20
|
+
is tested on.
|
|
20
21
|
|
|
21
22
|
The package doesn't use any Workers-specific APIs and doesn't depend on
|
|
22
23
|
`@cloudflare/workers-types`. It needs:
|
|
@@ -30,6 +31,62 @@ Its exported types describe only these needs, so the `Env` generated by
|
|
|
30
31
|
`wrangler types` (with `DB` as a `D1Database` and `PRIVATE_KEY` as a
|
|
31
32
|
`SecretsStoreSecret`) can be passed directly as `SshCaBindings`.
|
|
32
33
|
|
|
34
|
+
### Running on Node.js
|
|
35
|
+
|
|
36
|
+
Node.js 24 or later is required. Helpers for Node.js are imported from
|
|
37
|
+
`@andrewheberle/serverless-ssh-ca/node`:
|
|
38
|
+
|
|
39
|
+
| Helper | Purpose |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `fromNodeSqlite(db)` | Uses a built-in `node:sqlite` database as the `DB` binding |
|
|
42
|
+
| `secretFromFile(path)` | Reads the CA private key from a file (re-read each time it is needed) |
|
|
43
|
+
| `secretFromString(value)` | Uses a string as the CA private key |
|
|
44
|
+
| `bindingsFromEnv({ DB, PRIVATE_KEY })` | Reads and validates the remaining configuration from `process.env` |
|
|
45
|
+
|
|
46
|
+
`createSshCa(bindings)` returns a `fetch(request)` handler that takes a
|
|
47
|
+
standard `Request` and returns a `Response`, and a `cleanup()` task that
|
|
48
|
+
replaces the Workers cron trigger.
|
|
49
|
+
|
|
50
|
+
The handler can be served by any server that works with `Request` and
|
|
51
|
+
`Response`. For example, with [`@hono/node-server`](https://www.npmjs.com/package/@hono/node-server)
|
|
52
|
+
(installed separately with `npm install @hono/node-server`):
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { DatabaseSync } from "node:sqlite"
|
|
56
|
+
import { serve } from "@hono/node-server"
|
|
57
|
+
import { createSshCa } from "@andrewheberle/serverless-ssh-ca"
|
|
58
|
+
import { bindingsFromEnv, fromNodeSqlite, secretFromFile } from "@andrewheberle/serverless-ssh-ca/node"
|
|
59
|
+
|
|
60
|
+
const ca = createSshCa(bindingsFromEnv({
|
|
61
|
+
DB: fromNodeSqlite(new DatabaseSync("/var/lib/ssh-ca/ssh-ca.sqlite")),
|
|
62
|
+
PRIVATE_KEY: secretFromFile("/etc/ssh-ca/ca_key"),
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
// remove expired certificates once a day
|
|
66
|
+
setInterval(() => void ca.cleanup(), 24 * 60 * 60 * 1000)
|
|
67
|
+
|
|
68
|
+
serve({ fetch: (req) => ca.fetch(req), port: 8787 })
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Notes:
|
|
72
|
+
|
|
73
|
+
- The configuration environment variables have the same names and formats
|
|
74
|
+
as the `vars` in the Workers [`wrangler.jsonc`](https://github.com/andrewheberle/serverless-ssh-ca/blob/main/packages/workers-integration/wrangler.jsonc).
|
|
75
|
+
`bindingsFromEnv` validates them at start up and throws an
|
|
76
|
+
`InvalidConfigurationError` that lists every missing or invalid variable.
|
|
77
|
+
- The database schema is created and migrated automatically on first use, as
|
|
78
|
+
on Workers. Use a file path for `DatabaseSync` (not `:memory:`) so issued
|
|
79
|
+
and revoked certificates survive restarts.
|
|
80
|
+
- Only SQLite databases are supported. Other SQLite-compatible databases can
|
|
81
|
+
be used by implementing the `CaDatabase` interface.
|
|
82
|
+
- The CA private key has the same requirements as on Workers (see
|
|
83
|
+
[CA Private Key](#ca-private-key)). Keep the key file readable only by the
|
|
84
|
+
user running the CA.
|
|
85
|
+
- `cleanup()` can also be run on a schedule outside the process (for example
|
|
86
|
+
from a systemd timer or cron job) instead of using `setInterval`.
|
|
87
|
+
- Put the CA behind a reverse proxy that terminates TLS, as clients send
|
|
88
|
+
identity tokens to it.
|
|
89
|
+
|
|
33
90
|
See the following respositories for additional information:
|
|
34
91
|
|
|
35
92
|
|
|
@@ -193,7 +250,7 @@ must be an Ed25519 or ECDSA (`nistp256`, `nistp384` or `nistp521`) key:
|
|
|
193
250
|
ssh-keygen -t ed25519 -N "" -C "" -f ssh-ca
|
|
194
251
|
```
|
|
195
252
|
|
|
196
|
-
RSA CA keys are not supported
|
|
253
|
+
RSA CA keys are not supported. With an RSA CA key
|
|
197
254
|
configured, every request that uses the CA key (including `/api/v3/ca` and
|
|
198
255
|
the KRL endpoints) returns a `500` error, and the reason is logged:
|
|
199
256
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import type { ExecutionContext } from "hono";
|
|
2
2
|
import type { SshCaBindings } from "./types.js";
|
|
3
|
-
export type { CaDatabase, CaSecret, SshCaBindings } from "./types.js";
|
|
3
|
+
export type { CaDatabase, CaDatabaseQueryResult, CaDatabaseResult, CaPreparedStatement, CaSecret, SshCaBindings } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* A runtime neutral instance of the CA, returned by {@link createSshCa}.
|
|
6
|
+
*/
|
|
7
|
+
export interface SshCa {
|
|
8
|
+
/**
|
|
9
|
+
* Handle a request to the CA's API, suitable for any server that speaks
|
|
10
|
+
* the Fetch API `Request` and `Response` types.
|
|
11
|
+
*/
|
|
12
|
+
fetch(req: Request): Promise<Response>;
|
|
13
|
+
/**
|
|
14
|
+
* Remove expired certificates from the database according to
|
|
15
|
+
* `DB_CERTIFICATE_RETENTION`. This should be run periodically (for example
|
|
16
|
+
* once a day), as the `scheduled` handler does on Cloudflare Workers.
|
|
17
|
+
*/
|
|
18
|
+
cleanup(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create an instance of the CA bound to a fixed set of bindings, for use on
|
|
22
|
+
* runtimes other than Cloudflare Workers.
|
|
23
|
+
*
|
|
24
|
+
* On Workers use the default export instead, which receives its bindings from
|
|
25
|
+
* the runtime on each request.
|
|
26
|
+
*
|
|
27
|
+
* @param env The bindings (configuration, database and private key) for the CA, see {@link SshCaBindings}
|
|
28
|
+
* @returns The CA's request handler and database cleanup task, see {@link SshCa}
|
|
29
|
+
*/
|
|
30
|
+
export declare const createSshCa: (env: SshCaBindings) => SshCa;
|
|
4
31
|
declare const _default: {
|
|
5
32
|
fetch(req: Request, env: SshCaBindings, ctx: ExecutionContext): Promise<Response> | Response;
|
|
6
33
|
scheduled(controller: unknown, env: SshCaBindings, ctx: ExecutionContext): Promise<void>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,MAAM,CAAA;AAG5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC/C,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,MAAM,CAAA;AAG5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC/C,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAQnI;;GAEG;AACH,MAAM,WAAW,KAAK;IAClB;;;OAGG;IACH,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACtC;;;;OAIG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,QAAS,aAAa,KAAG,KAOhD,CAAA;;eAKc,OAAO,OAAO,aAAa,OAAO,gBAAgB;0BAMjC,OAAO,OAAO,aAAa,OAAO,gBAAgB"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { dbCleanup } from "./db/index.js";
|
|
2
2
|
import { createApp } from "./router.js";
|
|
3
|
+
/**
|
|
4
|
+
* Create an instance of the CA bound to a fixed set of bindings, for use on
|
|
5
|
+
* runtimes other than Cloudflare Workers.
|
|
6
|
+
*
|
|
7
|
+
* On Workers use the default export instead, which receives its bindings from
|
|
8
|
+
* the runtime on each request.
|
|
9
|
+
*
|
|
10
|
+
* @param env The bindings (configuration, database and private key) for the CA, see {@link SshCaBindings}
|
|
11
|
+
* @returns The CA's request handler and database cleanup task, see {@link SshCa}
|
|
12
|
+
*/
|
|
13
|
+
export const createSshCa = (env) => {
|
|
14
|
+
const app = createApp(env);
|
|
15
|
+
return {
|
|
16
|
+
fetch: async (req) => await app.fetch(req, env),
|
|
17
|
+
cleanup: async () => await dbCleanup(env),
|
|
18
|
+
};
|
|
19
|
+
};
|
|
3
20
|
let app;
|
|
4
21
|
export default {
|
|
5
22
|
fetch(req, env, ctx) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AA2BvC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,GAAkB,EAAS,EAAE;IACrD,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,CAAA;IAE1B,OAAO;QACH,KAAK,EAAE,KAAK,EAAE,GAAY,EAAqB,EAAE,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC;QAC3E,OAAO,EAAE,KAAK,IAAmB,EAAE,CAAC,MAAM,SAAS,CAAC,GAAG,CAAC;KAC3D,CAAA;AACL,CAAC,CAAA;AAED,IAAI,GAA6C,CAAA;AAEjD,eAAe;IACX,KAAK,CAAC,GAAY,EAAE,GAAkB,EAAE,GAAqB;QACzD,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACpB,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,CAAA;QACxB,CAAC;QACD,OAAO,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;IACnC,CAAC;IACD,KAAK,CAAC,SAAS,CAAC,UAAmB,EAAE,GAAkB,EAAE,GAAqB;QAC1E,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAA;IACjC,CAAC;CACmB,CAAA"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import type { CaDatabase, CaSecret, SshCaBindings } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The environment variables read by {@link bindingsFromEnv}, which match the
|
|
5
|
+
* Workers vars documented for the CA.
|
|
6
|
+
*/
|
|
7
|
+
export declare const EnvSchema: z.ZodObject<{
|
|
8
|
+
DB_CERTIFICATE_RETENTION: z.ZodUnion<readonly [z.ZodLiteral<"infinite">, z.ZodString]>;
|
|
9
|
+
SSH_CERTIFICATE_EXTENSIONS: z.ZodString;
|
|
10
|
+
SSH_CERTIFICATE_LIFETIME: z.ZodString;
|
|
11
|
+
SSH_CERTIFICATE_INCLUDE_SELF: z.ZodOptional<z.ZodEnum<{
|
|
12
|
+
false: "false";
|
|
13
|
+
true: "true";
|
|
14
|
+
}>>;
|
|
15
|
+
SSH_CERTIFICATE_INCLUDE_SELF_EMAIL: z.ZodOptional<z.ZodEnum<{
|
|
16
|
+
false: "false";
|
|
17
|
+
true: "true";
|
|
18
|
+
}>>;
|
|
19
|
+
SSH_CERTIFICATE_PRINCIPALS: z.ZodOptional<z.ZodString>;
|
|
20
|
+
SSH_HOST_CERTIFICATE_LIFETIME: z.ZodString;
|
|
21
|
+
SSH_HOST_CERTIFICATE_ALLOWED_EMAILS: z.ZodOptional<z.ZodString>;
|
|
22
|
+
SSH_HOST_CERTIFICATE_ALLOWED_ROLES: z.ZodOptional<z.ZodString>;
|
|
23
|
+
ISSUER_DN: z.ZodString;
|
|
24
|
+
JWT_JWKS_URL: z.ZodURL;
|
|
25
|
+
JWT_AUD: z.ZodOptional<z.ZodString>;
|
|
26
|
+
JWT_ISSUER: z.ZodString;
|
|
27
|
+
JWT_ALGORITHMS: z.ZodString;
|
|
28
|
+
JWT_SSH_CERTIFICATE_PRINCIPALS_CLAIM: z.ZodString;
|
|
29
|
+
CERTIFICATE_REQUEST_TIME_SKEW_MAX: z.ZodString;
|
|
30
|
+
LOG_LEVEL: z.ZodOptional<z.ZodEnum<{
|
|
31
|
+
debug: "debug";
|
|
32
|
+
error: "error";
|
|
33
|
+
info: "info";
|
|
34
|
+
none: "none";
|
|
35
|
+
warning: "warning";
|
|
36
|
+
}>>;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
/**
|
|
39
|
+
* The CA's configuration as read from environment variables, see {@link EnvSchema}.
|
|
40
|
+
*/
|
|
41
|
+
export type CaEnv = z.infer<typeof EnvSchema>;
|
|
42
|
+
/**
|
|
43
|
+
* Thrown by {@link bindingsFromEnv} when the environment is not a valid configuration.
|
|
44
|
+
*/
|
|
45
|
+
export declare class InvalidConfigurationError extends Error {
|
|
46
|
+
constructor(message: string);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build the CA's bindings from environment variables, for runtimes where
|
|
50
|
+
* configuration is not supplied as Workers bindings.
|
|
51
|
+
*
|
|
52
|
+
* The variables have the same names and formats as the Workers vars, and are
|
|
53
|
+
* validated before use so that misconfiguration is reported at start up.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const env = bindingsFromEnv({
|
|
58
|
+
* DB: fromNodeSqlite(new DatabaseSync("ssh-ca.sqlite")),
|
|
59
|
+
* PRIVATE_KEY: secretFromFile("/etc/ssh-ca/ca_key"),
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @param bindings The database and private key, see {@link CaDatabase} and {@link CaSecret}
|
|
64
|
+
* @param source The environment variables to read, `process.env` by default
|
|
65
|
+
* @returns The bindings to pass to `createSshCa`
|
|
66
|
+
* @throws {@link InvalidConfigurationError} if a variable is missing or invalid
|
|
67
|
+
*/
|
|
68
|
+
export declare const bindingsFromEnv: (bindings: {
|
|
69
|
+
DB: CaDatabase;
|
|
70
|
+
PRIVATE_KEY: CaSecret;
|
|
71
|
+
}, source?: Record<string, string | undefined>) => SshCaBindings;
|
|
72
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/node/env.ts"],"names":[],"mappings":"AACA,OAAO,CAAC,MAAM,KAAK,CAAA;AACnB,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAYtE;;;GAGG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsBpB,CAAA;AAEF;;GAEG;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAA;AAE7C;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IACnD,YAAY,OAAO,EAAE,MAAM,EAK1B;CACD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,aACjB;IAAE,EAAE,EAAE,UAAU,CAAC;IAAC,WAAW,EAAE,QAAQ,CAAA;CAAE,WAC3C,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KACxC,aAOF,CAAA"}
|
package/dist/node/env.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { seconds } from "itty-time";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
const required = z.string().trim().min(1, { message: "must be set" });
|
|
4
|
+
// a duration in the format accepted by itty-time, such as "24 hours"
|
|
5
|
+
const duration = (opts) => required.refine(v => {
|
|
6
|
+
const s = seconds(v);
|
|
7
|
+
return Number.isFinite(s) && (opts.allowZero ? s >= 0 : s > 0);
|
|
8
|
+
}, { message: opts.allowZero ? "must be a duration such as \"90 seconds\"" : "must be a positive duration such as \"24 hours\"" });
|
|
9
|
+
const boolean = z.enum(["true", "false"], { message: "must be \"true\" or \"false\"" });
|
|
10
|
+
/**
|
|
11
|
+
* The environment variables read by {@link bindingsFromEnv}, which match the
|
|
12
|
+
* Workers vars documented for the CA.
|
|
13
|
+
*/
|
|
14
|
+
export const EnvSchema = z.object({
|
|
15
|
+
DB_CERTIFICATE_RETENTION: z.union([
|
|
16
|
+
z.literal("infinite"),
|
|
17
|
+
// an SQLite date modifier such as "1 year" or "90 days"
|
|
18
|
+
z.string().regex(/^[+-]?\d+(\.\d+)? (second|minute|hour|day|month|year)s?$/),
|
|
19
|
+
], { message: "must be \"infinite\" or a period such as \"1 year\"" }),
|
|
20
|
+
SSH_CERTIFICATE_EXTENSIONS: z.string(),
|
|
21
|
+
SSH_CERTIFICATE_LIFETIME: duration({ allowZero: false }),
|
|
22
|
+
SSH_CERTIFICATE_INCLUDE_SELF: boolean.optional(),
|
|
23
|
+
SSH_CERTIFICATE_INCLUDE_SELF_EMAIL: boolean.optional(),
|
|
24
|
+
SSH_CERTIFICATE_PRINCIPALS: z.string().optional(),
|
|
25
|
+
SSH_HOST_CERTIFICATE_LIFETIME: duration({ allowZero: false }),
|
|
26
|
+
SSH_HOST_CERTIFICATE_ALLOWED_EMAILS: z.string().optional(),
|
|
27
|
+
SSH_HOST_CERTIFICATE_ALLOWED_ROLES: z.string().optional(),
|
|
28
|
+
ISSUER_DN: required,
|
|
29
|
+
JWT_JWKS_URL: z.url({ protocol: /^https?$/, message: "must be an http or https URL" }),
|
|
30
|
+
JWT_AUD: z.string().optional(),
|
|
31
|
+
JWT_ISSUER: required,
|
|
32
|
+
JWT_ALGORITHMS: required,
|
|
33
|
+
JWT_SSH_CERTIFICATE_PRINCIPALS_CLAIM: required,
|
|
34
|
+
CERTIFICATE_REQUEST_TIME_SKEW_MAX: duration({ allowZero: true }),
|
|
35
|
+
LOG_LEVEL: z.enum(["none", "error", "warning", "info", "debug"]).optional(),
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Thrown by {@link bindingsFromEnv} when the environment is not a valid configuration.
|
|
39
|
+
*/
|
|
40
|
+
export class InvalidConfigurationError extends Error {
|
|
41
|
+
constructor(message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "InvalidConfigurationError";
|
|
44
|
+
Object.setPrototypeOf(this, InvalidConfigurationError.prototype);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build the CA's bindings from environment variables, for runtimes where
|
|
49
|
+
* configuration is not supplied as Workers bindings.
|
|
50
|
+
*
|
|
51
|
+
* The variables have the same names and formats as the Workers vars, and are
|
|
52
|
+
* validated before use so that misconfiguration is reported at start up.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const env = bindingsFromEnv({
|
|
57
|
+
* DB: fromNodeSqlite(new DatabaseSync("ssh-ca.sqlite")),
|
|
58
|
+
* PRIVATE_KEY: secretFromFile("/etc/ssh-ca/ca_key"),
|
|
59
|
+
* })
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @param bindings The database and private key, see {@link CaDatabase} and {@link CaSecret}
|
|
63
|
+
* @param source The environment variables to read, `process.env` by default
|
|
64
|
+
* @returns The bindings to pass to `createSshCa`
|
|
65
|
+
* @throws {@link InvalidConfigurationError} if a variable is missing or invalid
|
|
66
|
+
*/
|
|
67
|
+
export const bindingsFromEnv = (bindings, source = process.env) => {
|
|
68
|
+
const result = EnvSchema.safeParse(source);
|
|
69
|
+
if (!result.success) {
|
|
70
|
+
throw new InvalidConfigurationError(`invalid configuration:\n${z.prettifyError(result.error)}`);
|
|
71
|
+
}
|
|
72
|
+
return { ...result.data, DB: bindings.DB, PRIVATE_KEY: bindings.PRIVATE_KEY };
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/node/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,CAAC,MAAM,KAAK,CAAA;AAGnB,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAA;AAErE,qEAAqE;AACrE,MAAM,QAAQ,GAAG,CAAC,IAA4B,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;IACtE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IACpB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;AAC/D,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,2CAA2C,CAAC,CAAC,CAAC,kDAAkD,EAAE,CAAC,CAAA;AAElI,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,+BAA+B,EAAE,CAAC,CAAA;AAEvF;;;GAGG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC;IACjC,wBAAwB,EAAE,CAAC,CAAC,KAAK,CAAC;QACjC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;QACrB,wDAAwD;QACxD,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,0DAA0D,CAAC;KAC5E,EAAE,EAAE,OAAO,EAAE,qDAAqD,EAAE,CAAC;IACtE,0BAA0B,EAAE,CAAC,CAAC,MAAM,EAAE;IACtC,wBAAwB,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IACxD,4BAA4B,EAAE,OAAO,CAAC,QAAQ,EAAE;IAChD,kCAAkC,EAAE,OAAO,CAAC,QAAQ,EAAE;IACtD,0BAA0B,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjD,6BAA6B,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAC7D,mCAAmC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC1D,kCAAkC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACzD,SAAS,EAAE,QAAQ;IACnB,YAAY,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,8BAA8B,EAAE,CAAC;IACtF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,UAAU,EAAE,QAAQ;IACpB,cAAc,EAAE,QAAQ;IACxB,oCAAoC,EAAE,QAAQ;IAC9C,iCAAiC,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAChE,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE;CAC3E,CAAC,CAAA;AAOF;;GAEG;AACH,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IACnD,YAAY,OAAe;QAC1B,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAA;QAEvC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,yBAAyB,CAAC,SAAS,CAAC,CAAA;IACjE,CAAC;CACD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAC9B,QAAmD,EACnD,MAAM,GAAuC,OAAO,CAAC,GAAG,EACxC,EAAE;IAClB,MAAM,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC1C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,IAAI,yBAAyB,CAAC,2BAA2B,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAChG,CAAC;IAED,OAAO,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAA;AAC9E,CAAC,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
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";
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAA;AAChF,YAAY,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -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 { secretFromFile, secretFromString } from "./secret.js";
|
|
21
|
+
export { fromNodeSqlite } from "./sqlite.js";
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAA;AAEhF,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -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"}
|