@openparachute/hub 0.3.0-rc.1 → 0.5.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.
Files changed (91) hide show
  1. package/README.md +19 -17
  2. package/package.json +15 -4
  3. package/src/__tests__/admin-auth.test.ts +197 -0
  4. package/src/__tests__/admin-config.test.ts +281 -0
  5. package/src/__tests__/admin-grants.test.ts +271 -0
  6. package/src/__tests__/admin-handlers.test.ts +530 -0
  7. package/src/__tests__/admin-host-admin-token.test.ts +115 -0
  8. package/src/__tests__/admin-vault-admin-token.test.ts +190 -0
  9. package/src/__tests__/admin-vaults.test.ts +615 -0
  10. package/src/__tests__/auth-codes.test.ts +253 -0
  11. package/src/__tests__/auth.test.ts +1063 -17
  12. package/src/__tests__/cli.test.ts +50 -0
  13. package/src/__tests__/clients.test.ts +264 -0
  14. package/src/__tests__/cloudflare-state.test.ts +167 -7
  15. package/src/__tests__/csrf.test.ts +117 -0
  16. package/src/__tests__/expose-cloudflare.test.ts +232 -37
  17. package/src/__tests__/expose-off-auto.test.ts +15 -9
  18. package/src/__tests__/expose-public-auto.test.ts +153 -0
  19. package/src/__tests__/expose.test.ts +216 -24
  20. package/src/__tests__/grants.test.ts +164 -0
  21. package/src/__tests__/hub-db.test.ts +153 -0
  22. package/src/__tests__/hub-server.test.ts +984 -26
  23. package/src/__tests__/hub.test.ts +56 -49
  24. package/src/__tests__/install.test.ts +327 -3
  25. package/src/__tests__/jwks.test.ts +37 -0
  26. package/src/__tests__/jwt-sign.test.ts +361 -0
  27. package/src/__tests__/lifecycle.test.ts +616 -5
  28. package/src/__tests__/module-manifest.test.ts +183 -0
  29. package/src/__tests__/oauth-handlers.test.ts +3112 -0
  30. package/src/__tests__/oauth-ui.test.ts +253 -0
  31. package/src/__tests__/operator-token.test.ts +140 -0
  32. package/src/__tests__/providers-detect.test.ts +158 -0
  33. package/src/__tests__/scope-explanations.test.ts +108 -0
  34. package/src/__tests__/scope-registry.test.ts +220 -0
  35. package/src/__tests__/services-manifest.test.ts +137 -1
  36. package/src/__tests__/sessions.test.ts +116 -0
  37. package/src/__tests__/setup.test.ts +361 -0
  38. package/src/__tests__/signing-keys.test.ts +153 -0
  39. package/src/__tests__/upgrade.test.ts +541 -0
  40. package/src/__tests__/users.test.ts +154 -0
  41. package/src/__tests__/well-known.test.ts +127 -10
  42. package/src/admin-auth.ts +126 -0
  43. package/src/admin-config-ui.ts +534 -0
  44. package/src/admin-config.ts +226 -0
  45. package/src/admin-grants.ts +160 -0
  46. package/src/admin-handlers.ts +365 -0
  47. package/src/admin-host-admin-token.ts +83 -0
  48. package/src/admin-vault-admin-token.ts +98 -0
  49. package/src/admin-vaults.ts +359 -0
  50. package/src/auth-codes.ts +189 -0
  51. package/src/cli.ts +202 -25
  52. package/src/clients.ts +210 -0
  53. package/src/cloudflare/config.ts +25 -6
  54. package/src/cloudflare/state.ts +108 -28
  55. package/src/commands/auth.ts +851 -19
  56. package/src/commands/expose-cloudflare.ts +85 -45
  57. package/src/commands/expose-interactive.ts +20 -44
  58. package/src/commands/expose-off-auto.ts +27 -11
  59. package/src/commands/expose-public-auto.ts +179 -0
  60. package/src/commands/expose.ts +63 -32
  61. package/src/commands/install.ts +337 -48
  62. package/src/commands/lifecycle.ts +269 -38
  63. package/src/commands/setup.ts +366 -0
  64. package/src/commands/status.ts +4 -1
  65. package/src/commands/upgrade.ts +429 -0
  66. package/src/csrf.ts +101 -0
  67. package/src/grants.ts +142 -0
  68. package/src/help.ts +133 -19
  69. package/src/hub-control.ts +12 -0
  70. package/src/hub-db.ts +164 -0
  71. package/src/hub-server.ts +643 -22
  72. package/src/hub.ts +97 -390
  73. package/src/jwks.ts +41 -0
  74. package/src/jwt-audience.ts +40 -0
  75. package/src/jwt-sign.ts +275 -0
  76. package/src/module-manifest.ts +435 -0
  77. package/src/oauth-handlers.ts +1175 -0
  78. package/src/oauth-ui.ts +582 -0
  79. package/src/operator-token.ts +129 -0
  80. package/src/providers/detect.ts +97 -0
  81. package/src/scope-explanations.ts +137 -0
  82. package/src/scope-registry.ts +158 -0
  83. package/src/service-spec.ts +270 -97
  84. package/src/services-manifest.ts +57 -1
  85. package/src/sessions.ts +115 -0
  86. package/src/signing-keys.ts +120 -0
  87. package/src/users.ts +144 -0
  88. package/src/well-known.ts +62 -26
  89. package/web/ui/dist/assets/index-BKzPDdB0.js +60 -0
  90. package/web/ui/dist/assets/index-Dyk6g7vT.css +1 -0
  91. package/web/ui/dist/index.html +14 -0
