@andrewheberle/serverless-ssh-ca-testing 0.0.0-stage → 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.
package/README.md CHANGED
@@ -1,3 +1,85 @@
1
- # Temporary Holding Version
1
+ # @andrewheberle/serverless-ssh-ca-testing
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Runs the [Serverless SSH CA](../core/README.md) for end-to-end tests of
4
+ clients, such as [ssh-ca-client](https://github.com/andrewheberle/ssh-ca-client).
5
+
6
+ The CA can be run under:
7
+
8
+ - `node`: Node.js, using the package's Node.js helpers with an in-memory
9
+ `node:sqlite` database
10
+ - `workerd`: the Cloudflare Workers runtime, using the Wrangler test harness
11
+ with a local D1 database and Secrets Store, as `wrangler dev` would
12
+
13
+ Nothing is persisted between runs.
14
+
15
+ This package is released with `@andrewheberle/serverless-ssh-ca` and uses
16
+ whichever version of that package is installed alongside it. To test a client
17
+ against an unreleased version of the CA, install a tarball from `npm pack` in
18
+ its place.
19
+
20
+ ## Requirements
21
+
22
+ Node.js 24 or later.
23
+
24
+ ## Usage
25
+
26
+ ```sh
27
+ npm install --save-dev @andrewheberle/serverless-ssh-ca @andrewheberle/serverless-ssh-ca-testing
28
+ npx serverless-ssh-ca-test-server [--runtime node|workerd] config.json
29
+ ```
30
+
31
+ The runtime defaults to `node`. Clients that start the server from another
32
+ language can run `node node_modules/@andrewheberle/serverless-ssh-ca-testing/dist/cli.js`,
33
+ which avoids the platform-specific shims in `node_modules/.bin`.
34
+
35
+ The server exits when its standard input is closed, so it does not outlive
36
+ the test that started it. If it cannot start, it writes the reason to
37
+ standard error and exits with status 1.
38
+
39
+ ## Configuration
40
+
41
+ The configuration file is a JSON object:
42
+
43
+ | Property | Description |
44
+ |---------------|--------------------------------------------------------------------------|
45
+ | `private_key` | The CA private key in OpenSSH format |
46
+ | `bindings` | The CA configuration, as the Workers vars (environment variables) |
47
+ | `port_file` | Path the port is written to once the CA is listening on `127.0.0.1` |
48
+ | `log_file` | Path that log output from the CA and harness is appended to as JSON lines |
49
+
50
+ The bindings are those documented for the CA, with every value as a string.
51
+ Under `workerd`, Wrangler's working files are written to the directory
52
+ containing the configuration file.
53
+
54
+ ## Log file
55
+
56
+ Each line of the log file is a JSON object with at least `level` and
57
+ `message`. Lines logged by the CA are written as they are, and the harness adds
58
+ lines whose `message` starts with `harness`:
59
+
60
+ - `{"level":"info","message":"harness request","method":...,"url":...,"status":...}`
61
+ for every request, where `url` is the path and query. When `status` is 400
62
+ or more, `body` holds the response body, so a test can check why a request
63
+ was rejected.
64
+ - `{"level":"error","message":"harness ...",...}` for a fault, meaning the CA
65
+ or harness failed rather than rejected a request. Some faults, such as a
66
+ failure to record an issued certificate, are only visible in the log, so
67
+ tests should fail if any of these lines appear. A CA log line that
68
+ indicates a fault is followed by a line with the message `harness fault`
69
+ and the original line in `line`.
70
+
71
+ Under `workerd`, the CA's output is copied to the log file every 100ms, so a
72
+ request may be logged shortly after its response is received.
73
+
74
+ ## Programmatic use
75
+
76
+ ```ts
77
+ import { startTestServer } from "@andrewheberle/serverless-ssh-ca-testing"
78
+
79
+ const server = await startTestServer("node", "config.json")
80
+ // make requests to http://127.0.0.1:${server.port}
81
+ await server.close()
82
+ ```
83
+
84
+ Under `node` the CA's `console` output is redirected to the log file until the
85
+ server is closed.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ // Runs the Serverless SSH CA for end-to-end tests of clients.
3
+ //
4
+ // Usage: serverless-ssh-ca-test-server [--runtime node|workerd] <config.json>
5
+ //
6
+ // The process exits when its stdin is closed, so it does not outlive the test
7
+ // that started it.
8
+ import { parseArgs } from "node:util";
9
+ import z from "zod";
10
+ import { readConfig, RuntimeSchema } from "./config.js";
11
+ import { startTestServer } from "./index.js";
12
+ import { LogFile } from "./log.js";
13
+ const usage = "usage: serverless-ssh-ca-test-server [--runtime node|workerd] <config.json>";
14
+ const main = async () => {
15
+ const { values, positionals } = parseArgs({
16
+ options: {
17
+ runtime: { type: "string", default: "node" },
18
+ },
19
+ allowPositionals: true,
20
+ });
21
+ const runtime = RuntimeSchema.safeParse(values.runtime);
22
+ if (!runtime.success) {
23
+ throw new Error(`--runtime ${z.prettifyError(runtime.error)}\n${usage}`);
24
+ }
25
+ const [configFile, ...extra] = positionals;
26
+ if (configFile === undefined || extra.length > 0) {
27
+ throw new Error(usage);
28
+ }
29
+ // faults after start up are written to the log file, where the test
30
+ // reading it will find them
31
+ const config = await readConfig(configFile);
32
+ const log = new LogFile(config.log_file);
33
+ process.on("uncaughtException", (err) => {
34
+ log.fault("uncaught exception", err);
35
+ process.exit(1);
36
+ });
37
+ const server = await startTestServer(runtime.data, configFile);
38
+ process.stdin.on("end", () => {
39
+ server.close().then(() => process.exit(0), (err) => {
40
+ log.fault("close failed", err);
41
+ process.exit(1);
42
+ });
43
+ });
44
+ process.stdin.resume();
45
+ };
46
+ main().catch((err) => {
47
+ console.error(err instanceof Error ? err.message : err);
48
+ process.exit(1);
49
+ });
50
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,8DAA8D;AAC9D,EAAE;AACF,8EAA8E;AAC9E,EAAE;AACF,8EAA8E;AAC9E,mBAAmB;AACnB,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,CAAC,MAAM,KAAK,CAAA;AACnB,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAElC,MAAM,KAAK,GAAG,6EAA6E,CAAA;AAE3F,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE;IACtC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,CAAC;QACzC,OAAO,EAAE;YACR,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE;SAC5C;QACD,gBAAgB,EAAE,IAAI;KACtB,CAAC,CAAA;IAEF,MAAM,OAAO,GAAG,aAAa,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACvD,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,aAAa,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,EAAE,CAAC,CAAA;IACzE,CAAC;IAED,MAAM,CAAC,UAAU,EAAE,GAAG,KAAK,CAAC,GAAG,WAAW,CAAA;IAC1C,IAAI,UAAU,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,CAAA;IACvB,CAAC;IAED,oEAAoE;IACpE,4BAA4B;IAC5B,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,UAAU,CAAC,CAAA;IAC3C,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IACxC,OAAO,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,GAAG,EAAE,EAAE;QACvC,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAA;QACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC,CAAC,CAAA;IAEF,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IAE9D,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;QAC5B,MAAM,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,GAAY,EAAE,EAAE;YAC3D,GAAG,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAA;YAC9B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAChB,CAAC,CAAC,CAAA;IACH,CAAC,CAAC,CAAA;IACF,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,CAAA;AACvB,CAAC,CAAA;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC7B,OAAO,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACvD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAChB,CAAC,CAAC,CAAA"}
@@ -0,0 +1,37 @@
1
+ import z from "zod";
2
+ /**
3
+ * The runtimes the CA can be run under by {@link startTestServer}: `node` uses
4
+ * the package's Node.js helpers with an in-memory `node:sqlite` database, and
5
+ * `workerd` uses the Cloudflare Workers runtime with a local D1 database and
6
+ * Secrets Store.
7
+ */
8
+ export declare const RuntimeSchema: z.ZodEnum<{
9
+ node: "node";
10
+ workerd: "workerd";
11
+ }>;
12
+ /**
13
+ * A runtime the CA can be run under, see {@link RuntimeSchema}.
14
+ */
15
+ export type Runtime = z.infer<typeof RuntimeSchema>;
16
+ /**
17
+ * The configuration file read by {@link readConfig}.
18
+ */
19
+ export declare const ConfigSchema: z.ZodObject<{
20
+ private_key: z.ZodString;
21
+ bindings: z.ZodRecord<z.ZodString, z.ZodString>;
22
+ port_file: z.ZodString;
23
+ log_file: z.ZodString;
24
+ }, z.core.$strip>;
25
+ /**
26
+ * The configuration of a test server, see {@link ConfigSchema}.
27
+ */
28
+ export type TestServerConfig = z.infer<typeof ConfigSchema>;
29
+ /**
30
+ * Read and validate a test server configuration file.
31
+ *
32
+ * @param path The path of a JSON file matching {@link ConfigSchema}
33
+ * @returns The validated configuration
34
+ * @throws Error if the file cannot be read, is not JSON or is not a valid configuration
35
+ */
36
+ export declare const readConfig: (path: string) => Promise<TestServerConfig>;
37
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,CAAC,MAAM,KAAK,CAAA;AAEnB;;;;;GAKG;AACH,eAAO,MAAM,aAAa;;;EAA8B,CAAA;AAExD;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAA;AAEnD;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;iBASvB,CAAA;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAE3D;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,SAAgB,MAAM,KAAG,OAAO,CAAC,gBAAgB,CAgBvE,CAAA"}
package/dist/config.js ADDED
@@ -0,0 +1,45 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import z from "zod";
3
+ /**
4
+ * The runtimes the CA can be run under by {@link startTestServer}: `node` uses
5
+ * the package's Node.js helpers with an in-memory `node:sqlite` database, and
6
+ * `workerd` uses the Cloudflare Workers runtime with a local D1 database and
7
+ * Secrets Store.
8
+ */
9
+ export const RuntimeSchema = z.enum(["node", "workerd"]);
10
+ /**
11
+ * The configuration file read by {@link readConfig}.
12
+ */
13
+ export const ConfigSchema = z.object({
14
+ /** The CA private key in OpenSSH format */
15
+ private_key: z.string().min(1),
16
+ /** The CA configuration, as the Workers vars (environment variables) */
17
+ bindings: z.record(z.string(), z.string()),
18
+ /** Path the listening port is written to once the CA is ready */
19
+ port_file: z.string().min(1),
20
+ /** Path that log output from the CA and harness is appended to */
21
+ log_file: z.string().min(1),
22
+ });
23
+ /**
24
+ * Read and validate a test server configuration file.
25
+ *
26
+ * @param path The path of a JSON file matching {@link ConfigSchema}
27
+ * @returns The validated configuration
28
+ * @throws Error if the file cannot be read, is not JSON or is not a valid configuration
29
+ */
30
+ export const readConfig = async (path) => {
31
+ const text = await readFile(path, { encoding: "utf-8" });
32
+ let json;
33
+ try {
34
+ json = JSON.parse(text);
35
+ }
36
+ catch (err) {
37
+ throw new Error(`parsing ${path}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
38
+ }
39
+ const result = ConfigSchema.safeParse(json);
40
+ if (!result.success) {
41
+ throw new Error(`invalid configuration in ${path}:\n${z.prettifyError(result.error)}`);
42
+ }
43
+ return result.data;
44
+ };
45
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,CAAC,MAAM,KAAK,CAAA;AAEnB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAA;AAOxD;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,2CAA2C;IAC3C,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,wEAAwE;IACxE,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;IAC1C,iEAAiE;IACjE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC3B,CAAC,CAAA;AAOF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,IAAY,EAA6B,EAAE;IAC3E,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAA;IAExD,IAAI,IAAa,CAAA;IACjB,IAAI,CAAC;QACJ,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACxB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;IACxG,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;IAC3C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,4BAA4B,IAAI,MAAM,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IACvF,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAA;AACnB,CAAC,CAAA"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Format a value logged by the CA as text, with objects as JSON so log lines
3
+ * from the CA's logger are written as JSON lines.
4
+ *
5
+ * @param value A value passed to a `console` method
6
+ * @returns The value as a string
7
+ */
8
+ export declare const formatValue: (value: unknown) => string;
9
+ /**
10
+ * Format the arguments to a `console` method as a single line.
11
+ *
12
+ * @param args The arguments passed to a `console` method
13
+ * @returns The arguments formatted with {@link formatValue}, separated by spaces
14
+ */
15
+ export declare const formatArgs: (args: readonly unknown[]) => string;
16
+ /**
17
+ * Format a request handled by the CA as a log line, including the response
18
+ * body when the request was rejected.
19
+ *
20
+ * @param method The request method
21
+ * @param url The path and query of the request
22
+ * @param status The response status code
23
+ * @param body The response body
24
+ * @returns A JSON line with the message "harness request"
25
+ */
26
+ export declare const formatRequest: (method: string, url: string, status: number, body: string) => string;
27
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,UAAW,OAAO,KAAG,MAIA,CAAA;AAE7C;;;;;GAKG;AACH,eAAO,MAAM,UAAU,SAAU,SAAS,OAAO,EAAE,KAAG,MAAyC,CAAA;AAE/F;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,WAAY,MAAM,OAAO,MAAM,UAAU,MAAM,QAAQ,MAAM,KAAG,MAC0B,CAAA"}
package/dist/format.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Format a value logged by the CA as text, with objects as JSON so log lines
3
+ * from the CA's logger are written as JSON lines.
4
+ *
5
+ * @param value A value passed to a `console` method
6
+ * @returns The value as a string
7
+ */
8
+ export const formatValue = (value) => typeof value === "string"
9
+ ? value
10
+ : JSON.stringify(value, (_, x) => x instanceof Error
11
+ ? { name: x.name, message: x.message, stack: x.stack }
12
+ : typeof x === "bigint" ? x.toString() : x);
13
+ /**
14
+ * Format the arguments to a `console` method as a single line.
15
+ *
16
+ * @param args The arguments passed to a `console` method
17
+ * @returns The arguments formatted with {@link formatValue}, separated by spaces
18
+ */
19
+ export const formatArgs = (args) => args.map(formatValue).join(" ");
20
+ /**
21
+ * Format a request handled by the CA as a log line, including the response
22
+ * body when the request was rejected.
23
+ *
24
+ * @param method The request method
25
+ * @param url The path and query of the request
26
+ * @param status The response status code
27
+ * @param body The response body
28
+ * @returns A JSON line with the message "harness request"
29
+ */
30
+ export const formatRequest = (method, url, status, body) => formatValue({ level: "info", message: "harness request", method, url, status, ...(status >= 400 ? { body } : {}) });
31
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAc,EAAU,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ;IAC/E,CAAC,CAAC,KAAK;IACP,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAU,EAAE,EAAE,CAAC,CAAC,YAAY,KAAK;QAC5D,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE;QACtD,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAE7C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAwB,EAAU,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAE/F;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,MAAc,EAAE,GAAW,EAAE,MAAc,EAAE,IAAY,EAAU,EAAE,CAClG,WAAW,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA"}
@@ -0,0 +1,21 @@
1
+ import { type Runtime } from "./config.js";
2
+ import type { TestServer } from "./server.js";
3
+ export { ConfigSchema, readConfig, RuntimeSchema } from "./config.js";
4
+ export type { Runtime, TestServerConfig } from "./config.js";
5
+ export { FAULT_MESSAGE, FAULT_MESSAGES, isFault, LogFile } from "./log.js";
6
+ export type { TestServer } from "./server.js";
7
+ /**
8
+ * Start the CA under `runtime` using the configuration in `configFile` (see
9
+ * {@link ConfigSchema}), then write the port it is listening on to the
10
+ * configuration's `port_file`.
11
+ *
12
+ * Output from the CA and harness is appended to the configuration's
13
+ * `log_file`, see {@link LogFile}. Under workerd, Wrangler's working files are
14
+ * written to the directory containing `configFile`.
15
+ *
16
+ * @param runtime The runtime to run the CA under, see {@link Runtime}
17
+ * @param configFile The path of the configuration file
18
+ * @returns The running server, see {@link TestServer}
19
+ */
20
+ export declare const startTestServer: (runtime: Runtime, configFile: string) => Promise<TestServer>;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,aAAa,CAAA;AAGtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AACrE,YAAY,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAC1E,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,YAAmB,OAAO,cAAc,MAAM,KAAG,OAAO,CAAC,UAAU,CA6B9F,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Runs the Serverless SSH CA for end-to-end tests of clients, under Node.js or
3
+ * workerd, configured from a file and reporting what it does through a log
4
+ * file of JSON lines (see {@link LogFile}).
5
+ *
6
+ * Most clients run the `serverless-ssh-ca-test-server` command rather than
7
+ * using this module directly.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { startTestServer } from "@andrewheberle/serverless-ssh-ca-testing"
12
+ *
13
+ * const server = await startTestServer("node", "config.json")
14
+ * // make requests to http://127.0.0.1:${server.port}
15
+ * await server.close()
16
+ * ```
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ import { writeFile } from "node:fs/promises";
21
+ import { dirname, resolve } from "node:path";
22
+ import { readConfig } from "./config.js";
23
+ import { LogFile } from "./log.js";
24
+ import { startNode } from "./node.js";
25
+ export { ConfigSchema, readConfig, RuntimeSchema } from "./config.js";
26
+ export { FAULT_MESSAGE, FAULT_MESSAGES, isFault, LogFile } from "./log.js";
27
+ /**
28
+ * Start the CA under `runtime` using the configuration in `configFile` (see
29
+ * {@link ConfigSchema}), then write the port it is listening on to the
30
+ * configuration's `port_file`.
31
+ *
32
+ * Output from the CA and harness is appended to the configuration's
33
+ * `log_file`, see {@link LogFile}. Under workerd, Wrangler's working files are
34
+ * written to the directory containing `configFile`.
35
+ *
36
+ * @param runtime The runtime to run the CA under, see {@link Runtime}
37
+ * @param configFile The path of the configuration file
38
+ * @returns The running server, see {@link TestServer}
39
+ */
40
+ export const startTestServer = async (runtime, configFile) => {
41
+ const config = await readConfig(configFile);
42
+ const log = new LogFile(config.log_file);
43
+ let server;
44
+ switch (runtime) {
45
+ case "node":
46
+ server = await startNode(config, log);
47
+ break;
48
+ case "workerd": {
49
+ // loaded on demand as importing Wrangler is slow
50
+ const { startWorkerd } = await import("./workerd.js");
51
+ server = await startWorkerd(config, log, dirname(resolve(configFile)));
52
+ break;
53
+ }
54
+ default: {
55
+ const unknown = runtime;
56
+ throw new Error(`unknown runtime: ${String(unknown)}`);
57
+ }
58
+ }
59
+ try {
60
+ await writeFile(config.port_file, String(server.port));
61
+ }
62
+ catch (err) {
63
+ await server.close();
64
+ throw err;
65
+ }
66
+ return server;
67
+ };
68
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAgB,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAGrC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAErE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAG1E;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAAE,OAAgB,EAAE,UAAkB,EAAuB,EAAE;IAClG,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,UAAU,CAAC,CAAA;IAC3C,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAExC,IAAI,MAAkB,CAAA;IACtB,QAAQ,OAAO,EAAE,CAAC;QACjB,KAAK,MAAM;YACV,MAAM,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;YACrC,MAAK;QACN,KAAK,SAAS,EAAE,CAAC;YAChB,iDAAiD;YACjD,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAA;YACrD,MAAM,GAAG,MAAM,YAAY,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;YACtE,MAAK;QACN,CAAC;QACD,SAAS,CAAC;YACT,MAAM,OAAO,GAAU,OAAO,CAAA;YAC9B,MAAM,IAAI,KAAK,CAAC,oBAAoB,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QACvD,CAAC;IACF,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,SAAS,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IACvD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QACpB,MAAM,GAAG,CAAA;IACV,CAAC;IAED,OAAO,MAAM,CAAA;AACd,CAAC,CAAA"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Messages the CA logs at error level when it has failed rather than rejected
3
+ * a request, see {@link isFault}.
4
+ */
5
+ export declare const FAULT_MESSAGES: readonly string[];
6
+ /**
7
+ * The message of the line written by {@link LogFile.write} after a CA log line
8
+ * that indicates a fault.
9
+ */
10
+ export declare const FAULT_MESSAGE = "harness fault";
11
+ /**
12
+ * Report whether a line logged by the CA indicates a fault, such as a database
13
+ * error, rather than an expected rejection of a request. Some faults are only
14
+ * logged, as the CA still issues a certificate when it cannot record it.
15
+ *
16
+ * @param line A line of output from the CA
17
+ * @returns true if the line is an error logged by the CA for a fault
18
+ */
19
+ export declare const isFault: (line: string) => boolean;
20
+ /**
21
+ * The log file of a test server, written as JSON lines.
22
+ *
23
+ * Lines from the CA are written as they are logged. The harness adds the
24
+ * following lines, whose messages all start with "harness":
25
+ *
26
+ * - `{"level":"info","message":"harness request",...}` for each request, with
27
+ * `method`, `url`, `status` and, when `status` is 400 or more, the response
28
+ * `body`.
29
+ * - `{"level":"error","message":"harness ...",...}` for a fault in the CA or
30
+ * harness. A CA log line that indicates a fault (see {@link isFault}) is
31
+ * followed by a line with the message {@link FAULT_MESSAGE}.
32
+ */
33
+ export declare class LogFile {
34
+ readonly path: string;
35
+ constructor(path: string);
36
+ /**
37
+ * Append a line of output from the CA, followed by a fault line if it
38
+ * indicates a fault.
39
+ */
40
+ write(line: string): void;
41
+ /**
42
+ * Append a request handled by the CA.
43
+ */
44
+ request(method: string, url: string, status: number, body: string): void;
45
+ /**
46
+ * Append a fault in the harness.
47
+ *
48
+ * @param message A description of the fault, which is prefixed with "harness"
49
+ * @param error The error that caused the fault
50
+ */
51
+ fault(message: string, error: unknown): void;
52
+ private append;
53
+ }
54
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAAwD,CAAA;AAEpG;;;GAGG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAQ5C;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,SAAU,MAAM,KAAG,OActC,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,OAAO;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB,YAAY,IAAI,EAAE,MAAM,EAEvB;IAED;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAKxB;IAED;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAEvE;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAE3C;IAED,OAAO,CAAC,MAAM;CAGd"}
package/dist/log.js ADDED
@@ -0,0 +1,88 @@
1
+ import { appendFileSync } from "node:fs";
2
+ import z from "zod";
3
+ import { formatRequest, formatValue } from "./format.js";
4
+ /**
5
+ * Messages the CA logs at error level when it has failed rather than rejected
6
+ * a request, see {@link isFault}.
7
+ */
8
+ export const FAULT_MESSAGES = ["unhandled error", "unexpected error from router"];
9
+ /**
10
+ * The message of the line written by {@link LogFile.write} after a CA log line
11
+ * that indicates a fault.
12
+ */
13
+ export const FAULT_MESSAGE = "harness fault";
14
+ // the fields of a CA log line used to detect faults
15
+ const LogLineSchema = z.object({
16
+ level: z.string(),
17
+ message: z.string(),
18
+ });
19
+ /**
20
+ * Report whether a line logged by the CA indicates a fault, such as a database
21
+ * error, rather than an expected rejection of a request. Some faults are only
22
+ * logged, as the CA still issues a certificate when it cannot record it.
23
+ *
24
+ * @param line A line of output from the CA
25
+ * @returns true if the line is an error logged by the CA for a fault
26
+ */
27
+ export const isFault = (line) => {
28
+ let json;
29
+ try {
30
+ json = JSON.parse(line);
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ const result = LogLineSchema.safeParse(json);
36
+ if (!result.success || result.data.level.toLowerCase() !== "error") {
37
+ return false;
38
+ }
39
+ return FAULT_MESSAGES.includes(result.data.message) || result.data.message.includes("database");
40
+ };
41
+ /**
42
+ * The log file of a test server, written as JSON lines.
43
+ *
44
+ * Lines from the CA are written as they are logged. The harness adds the
45
+ * following lines, whose messages all start with "harness":
46
+ *
47
+ * - `{"level":"info","message":"harness request",...}` for each request, with
48
+ * `method`, `url`, `status` and, when `status` is 400 or more, the response
49
+ * `body`.
50
+ * - `{"level":"error","message":"harness ...",...}` for a fault in the CA or
51
+ * harness. A CA log line that indicates a fault (see {@link isFault}) is
52
+ * followed by a line with the message {@link FAULT_MESSAGE}.
53
+ */
54
+ export class LogFile {
55
+ path;
56
+ constructor(path) {
57
+ this.path = path;
58
+ }
59
+ /**
60
+ * Append a line of output from the CA, followed by a fault line if it
61
+ * indicates a fault.
62
+ */
63
+ write(line) {
64
+ this.append(line);
65
+ if (isFault(line)) {
66
+ this.append(formatValue({ level: "error", message: FAULT_MESSAGE, line }));
67
+ }
68
+ }
69
+ /**
70
+ * Append a request handled by the CA.
71
+ */
72
+ request(method, url, status, body) {
73
+ this.append(formatRequest(method, url, status, body));
74
+ }
75
+ /**
76
+ * Append a fault in the harness.
77
+ *
78
+ * @param message A description of the fault, which is prefixed with "harness"
79
+ * @param error The error that caused the fault
80
+ */
81
+ fault(message, error) {
82
+ this.append(formatValue({ level: "error", message: `harness ${message}`, error }));
83
+ }
84
+ append(line) {
85
+ appendFileSync(this.path, line + "\n");
86
+ }
87
+ }
88
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,CAAC,MAAM,KAAK,CAAA;AACnB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAExD;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsB,CAAC,iBAAiB,EAAE,8BAA8B,CAAC,CAAA;AAEpG;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAA;AAE5C,oDAAoD;AACpD,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;CACnB,CAAC,CAAA;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAY,EAAW,EAAE;IAChD,IAAI,IAAa,CAAA;IACjB,IAAI,CAAC;QACJ,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACxB,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAA;IACb,CAAC;IAED,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;IAC5C,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,OAAO,EAAE,CAAC;QACpE,OAAO,KAAK,CAAA;IACb,CAAC;IAED,OAAO,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;AAChG,CAAC,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,OAAO;IACV,IAAI,CAAQ;IAErB,YAAY,IAAY;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IACjB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAY;QACjB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACjB,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACnB,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QAC3E,CAAC;IACF,CAAC;IAED;;OAEG;IACH,OAAO,CAAC,MAAc,EAAE,GAAW,EAAE,MAAc,EAAE,IAAY;QAChE,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;IACtD,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAe,EAAE,KAAc;QACpC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;IACnF,CAAC;IAEO,MAAM,CAAC,IAAY;QAC1B,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC,CAAA;IACvC,CAAC;CACD"}
package/dist/node.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { TestServerConfig } from "./config.js";
2
+ import type { LogFile } from "./log.js";
3
+ import type { TestServer } from "./server.js";
4
+ /**
5
+ * Run the CA on Node.js, created with the package's Node.js helpers and an
6
+ * in-memory `node:sqlite` database, behind an HTTP server on a random port.
7
+ *
8
+ * Output the CA writes with `console` is written to `log` until the server is
9
+ * closed.
10
+ *
11
+ * @param config The test server configuration
12
+ * @param log The log file for the CA and harness
13
+ * @returns The running server
14
+ * @throws InvalidConfigurationError if `config.bindings` is not a valid CA configuration
15
+ */
16
+ export declare const startNode: (config: TestServerConfig, log: LogFile) => Promise<TestServer>;
17
+ //# sourceMappingURL=node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AA2B7C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,SAAS,WAAkB,gBAAgB,OAAO,OAAO,KAAG,OAAO,CAAC,UAAU,CAiE1F,CAAA"}