otavia 0.1.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/bun.lock +589 -0
- package/package.json +35 -0
- package/src/cli.ts +153 -0
- package/src/commands/__tests__/aws-auth.test.ts +32 -0
- package/src/commands/__tests__/cell.test.ts +44 -0
- package/src/commands/__tests__/dev.test.ts +49 -0
- package/src/commands/__tests__/init.test.ts +47 -0
- package/src/commands/__tests__/setup.test.ts +263 -0
- package/src/commands/aws-auth.ts +32 -0
- package/src/commands/aws.ts +59 -0
- package/src/commands/cell.ts +33 -0
- package/src/commands/clean.ts +32 -0
- package/src/commands/deploy.ts +508 -0
- package/src/commands/dev/__tests__/fixtures/gateway-cell/cell.yaml +8 -0
- package/src/commands/dev/__tests__/gateway-backend-routes.test.ts +13 -0
- package/src/commands/dev/__tests__/gateway-forward-url.test.ts +20 -0
- package/src/commands/dev/__tests__/gateway-sso-base-url.test.ts +93 -0
- package/src/commands/dev/__tests__/tunnel.test.ts +93 -0
- package/src/commands/dev/__tests__/vite-dev-proxy-rules.test.ts +220 -0
- package/src/commands/dev/__tests__/well-known.test.ts +88 -0
- package/src/commands/dev/forward-url.ts +7 -0
- package/src/commands/dev/gateway.ts +421 -0
- package/src/commands/dev/main-frontend-runtime/main-entry.ts +35 -0
- package/src/commands/dev/main-frontend-runtime/vite-config.ts +210 -0
- package/src/commands/dev/mount-selection.ts +9 -0
- package/src/commands/dev/tunnel.ts +176 -0
- package/src/commands/dev/vite-dev.ts +382 -0
- package/src/commands/dev/well-known.ts +76 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/init.ts +69 -0
- package/src/commands/lint.ts +49 -0
- package/src/commands/setup.ts +887 -0
- package/src/commands/test.ts +331 -0
- package/src/commands/typecheck.ts +36 -0
- package/src/config/__tests__/load-cell-yaml.test.ts +248 -0
- package/src/config/__tests__/load-otavia-yaml.test.ts +492 -0
- package/src/config/__tests__/ports.test.ts +48 -0
- package/src/config/__tests__/resolve-cell-dir.test.ts +60 -0
- package/src/config/__tests__/resolve-params.test.ts +137 -0
- package/src/config/__tests__/resource-names.test.ts +62 -0
- package/src/config/cell-yaml-schema.ts +115 -0
- package/src/config/load-cell-yaml.ts +87 -0
- package/src/config/load-otavia-yaml.ts +256 -0
- package/src/config/otavia-yaml-schema.ts +49 -0
- package/src/config/ports.ts +57 -0
- package/src/config/resolve-cell-dir.ts +55 -0
- package/src/config/resolve-params.ts +160 -0
- package/src/config/resource-names.ts +60 -0
- package/src/deploy/__tests__/template.test.ts +137 -0
- package/src/deploy/api-gateway.ts +96 -0
- package/src/deploy/cloudflare-dns.ts +261 -0
- package/src/deploy/cloudfront.ts +228 -0
- package/src/deploy/dynamodb.ts +68 -0
- package/src/deploy/lambda.ts +121 -0
- package/src/deploy/s3.ts +57 -0
- package/src/deploy/template.ts +264 -0
- package/src/deploy/types.ts +16 -0
- package/src/local/docker.ts +175 -0
- package/src/local/dynamodb-local.ts +124 -0
- package/src/local/minio-local.ts +44 -0
- package/src/utils/env.test.ts +74 -0
- package/src/utils/env.ts +79 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { applyResourceNameEnvVars, resolveGatewaySsoBaseUrl } from "../gateway";
|
|
3
|
+
import { resolveRootRedirectMount } from "../mount-selection";
|
|
4
|
+
|
|
5
|
+
describe("resolveGatewaySsoBaseUrl", () => {
|
|
6
|
+
test("prefers configured SSO base URL from env", () => {
|
|
7
|
+
expect(resolveGatewaySsoBaseUrl("http://localhost:7100/sso", 8900, "sso")).toBe(
|
|
8
|
+
"http://localhost:7100/sso"
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("falls back to backend mount URL when env is missing", () => {
|
|
13
|
+
expect(resolveGatewaySsoBaseUrl(undefined, 8900, "sso")).toBe("http://localhost:8900/sso");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("uses tunnel host when provided and env is missing", () => {
|
|
17
|
+
expect(resolveGatewaySsoBaseUrl(undefined, 8900, "sso", "https://mybox.dev.example.com")).toBe(
|
|
18
|
+
"https://mybox.dev.example.com/sso"
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("overrides localhost SSO base URL in tunnel mode", () => {
|
|
23
|
+
expect(
|
|
24
|
+
resolveGatewaySsoBaseUrl(
|
|
25
|
+
"http://localhost:7100/sso",
|
|
26
|
+
8900,
|
|
27
|
+
"sso",
|
|
28
|
+
"https://mybox.dev.example.com"
|
|
29
|
+
)
|
|
30
|
+
).toBe("https://mybox.dev.example.com/sso");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("keeps non-local configured SSO base URL in tunnel mode", () => {
|
|
34
|
+
expect(
|
|
35
|
+
resolveGatewaySsoBaseUrl(
|
|
36
|
+
"https://sso.example.com",
|
|
37
|
+
8900,
|
|
38
|
+
"sso",
|
|
39
|
+
"https://mybox.dev.example.com"
|
|
40
|
+
)
|
|
41
|
+
).toBe("https://sso.example.com");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("applyResourceNameEnvVars", () => {
|
|
46
|
+
test("injects DYNAMODB_TABLE_* and S3_BUCKET_* env vars from stack/mount/key", () => {
|
|
47
|
+
const cells = [
|
|
48
|
+
{
|
|
49
|
+
mount: "agent",
|
|
50
|
+
cellDir: "/tmp/agent",
|
|
51
|
+
packageName: "@otavia/agent",
|
|
52
|
+
config: {
|
|
53
|
+
name: "agent",
|
|
54
|
+
params: [],
|
|
55
|
+
tables: {
|
|
56
|
+
settings: { keys: { pk: "S", sk: "S" } },
|
|
57
|
+
pending_client_info: { keys: { pk: "S" } },
|
|
58
|
+
},
|
|
59
|
+
buckets: {
|
|
60
|
+
uploads: {},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
env: {} as Record<string, string>,
|
|
64
|
+
},
|
|
65
|
+
] as any;
|
|
66
|
+
|
|
67
|
+
applyResourceNameEnvVars(cells, "otavia-local");
|
|
68
|
+
|
|
69
|
+
expect(cells[0].env.DYNAMODB_TABLE_SETTINGS).toBe("otavia-local-agent-settings");
|
|
70
|
+
expect(cells[0].env.DYNAMODB_TABLE_PENDING_CLIENT_INFO).toBe(
|
|
71
|
+
"otavia-local-agent-pending-client-info"
|
|
72
|
+
);
|
|
73
|
+
expect(cells[0].env.S3_BUCKET_UPLOADS).toBe("otavia-local-agent-uploads");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("resolveRootRedirectMount", () => {
|
|
78
|
+
test("prefers configured mount when it is mounted", () => {
|
|
79
|
+
expect(resolveRootRedirectMount(["sso", "drive", "agent"], "drive")).toBe("drive");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("falls back to first mount when configured mount is absent", () => {
|
|
83
|
+
expect(resolveRootRedirectMount(["sso", "agent"], "drive")).toBe("sso");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("falls back to first mount when no preferred mount is configured", () => {
|
|
87
|
+
expect(resolveRootRedirectMount(["sso", "drive", "agent"])).toBe("sso");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("returns empty string when there are no mounts", () => {
|
|
91
|
+
expect(resolveRootRedirectMount([])).toBe("");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildCloudflaredTunnelCommand,
|
|
4
|
+
extractTunnelHostFromConfig,
|
|
5
|
+
normalizeTunnelPublicBaseUrl,
|
|
6
|
+
resolveTunnelLogLevel,
|
|
7
|
+
resolveTunnelProtocol,
|
|
8
|
+
} from "../tunnel";
|
|
9
|
+
|
|
10
|
+
describe("extractTunnelHostFromConfig", () => {
|
|
11
|
+
test("returns first non-wildcard hostname in ingress", () => {
|
|
12
|
+
const config = `
|
|
13
|
+
tunnel: otavia-dev-mybox
|
|
14
|
+
credentials-file: /tmp/credentials.json
|
|
15
|
+
ingress:
|
|
16
|
+
- hostname: "*.mybox.dev.example.com"
|
|
17
|
+
service: http://127.0.0.1:7100
|
|
18
|
+
- hostname: "mybox.dev.example.com"
|
|
19
|
+
service: http://127.0.0.1:7100
|
|
20
|
+
- service: http_status:404
|
|
21
|
+
`;
|
|
22
|
+
expect(extractTunnelHostFromConfig(config)).toBe("mybox.dev.example.com");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("returns null when no hostname is configured", () => {
|
|
26
|
+
const config = `
|
|
27
|
+
tunnel: otavia-dev-mybox
|
|
28
|
+
credentials-file: /tmp/credentials.json
|
|
29
|
+
ingress:
|
|
30
|
+
- service: http://127.0.0.1:7100
|
|
31
|
+
- service: http_status:404
|
|
32
|
+
`;
|
|
33
|
+
expect(extractTunnelHostFromConfig(config)).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("normalizeTunnelPublicBaseUrl", () => {
|
|
38
|
+
test("adds https scheme for plain host", () => {
|
|
39
|
+
expect(normalizeTunnelPublicBaseUrl("mybox.dev.example.com")).toBe(
|
|
40
|
+
"https://mybox.dev.example.com"
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("trims trailing slash when already full URL", () => {
|
|
45
|
+
expect(normalizeTunnelPublicBaseUrl("https://mybox.dev.example.com/")).toBe(
|
|
46
|
+
"https://mybox.dev.example.com"
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("resolveTunnelLogLevel", () => {
|
|
52
|
+
test("defaults to warn", () => {
|
|
53
|
+
expect(resolveTunnelLogLevel()).toBe("warn");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("normalizes mixed-case level", () => {
|
|
57
|
+
expect(resolveTunnelLogLevel("InFo")).toBe("info");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("throws on invalid value", () => {
|
|
61
|
+
expect(() => resolveTunnelLogLevel("verbose")).toThrow(
|
|
62
|
+
'Invalid tunnel log level "verbose". Expected one of: debug, info, warn, error.'
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("buildCloudflaredTunnelCommand", () => {
|
|
68
|
+
test("builds command with quoted config path and log level", () => {
|
|
69
|
+
expect(buildCloudflaredTunnelCommand("/tmp/dev config.yml", "warn", "quic")).toBe(
|
|
70
|
+
'cloudflared tunnel --loglevel warn --protocol quic --config "/tmp/dev config.yml" run'
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("resolveTunnelProtocol", () => {
|
|
76
|
+
test("defaults to quic", () => {
|
|
77
|
+
expect(resolveTunnelProtocol()).toBe("quic");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("normalizes mixed-case protocol", () => {
|
|
81
|
+
expect(resolveTunnelProtocol("Http2")).toBe("http2");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("allows auto protocol", () => {
|
|
85
|
+
expect(resolveTunnelProtocol("auto")).toBe("auto");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("throws on invalid protocol", () => {
|
|
89
|
+
expect(() => resolveTunnelProtocol("ws")).toThrow(
|
|
90
|
+
'Invalid tunnel protocol "ws". Expected one of: auto, quic, http2.'
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildMainDevGeneratedConfig,
|
|
4
|
+
deriveFrontendModuleProxySpecs,
|
|
5
|
+
deriveFrontendRouteRulesFromCellConfig,
|
|
6
|
+
deriveRouteRulesFromCellConfig,
|
|
7
|
+
} from "../vite-dev.js";
|
|
8
|
+
|
|
9
|
+
describe("deriveRouteRulesFromCellConfig", () => {
|
|
10
|
+
test("converts backend routes into exact/prefix rules", () => {
|
|
11
|
+
const config = {
|
|
12
|
+
name: "agent",
|
|
13
|
+
backend: {
|
|
14
|
+
runtime: "nodejs20.x",
|
|
15
|
+
entries: {
|
|
16
|
+
api: {
|
|
17
|
+
handler: "lambda.ts",
|
|
18
|
+
timeout: 30,
|
|
19
|
+
memory: 1024,
|
|
20
|
+
routes: ["/api/*", "/oauth/login", "/.well-known/*"],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
} as any;
|
|
25
|
+
|
|
26
|
+
expect(deriveRouteRulesFromCellConfig(config)).toEqual([
|
|
27
|
+
{ path: "/api", match: "prefix" },
|
|
28
|
+
{ path: "/oauth/login", match: "exact" },
|
|
29
|
+
{ path: "/.well-known", match: "prefix" },
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("throws when a backend route does not start with slash", () => {
|
|
34
|
+
const config = {
|
|
35
|
+
name: "broken-cell",
|
|
36
|
+
backend: {
|
|
37
|
+
runtime: "nodejs20.x",
|
|
38
|
+
entries: {
|
|
39
|
+
api: {
|
|
40
|
+
handler: "lambda.ts",
|
|
41
|
+
timeout: 30,
|
|
42
|
+
memory: 1024,
|
|
43
|
+
routes: ["api/*"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
} as any;
|
|
48
|
+
|
|
49
|
+
expect(() => deriveRouteRulesFromCellConfig(config)).toThrow(
|
|
50
|
+
'Invalid backend route "api": route must start with "/"'
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("buildMainDevGeneratedConfig", () => {
|
|
56
|
+
test("builds mounted proxy rules from per-cell route rules", () => {
|
|
57
|
+
const generated = buildMainDevGeneratedConfig(
|
|
58
|
+
[
|
|
59
|
+
{
|
|
60
|
+
mount: "sso",
|
|
61
|
+
routeRules: [
|
|
62
|
+
{ path: "/oauth/authorize", match: "exact" },
|
|
63
|
+
{ path: "/oauth/callback", match: "exact" },
|
|
64
|
+
],
|
|
65
|
+
moduleProxySpecs: [],
|
|
66
|
+
frontendRouteRules: [],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
mount: "agent",
|
|
70
|
+
routeRules: [
|
|
71
|
+
{ path: "/api", match: "prefix" },
|
|
72
|
+
{ path: "/oauth/login", match: "exact" },
|
|
73
|
+
],
|
|
74
|
+
moduleProxySpecs: [
|
|
75
|
+
{
|
|
76
|
+
mount: "agent",
|
|
77
|
+
routePath: "/agent/sw.js",
|
|
78
|
+
sourcePath: "/repo/cells/agent/frontend/sw.ts",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
frontendRouteRules: [],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
8900
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(generated.firstMount).toBe("sso");
|
|
88
|
+
expect(generated.mounts).toEqual(["sso", "agent"]);
|
|
89
|
+
expect(generated.frontendModuleProxyRules).toEqual([
|
|
90
|
+
{
|
|
91
|
+
path: "/agent/sw.js",
|
|
92
|
+
sourcePath: "/repo/cells/agent/frontend/sw.ts",
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
expect(generated.routeRules).toEqual(
|
|
96
|
+
expect.arrayContaining([
|
|
97
|
+
{ path: "/api", match: "prefix" },
|
|
98
|
+
{ path: "/oauth/login", match: "exact" },
|
|
99
|
+
{ path: "/oauth/authorize", match: "exact" },
|
|
100
|
+
{ path: "/oauth/callback", match: "exact" },
|
|
101
|
+
{ path: "/.well-known", match: "prefix" },
|
|
102
|
+
])
|
|
103
|
+
);
|
|
104
|
+
expect(generated.proxyRules).toEqual(
|
|
105
|
+
expect.arrayContaining([
|
|
106
|
+
{
|
|
107
|
+
mount: "__global__",
|
|
108
|
+
path: "/.well-known",
|
|
109
|
+
match: "prefix",
|
|
110
|
+
target: "http://localhost:8900",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
mount: "agent",
|
|
114
|
+
path: "/agent/api",
|
|
115
|
+
match: "prefix",
|
|
116
|
+
target: "http://localhost:8900",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
mount: "agent",
|
|
120
|
+
path: "/agent/oauth/login",
|
|
121
|
+
match: "exact",
|
|
122
|
+
target: "http://localhost:8900",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
mount: "sso",
|
|
126
|
+
path: "/sso/oauth/authorize",
|
|
127
|
+
match: "exact",
|
|
128
|
+
target: "http://localhost:8900",
|
|
129
|
+
},
|
|
130
|
+
])
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("converts module source paths to paths relative to base dir", () => {
|
|
135
|
+
const generated = buildMainDevGeneratedConfig(
|
|
136
|
+
[
|
|
137
|
+
{
|
|
138
|
+
mount: "agent",
|
|
139
|
+
routeRules: [],
|
|
140
|
+
moduleProxySpecs: [
|
|
141
|
+
{
|
|
142
|
+
mount: "agent",
|
|
143
|
+
routePath: "/agent/sw.js",
|
|
144
|
+
sourcePath: "/repo/apps/main/node_modules/@otavia/agent/frontend/sw.ts",
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
frontendRouteRules: [],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
8900,
|
|
151
|
+
"/repo/apps/main"
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
expect(generated.frontendModuleProxyRules).toEqual([
|
|
155
|
+
{
|
|
156
|
+
path: "/agent/sw.js",
|
|
157
|
+
sourcePath: "node_modules/@otavia/agent/frontend/sw.ts",
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("deriveFrontendRouteRulesFromCellConfig", () => {
|
|
164
|
+
test("converts frontend entry routes into mounted route rules", () => {
|
|
165
|
+
const config = {
|
|
166
|
+
name: "agent",
|
|
167
|
+
frontend: {
|
|
168
|
+
dir: "frontend",
|
|
169
|
+
entries: {
|
|
170
|
+
main: { entry: "index.html", routes: ["/*"] },
|
|
171
|
+
sw: { entry: "sw.ts", routes: ["/sw.js"] },
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
} as any;
|
|
175
|
+
|
|
176
|
+
expect(deriveFrontendRouteRulesFromCellConfig("agent", config)).toEqual([
|
|
177
|
+
{ mount: "agent", path: "/agent", match: "prefix", entryName: "main", entryType: "html" },
|
|
178
|
+
{ mount: "agent", path: "/agent/sw.js", match: "exact", entryName: "sw", entryType: "module" },
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("deriveFrontendModuleProxySpecs", () => {
|
|
184
|
+
test("creates module proxy specs from non-html entries", () => {
|
|
185
|
+
const config = {
|
|
186
|
+
name: "agent",
|
|
187
|
+
frontend: {
|
|
188
|
+
dir: "frontend",
|
|
189
|
+
entries: {
|
|
190
|
+
main: { entry: "index.html", routes: ["/*"] },
|
|
191
|
+
sw: { entry: "sw.ts", routes: ["/sw.js"] },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
} as any;
|
|
195
|
+
|
|
196
|
+
expect(deriveFrontendModuleProxySpecs("agent", "/repo/cells/agent", config)).toEqual([
|
|
197
|
+
{
|
|
198
|
+
mount: "agent",
|
|
199
|
+
routePath: "/agent/sw.js",
|
|
200
|
+
sourcePath: "/repo/cells/agent/frontend/sw.ts",
|
|
201
|
+
},
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("throws for wildcard module routes", () => {
|
|
206
|
+
const config = {
|
|
207
|
+
name: "broken",
|
|
208
|
+
frontend: {
|
|
209
|
+
dir: "frontend",
|
|
210
|
+
entries: {
|
|
211
|
+
worker: { entry: "worker.ts", routes: ["/workers/*"] },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
} as any;
|
|
215
|
+
|
|
216
|
+
expect(() => deriveFrontendModuleProxySpecs("broken", "/repo/cells/broken", config)).toThrow(
|
|
217
|
+
'Invalid module frontend route "/workers/*" for mount "broken": wildcard routes are only supported for HTML entries'
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildOAuthAuthorizationServerMetadata,
|
|
4
|
+
buildOAuthProtectedResourceMetadata,
|
|
5
|
+
createOAuthDiscoveryRegistry,
|
|
6
|
+
extractMountFromAuthorizationServerWellKnownPath,
|
|
7
|
+
extractProtectedResourcePathFromWellKnown,
|
|
8
|
+
} from "../well-known.js";
|
|
9
|
+
|
|
10
|
+
describe("extractMountFromAuthorizationServerWellKnownPath", () => {
|
|
11
|
+
test("extracts mount from RFC 8414 well-known path suffix", () => {
|
|
12
|
+
expect(
|
|
13
|
+
extractMountFromAuthorizationServerWellKnownPath(
|
|
14
|
+
"/.well-known/oauth-authorization-server/agent"
|
|
15
|
+
)
|
|
16
|
+
).toBe("agent");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("returns null for root no-suffix discovery path", () => {
|
|
20
|
+
expect(
|
|
21
|
+
extractMountFromAuthorizationServerWellKnownPath(
|
|
22
|
+
"/.well-known/oauth-authorization-server"
|
|
23
|
+
)
|
|
24
|
+
).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("returns null for invalid suffix with extra segments", () => {
|
|
28
|
+
expect(
|
|
29
|
+
extractMountFromAuthorizationServerWellKnownPath(
|
|
30
|
+
"/.well-known/oauth-authorization-server/agent/nested"
|
|
31
|
+
)
|
|
32
|
+
).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("createOAuthDiscoveryRegistry", () => {
|
|
37
|
+
test("registers only oauth-enabled cells", () => {
|
|
38
|
+
const registry = createOAuthDiscoveryRegistry([
|
|
39
|
+
{ mount: "agent", config: { oauth: { enabled: true, role: "both", scopes: ["use_mcp"] } } },
|
|
40
|
+
{ mount: "drive", config: { oauth: { enabled: false, role: "both", scopes: ["use_mcp"] } } },
|
|
41
|
+
{ mount: "plain", config: {} },
|
|
42
|
+
] as any);
|
|
43
|
+
expect(Array.from(registry.keys())).toEqual(["agent"]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("buildOAuthAuthorizationServerMetadata", () => {
|
|
48
|
+
test("builds metadata with issuer at mount path", () => {
|
|
49
|
+
const metadata = buildOAuthAuthorizationServerMetadata("http://localhost:8900", "agent", ["use_mcp"]);
|
|
50
|
+
expect(metadata.issuer).toBe("http://localhost:8900/agent");
|
|
51
|
+
expect(metadata.authorization_endpoint).toBe("http://localhost:8900/agent/oauth/authorize");
|
|
52
|
+
expect(metadata.token_endpoint).toBe("http://localhost:8900/agent/oauth/token");
|
|
53
|
+
expect(metadata.registration_endpoint).toBe("http://localhost:8900/agent/oauth/register");
|
|
54
|
+
expect(metadata.scopes_supported).toEqual(["use_mcp"]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("extractProtectedResourcePathFromWellKnown", () => {
|
|
59
|
+
test("extracts protected resource path from RFC 9728 suffix", () => {
|
|
60
|
+
expect(
|
|
61
|
+
extractProtectedResourcePathFromWellKnown(
|
|
62
|
+
"/.well-known/oauth-protected-resource/drive/mcp"
|
|
63
|
+
)
|
|
64
|
+
).toBe("/drive/mcp");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("returns null for root path without suffix", () => {
|
|
68
|
+
expect(
|
|
69
|
+
extractProtectedResourcePathFromWellKnown(
|
|
70
|
+
"/.well-known/oauth-protected-resource"
|
|
71
|
+
)
|
|
72
|
+
).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("buildOAuthProtectedResourceMetadata", () => {
|
|
77
|
+
test("builds resource metadata for mounted MCP route", () => {
|
|
78
|
+
const metadata = buildOAuthProtectedResourceMetadata(
|
|
79
|
+
"http://localhost:7100",
|
|
80
|
+
"/drive/mcp",
|
|
81
|
+
"drive",
|
|
82
|
+
["use_mcp"]
|
|
83
|
+
);
|
|
84
|
+
expect(metadata.authorization_servers).toEqual(["http://localhost:7100/drive"]);
|
|
85
|
+
expect(metadata.resource).toBe("http://localhost:7100/drive/mcp");
|
|
86
|
+
expect(metadata.scopes_supported).toEqual(["use_mcp"]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function buildForwardUrlForCellMount(rawUrl: string, prefix: string): URL {
|
|
2
|
+
const url = new URL(rawUrl);
|
|
3
|
+
const afterPrefix = url.pathname.slice(prefix.length) || "/";
|
|
4
|
+
const newUrl = new URL(afterPrefix, url.origin);
|
|
5
|
+
newUrl.search = url.search;
|
|
6
|
+
return newUrl;
|
|
7
|
+
}
|