@@ -0,0 +1,183 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { mkdirSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import {
7
+ ModuleManifestError,
8
+ readModuleManifest,
9
+ validateModuleManifest,
10
+ } from "../module-manifest.ts";
11
+
12
+ const VALID = {
13
+ name: "demo",
14
+ manifestName: "@example/demo",
15
+ kind: "api",
16
+ port: 1950,
17
+ paths: ["/demo"],
18
+ health: "/healthz",
19
+ } as const;
20
+
21
+ describe("validateModuleManifest", () => {
22
+ test("accepts a minimal valid manifest", () => {
23
+ const m = validateModuleManifest(VALID, "test");
24
+ expect(m.name).toBe("demo");
25
+ expect(m.kind).toBe("api");
26
+ expect(m.port).toBe(1950);
27
+ expect(m.paths).toEqual(["/demo"]);
28
+ expect(m.health).toBe("/healthz");
29
+ });
30
+
31
+ test("rejects non-object root", () => {
32
+ expect(() => validateModuleManifest("nope", "where")).toThrow(ModuleManifestError);
33
+ expect(() => validateModuleManifest([1, 2], "where")).toThrow(/root must be an object/);
34
+ });
35
+
36
+ test("rejects missing required fields", () => {
37
+ expect(() => validateModuleManifest({ ...VALID, name: undefined }, "x")).toThrow(/name/);
38
+ expect(() => validateModuleManifest({ ...VALID, kind: "weird" }, "x")).toThrow(/kind/);
39
+ expect(() => validateModuleManifest({ ...VALID, port: -1 }, "x")).toThrow(/port/);
40
+ expect(() => validateModuleManifest({ ...VALID, port: 99999 }, "x")).toThrow(/port/);
41
+ expect(() => validateModuleManifest({ ...VALID, paths: "not-array" }, "x")).toThrow(/paths/);
42
+ expect(() => validateModuleManifest({ ...VALID, health: "no-leading-slash" }, "x")).toThrow(
43
+ /health/,
44
+ );
45
+ });
46
+
47
+ test("rejects invalid name shape", () => {
48
+ expect(() => validateModuleManifest({ ...VALID, name: "Demo" }, "x")).toThrow(/name/);
49
+ expect(() => validateModuleManifest({ ...VALID, name: "1demo" }, "x")).toThrow(/name/);
50
+ expect(() => validateModuleManifest({ ...VALID, name: "a_b" }, "x")).toThrow(/name/);
51
+ });
52
+
53
+ test("scope namespace must match module name", () => {
54
+ expect(() =>
55
+ validateModuleManifest({ ...VALID, scopes: { defines: ["vault:read"] } }, "x"),
56
+ ).toThrow(/namespace.*does not match/);
57
+ const ok = validateModuleManifest(
58
+ { ...VALID, scopes: { defines: ["demo:read", "demo:write"] } },
59
+ "x",
60
+ );
61
+ expect(ok.scopes?.defines).toEqual(["demo:read", "demo:write"]);
62
+ });
63
+
64
+ test("scope without colon is rejected", () => {
65
+ expect(() => validateModuleManifest({ ...VALID, scopes: { defines: ["demo"] } }, "x")).toThrow(
66
+ /namespaced/,
67
+ );
68
+ });
69
+
70
+ test("dependencies block accepts optional + scopes", () => {
71
+ const m = validateModuleManifest(
72
+ {
73
+ ...VALID,
74
+ dependencies: {
75
+ "parachute-vault": { optional: false, scopes: ["vault:read"] },
76
+ "parachute-scribe": { optional: true },
77
+ },
78
+ },
79
+ "x",
80
+ );
81
+ expect(m.dependencies?.["parachute-vault"]?.optional).toBe(false);
82
+ expect(m.dependencies?.["parachute-vault"]?.scopes).toEqual(["vault:read"]);
83
+ expect(m.dependencies?.["parachute-scribe"]?.optional).toBe(true);
84
+ });
85
+
86
+ test("startCmd must be non-empty if present", () => {
87
+ expect(() => validateModuleManifest({ ...VALID, startCmd: [] }, "x")).toThrow(/startCmd/);
88
+ const m = validateModuleManifest({ ...VALID, startCmd: ["bin", "--flag"] }, "x");
89
+ expect(m.startCmd).toEqual(["bin", "--flag"]);
90
+ });
91
+
92
+ test("optional displayName + tagline pass through", () => {
93
+ const m = validateModuleManifest(
94
+ { ...VALID, displayName: "Demo", tagline: "a demo module" },
95
+ "x",
96
+ );
97
+ expect(m.displayName).toBe("Demo");
98
+ expect(m.tagline).toBe("a demo module");
99
+ });
100
+
101
+ test("managementUrl accepts a leading-slash path", () => {
102
+ const m = validateModuleManifest({ ...VALID, managementUrl: "/admin" }, "x");
103
+ expect(m.managementUrl).toBe("/admin");
104
+ });
105
+
106
+ test("managementUrl accepts an absolute https URL", () => {
107
+ const m = validateModuleManifest(
108
+ { ...VALID, managementUrl: "https://admin.example.com/" },
109
+ "x",
110
+ );
111
+ expect(m.managementUrl).toBe("https://admin.example.com/");
112
+ });
113
+
114
+ test("managementUrl rejects empty / non-string / non-url-or-path", () => {
115
+ expect(() => validateModuleManifest({ ...VALID, managementUrl: "" }, "x")).toThrow(
116
+ /managementUrl/,
117
+ );
118
+ expect(() => validateModuleManifest({ ...VALID, managementUrl: 7 }, "x")).toThrow(
119
+ /managementUrl/,
120
+ );
121
+ expect(() =>
122
+ validateModuleManifest({ ...VALID, managementUrl: "no-leading-slash" }, "x"),
123
+ ).toThrow(/path starting with "\/" or a full http\(s\) URL/);
124
+ expect(() =>
125
+ validateModuleManifest({ ...VALID, managementUrl: "ftp://example.com" }, "x"),
126
+ ).toThrow(/http:.*https:/);
127
+ });
128
+
129
+ test("managementUrl absent stays absent", () => {
130
+ const m = validateModuleManifest(VALID, "x");
131
+ expect(m.managementUrl).toBeUndefined();
132
+ });
133
+ });
134
+
135
+ describe("readModuleManifest", () => {
136
+ function tmp(): { dir: string; cleanup: () => void } {
137
+ const dir = mkdtempSync(join(tmpdir(), "pcli-manifest-"));
138
+ return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
139
+ }
140
+
141
+ test("returns null when .parachute/module.json is absent", async () => {
142
+ const { dir, cleanup } = tmp();
143
+ try {
144
+ expect(await readModuleManifest(dir)).toBeNull();
145
+ } finally {
146
+ cleanup();
147
+ }
148
+ });
149
+
150
+ test("reads + validates a real on-disk manifest", async () => {
151
+ const { dir, cleanup } = tmp();
152
+ try {
153
+ mkdirSync(join(dir, ".parachute"));
154
+ writeFileSync(join(dir, ".parachute", "module.json"), JSON.stringify(VALID));
155
+ const m = await readModuleManifest(dir);
156
+ expect(m?.name).toBe("demo");
157
+ } finally {
158
+ cleanup();
159
+ }
160
+ });
161
+
162
+ test("throws ModuleManifestError on malformed JSON", async () => {
163
+ const { dir, cleanup } = tmp();
164
+ try {
165
+ mkdirSync(join(dir, ".parachute"));
166
+ writeFileSync(join(dir, ".parachute", "module.json"), "{not json");
167
+ await expect(readModuleManifest(dir)).rejects.toThrow(ModuleManifestError);
168
+ } finally {
169
+ cleanup();
170
+ }
171
+ });
172
+
173
+ test("throws ModuleManifestError on validation failure", async () => {
174
+ const { dir, cleanup } = tmp();
175
+ try {
176
+ mkdirSync(join(dir, ".parachute"));
177
+ writeFileSync(join(dir, ".parachute", "module.json"), JSON.stringify({ name: "x" }));
178
+ await expect(readModuleManifest(dir)).rejects.toThrow(ModuleManifestError);
179
+ } finally {
180
+ cleanup();
181
+ }
182
+ });
183
+ });