@sixthsummit/ship-cli 1.2.0 → 1.3.0-alpha-20261023234220
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.
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageManager": "pnpm@10.34.6",
|
|
4
4
|
"packages": {
|
|
5
|
-
"@sixthsummit/ship-core": "1.
|
|
6
|
-
"@sixthsummit/ship-hono": "1.
|
|
7
|
-
"@sixthsummit/ship-contract": "1.
|
|
5
|
+
"@sixthsummit/ship-core": "1.2.0-alpha-20261023234220",
|
|
6
|
+
"@sixthsummit/ship-hono": "1.3.0-alpha-20261023234220",
|
|
7
|
+
"@sixthsummit/ship-contract": "1.4.0-alpha-20261023234220"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@hono/node-server": "^1.19.14",
|
|
11
|
-
"hono": "4.13.
|
|
12
|
-
"zod": "^4.
|
|
11
|
+
"hono": "^4.13.13",
|
|
12
|
+
"zod": "^4.6.7"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@biomejs/biome": "2.3.11",
|
|
16
|
-
"@sixthsummit/ship-cli": "1.
|
|
16
|
+
"@sixthsummit/ship-cli": "1.3.0-alpha-20261023234220",
|
|
17
17
|
"@types/node": "^24.1.0",
|
|
18
18
|
"@vitest/coverage-v8": "^4.1.8",
|
|
19
19
|
"tsx": "^4.20.3",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sixthsummit/ship-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0-alpha-20261023234220",
|
|
4
4
|
"description": "Deterministic application graph and API contract tools for Ship projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -39,17 +39,17 @@
|
|
|
39
39
|
"NOTICE"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@sixthsummit/ship-contract": "^1.
|
|
42
|
+
"@sixthsummit/ship-contract": "^1.4.0-alpha-20261023234220",
|
|
43
43
|
"commander": "^12.0.0",
|
|
44
44
|
"tsx": "^4.20.3"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@sixthsummit/ship-core": "^1.
|
|
47
|
+
"@sixthsummit/ship-core": "^1.2.0-alpha-20261023234220"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@sixthsummit/ship-core": "1.
|
|
51
|
-
"@sixthsummit/ship-hono": "^1.
|
|
52
|
-
"zod": "^4.
|
|
50
|
+
"@sixthsummit/ship-core": "1.2.0-alpha-20261023234220",
|
|
51
|
+
"@sixthsummit/ship-hono": "^1.3.0-alpha-20261023234220",
|
|
52
|
+
"zod": "^4.6.7"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
55
|
"node": ">=22"
|
|
@@ -14,6 +14,24 @@ pnpm run dev
|
|
|
14
14
|
`GET http://localhost:3000/health` executes `system.health` through the Hono
|
|
15
15
|
adapter. The same operation runs without HTTP in `src/application.test.ts`.
|
|
16
16
|
|
|
17
|
+
## Browser clients on another origin
|
|
18
|
+
|
|
19
|
+
`src/http.ts` builds the adapter with a cross-origin policy from the
|
|
20
|
+
`CORS_ORIGINS` environment variable, a comma-separated allowlist:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
CORS_ORIGINS=https://app.example.com,https://admin.example.com pnpm run dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Listed origins may send credentials (cookies, `Authorization`), preflights are
|
|
27
|
+
answered for every route, and `X-Request-Id` is exposed to the browser. Leave it
|
|
28
|
+
unset when no browser calls the API from another origin. `CORS_ORIGINS=*` is
|
|
29
|
+
refused at boot: browsers reject a wildcard with credentials, and a server that
|
|
30
|
+
emits it anyway is a CSRF bypass waiting for a lenient client. Change the policy
|
|
31
|
+
(for example `exposeHeaders` for headers your operations set) in `src/http.ts`;
|
|
32
|
+
the `hono` dependency is yours to pin, within the range `@sixthsummit/ship-hono`
|
|
33
|
+
declares as a peer, so the app and the adapter always share one Hono copy.
|
|
34
|
+
|
|
17
35
|
## Extend
|
|
18
36
|
|
|
19
37
|
Add a bounded directory under `src/modules/`. Declare operations with stable IDs,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { corsFromEnv, createHttp } from "./http.js";
|
|
3
|
+
|
|
4
|
+
const ORIGIN = "https://app.example.test";
|
|
5
|
+
|
|
6
|
+
describe("{{PROJECT_NAME}} HTTP", () => {
|
|
7
|
+
it("parses the CORS_ORIGINS allowlist", () => {
|
|
8
|
+
expect(corsFromEnv(undefined)).toBeUndefined();
|
|
9
|
+
expect(corsFromEnv(" , ")).toBeUndefined();
|
|
10
|
+
expect(corsFromEnv(` ${ORIGIN}, https://admin.example.test `)).toEqual({
|
|
11
|
+
origin: [ORIGIN, "https://admin.example.test"],
|
|
12
|
+
credentials: true,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("answers a cross-origin preflight for a listed origin", async () => {
|
|
17
|
+
const http = createHttp({ CORS_ORIGINS: ORIGIN });
|
|
18
|
+
|
|
19
|
+
const preflight = await http.request("/health", {
|
|
20
|
+
method: "OPTIONS",
|
|
21
|
+
headers: { origin: ORIGIN, "access-control-request-method": "GET" },
|
|
22
|
+
});
|
|
23
|
+
expect(preflight.status).toBe(204);
|
|
24
|
+
expect(preflight.headers.get("access-control-allow-origin")).toBe(ORIGIN);
|
|
25
|
+
expect(preflight.headers.get("access-control-allow-credentials")).toBe("true");
|
|
26
|
+
|
|
27
|
+
const response = await http.request("/health", { headers: { origin: ORIGIN } });
|
|
28
|
+
expect(response.status).toBe(200);
|
|
29
|
+
expect(response.headers.get("access-control-allow-origin")).toBe(ORIGIN);
|
|
30
|
+
expect(response.headers.get("access-control-expose-headers")).toContain("X-Request-Id");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("emits no cross-origin headers without an allowlist", async () => {
|
|
34
|
+
const response = await createHttp({}).request("/health", { headers: { origin: ORIGIN } });
|
|
35
|
+
|
|
36
|
+
expect(response.status).toBe(200);
|
|
37
|
+
expect(response.headers.get("access-control-allow-origin")).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("refuses a wildcard allowlist because credentials are enabled", () => {
|
|
41
|
+
expect(() => createHttp({ CORS_ORIGINS: "*" })).toThrow(/credentials: true/u);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createHonoAdapter, type HonoCorsOptions } from "@sixthsummit/ship-hono";
|
|
2
|
+
import { app } from "./application.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Browser origins allowed to call this API, from a comma-separated
|
|
6
|
+
* `CORS_ORIGINS` allowlist (`https://app.example.com,https://admin.example.com`).
|
|
7
|
+
*
|
|
8
|
+
* Unset or empty means no cross-origin policy: a browser on another origin is
|
|
9
|
+
* refused. Credentials (cookies, `Authorization`) are allowed for the listed
|
|
10
|
+
* origins, so `*` is rejected at boot; list every origin explicitly.
|
|
11
|
+
*/
|
|
12
|
+
export function corsFromEnv(value: string | undefined): HonoCorsOptions | undefined {
|
|
13
|
+
const origins = (value ?? "")
|
|
14
|
+
.split(",")
|
|
15
|
+
.map((entry) => entry.trim())
|
|
16
|
+
.filter((entry) => entry.length > 0);
|
|
17
|
+
if (origins.length === 0) return undefined;
|
|
18
|
+
return { origin: origins, credentials: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The HTTP projection of the application graph, with CORS from the environment. */
|
|
22
|
+
export function createHttp(env: { readonly CORS_ORIGINS?: string } = process.env) {
|
|
23
|
+
const cors = corsFromEnv(env.CORS_ORIGINS);
|
|
24
|
+
return createHonoAdapter(app, cors ? { cors } : {});
|
|
25
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { serve } from "@hono/node-server";
|
|
2
|
-
import {
|
|
3
|
-
import { app } from "./application.js";
|
|
2
|
+
import { createHttp } from "./http.js";
|
|
4
3
|
|
|
5
4
|
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
|
|
6
5
|
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {
|
|
7
6
|
throw new Error("PORT must be an integer between 1 and 65535");
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
// CORS comes from the CORS_ORIGINS allowlist; see src/http.ts.
|
|
10
|
+
const http = createHttp();
|
|
11
11
|
serve({ fetch: http.fetch, port }, ({ port: listeningPort }) => {
|
|
12
12
|
process.stdout.write(`{{PACKAGE_NAME}} listening on ${listeningPort}\n`);
|
|
13
13
|
});
|