@openparachute/hub 0.3.0-rc.1 → 0.5.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 +19 -17
- package/package.json +15 -4
- package/src/__tests__/admin-auth.test.ts +197 -0
- package/src/__tests__/admin-config.test.ts +281 -0
- package/src/__tests__/admin-grants.test.ts +271 -0
- package/src/__tests__/admin-handlers.test.ts +530 -0
- package/src/__tests__/admin-host-admin-token.test.ts +115 -0
- package/src/__tests__/admin-vault-admin-token.test.ts +190 -0
- package/src/__tests__/admin-vaults.test.ts +615 -0
- package/src/__tests__/auth-codes.test.ts +253 -0
- package/src/__tests__/auth.test.ts +712 -17
- package/src/__tests__/cli.test.ts +50 -0
- package/src/__tests__/clients.test.ts +264 -0
- package/src/__tests__/cloudflare-state.test.ts +167 -7
- package/src/__tests__/csrf.test.ts +117 -0
- package/src/__tests__/expose-cloudflare.test.ts +232 -37
- package/src/__tests__/expose-off-auto.test.ts +15 -9
- package/src/__tests__/expose-public-auto.test.ts +153 -0
- package/src/__tests__/expose.test.ts +216 -24
- package/src/__tests__/grants.test.ts +164 -0
- package/src/__tests__/hub-db.test.ts +153 -0
- package/src/__tests__/hub-server.test.ts +984 -26
- package/src/__tests__/hub.test.ts +56 -49
- package/src/__tests__/install.test.ts +327 -3
- package/src/__tests__/jwks.test.ts +37 -0
- package/src/__tests__/jwt-sign.test.ts +361 -0
- package/src/__tests__/lifecycle.test.ts +519 -5
- package/src/__tests__/module-manifest.test.ts +183 -0
- package/src/__tests__/oauth-handlers.test.ts +3112 -0
- package/src/__tests__/oauth-ui.test.ts +253 -0
- package/src/__tests__/operator-token.test.ts +140 -0
- package/src/__tests__/providers-detect.test.ts +158 -0
- package/src/__tests__/scope-explanations.test.ts +108 -0
- package/src/__tests__/scope-registry.test.ts +220 -0
- package/src/__tests__/services-manifest.test.ts +137 -1
- package/src/__tests__/sessions.test.ts +116 -0
- package/src/__tests__/setup.test.ts +361 -0
- package/src/__tests__/signing-keys.test.ts +153 -0
- package/src/__tests__/upgrade.test.ts +541 -0
- package/src/__tests__/users.test.ts +154 -0
- package/src/__tests__/well-known.test.ts +127 -10
- package/src/admin-auth.ts +126 -0
- package/src/admin-config-ui.ts +534 -0
- package/src/admin-config.ts +226 -0
- package/src/admin-grants.ts +160 -0
- package/src/admin-handlers.ts +365 -0
- package/src/admin-host-admin-token.ts +83 -0
- package/src/admin-vault-admin-token.ts +98 -0
- package/src/admin-vaults.ts +359 -0
- package/src/auth-codes.ts +189 -0
- package/src/cli.ts +202 -25
- package/src/clients.ts +210 -0
- package/src/cloudflare/config.ts +25 -6
- package/src/cloudflare/state.ts +108 -28
- package/src/commands/auth.ts +652 -19
- package/src/commands/expose-cloudflare.ts +85 -45
- package/src/commands/expose-interactive.ts +20 -44
- package/src/commands/expose-off-auto.ts +27 -11
- package/src/commands/expose-public-auto.ts +179 -0
- package/src/commands/expose.ts +63 -32
- package/src/commands/install.ts +337 -48
- package/src/commands/lifecycle.ts +242 -37
- package/src/commands/setup.ts +366 -0
- package/src/commands/status.ts +4 -1
- package/src/commands/upgrade.ts +429 -0
- package/src/csrf.ts +101 -0
- package/src/grants.ts +142 -0
- package/src/help.ts +133 -19
- package/src/hub-control.ts +12 -0
- package/src/hub-db.ts +164 -0
- package/src/hub-server.ts +643 -22
- package/src/hub.ts +97 -390
- package/src/jwks.ts +41 -0
- package/src/jwt-sign.ts +275 -0
- package/src/module-manifest.ts +435 -0
- package/src/oauth-handlers.ts +1206 -0
- package/src/oauth-ui.ts +582 -0
- package/src/operator-token.ts +129 -0
- package/src/providers/detect.ts +97 -0
- package/src/scope-explanations.ts +137 -0
- package/src/scope-registry.ts +158 -0
- package/src/service-spec.ts +270 -97
- package/src/services-manifest.ts +57 -1
- package/src/sessions.ts +115 -0
- package/src/signing-keys.ts +120 -0
- package/src/users.ts +144 -0
- package/src/well-known.ts +62 -26
- package/web/ui/dist/assets/index-BKzPDdB0.js +60 -0
- package/web/ui/dist/assets/index-Dyk6g7vT.css +1 -0
- package/web/ui/dist/index.html +14 -0
package/src/cloudflare/state.ts
CHANGED
|
@@ -11,8 +11,12 @@ import { CONFIG_DIR } from "../config.ts";
|
|
|
11
11
|
|
|
12
12
|
export const CLOUDFLARED_STATE_PATH = join(CONFIG_DIR, "cloudflared-state.json");
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Per-tunnel state. The fields that used to live at the top of the file in
|
|
16
|
+
* v1 (#32) — pid, hostname, etc. — now hang off a per-tunnel record so we
|
|
17
|
+
* can track multiple coexisting Cloudflare tunnels on one box.
|
|
18
|
+
*/
|
|
19
|
+
export interface CloudflaredTunnelRecord {
|
|
16
20
|
pid: number;
|
|
17
21
|
tunnelUuid: string;
|
|
18
22
|
tunnelName: string;
|
|
@@ -23,45 +27,81 @@ export interface CloudflaredState {
|
|
|
23
27
|
configPath: string;
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
/**
|
|
31
|
+
* v2 (current) — keys tunnels by name so a host can run multiple tunnels.
|
|
32
|
+
*
|
|
33
|
+
* v1 had a single record at top level. `readCloudflaredState` migrates v1
|
|
34
|
+
* files in place: parses the legacy shape, wraps the record under its
|
|
35
|
+
* `tunnelName`, returns v2. The next write commits the migration to disk.
|
|
36
|
+
*/
|
|
37
|
+
export interface CloudflaredState {
|
|
38
|
+
version: 2;
|
|
39
|
+
tunnels: Record<string, CloudflaredTunnelRecord>;
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
export class CloudflaredStateError extends Error {
|
|
27
43
|
override name = "CloudflaredStateError";
|
|
28
44
|
}
|
|
29
45
|
|
|
46
|
+
function requireString(r: Record<string, unknown>, key: string, path: string): string {
|
|
47
|
+
const v = r[key];
|
|
48
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
49
|
+
throw new CloudflaredStateError(`${path}: ${key} must be a non-empty string`);
|
|
50
|
+
}
|
|
51
|
+
return v;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function requirePositiveInt(r: Record<string, unknown>, key: string, path: string): number {
|
|
55
|
+
const v = r[key];
|
|
56
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v <= 0) {
|
|
57
|
+
throw new CloudflaredStateError(`${path}: ${key} must be a positive integer`);
|
|
58
|
+
}
|
|
59
|
+
return v;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function validateRecord(raw: unknown, path: string): CloudflaredTunnelRecord {
|
|
63
|
+
if (!raw || typeof raw !== "object") {
|
|
64
|
+
throw new CloudflaredStateError(`${path}: tunnel record must be an object`);
|
|
65
|
+
}
|
|
66
|
+
const r = raw as Record<string, unknown>;
|
|
67
|
+
return {
|
|
68
|
+
pid: requirePositiveInt(r, "pid", path),
|
|
69
|
+
tunnelUuid: requireString(r, "tunnelUuid", path),
|
|
70
|
+
tunnelName: requireString(r, "tunnelName", path),
|
|
71
|
+
hostname: requireString(r, "hostname", path),
|
|
72
|
+
startedAt: requireString(r, "startedAt", path),
|
|
73
|
+
configPath: requireString(r, "configPath", path),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
30
77
|
function validate(raw: unknown, path: string): CloudflaredState {
|
|
31
78
|
if (!raw || typeof raw !== "object") {
|
|
32
79
|
throw new CloudflaredStateError(`${path}: root must be an object`);
|
|
33
80
|
}
|
|
34
81
|
const r = raw as Record<string, unknown>;
|
|
35
|
-
if (r.version
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
if (typeof r.tunnelUuid !== "string" || r.tunnelUuid.length === 0) {
|
|
42
|
-
throw new CloudflaredStateError(`${path}: tunnelUuid must be a non-empty string`);
|
|
43
|
-
}
|
|
44
|
-
if (typeof r.tunnelName !== "string" || r.tunnelName.length === 0) {
|
|
45
|
-
throw new CloudflaredStateError(`${path}: tunnelName must be a non-empty string`);
|
|
82
|
+
if (r.version === 1) {
|
|
83
|
+
// v1 — single record at top level. Migrate by wrapping it under its
|
|
84
|
+
// tunnelName. Disk isn't rewritten until the next write.
|
|
85
|
+
const record = validateRecord(r, path);
|
|
86
|
+
return { version: 2, tunnels: { [record.tunnelName]: record } };
|
|
46
87
|
}
|
|
47
|
-
if (
|
|
48
|
-
throw new CloudflaredStateError(`${path}:
|
|
88
|
+
if (r.version !== 2) {
|
|
89
|
+
throw new CloudflaredStateError(`${path}: unsupported version ${String(r.version)}`);
|
|
49
90
|
}
|
|
50
|
-
if (typeof r.
|
|
51
|
-
throw new CloudflaredStateError(`${path}:
|
|
91
|
+
if (!r.tunnels || typeof r.tunnels !== "object") {
|
|
92
|
+
throw new CloudflaredStateError(`${path}: tunnels must be an object`);
|
|
52
93
|
}
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
const tunnels: Record<string, CloudflaredTunnelRecord> = {};
|
|
95
|
+
for (const [key, val] of Object.entries(r.tunnels as Record<string, unknown>)) {
|
|
96
|
+
const record = validateRecord(val, `${path}.tunnels.${key}`);
|
|
97
|
+
if (record.tunnelName !== key) {
|
|
98
|
+
throw new CloudflaredStateError(
|
|
99
|
+
`${path}: tunnels.${key}.tunnelName must equal its key (got "${record.tunnelName}")`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
tunnels[key] = record;
|
|
55
103
|
}
|
|
56
|
-
return {
|
|
57
|
-
version: 1,
|
|
58
|
-
pid: r.pid,
|
|
59
|
-
tunnelUuid: r.tunnelUuid,
|
|
60
|
-
tunnelName: r.tunnelName,
|
|
61
|
-
hostname: r.hostname,
|
|
62
|
-
startedAt: r.startedAt,
|
|
63
|
-
configPath: r.configPath,
|
|
64
|
-
};
|
|
104
|
+
return { version: 2, tunnels };
|
|
65
105
|
}
|
|
66
106
|
|
|
67
107
|
export function readCloudflaredState(
|
|
@@ -94,3 +134,43 @@ export function writeCloudflaredState(
|
|
|
94
134
|
export function clearCloudflaredState(path: string = CLOUDFLARED_STATE_PATH): void {
|
|
95
135
|
if (existsSync(path)) unlinkSync(path);
|
|
96
136
|
}
|
|
137
|
+
|
|
138
|
+
/** Look up a tunnel record by name. */
|
|
139
|
+
export function findTunnelRecord(
|
|
140
|
+
state: CloudflaredState | undefined,
|
|
141
|
+
tunnelName: string,
|
|
142
|
+
): CloudflaredTunnelRecord | undefined {
|
|
143
|
+
return state?.tunnels[tunnelName];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Pure: insert/replace the record in state under its tunnelName. */
|
|
147
|
+
export function withTunnelRecord(
|
|
148
|
+
state: CloudflaredState | undefined,
|
|
149
|
+
record: CloudflaredTunnelRecord,
|
|
150
|
+
): CloudflaredState {
|
|
151
|
+
const tunnels = { ...(state?.tunnels ?? {}), [record.tunnelName]: record };
|
|
152
|
+
return { version: 2, tunnels };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Pure: drop the named tunnel from state. Returns undefined when the result
|
|
157
|
+
* would be empty so callers can `clearCloudflaredState` instead of writing
|
|
158
|
+
* an empty file.
|
|
159
|
+
*/
|
|
160
|
+
export function withoutTunnelRecord(
|
|
161
|
+
state: CloudflaredState | undefined,
|
|
162
|
+
tunnelName: string,
|
|
163
|
+
): CloudflaredState | undefined {
|
|
164
|
+
if (!state) return undefined;
|
|
165
|
+
const { [tunnelName]: _dropped, ...rest } = state.tunnels;
|
|
166
|
+
if (Object.keys(rest).length === 0) return undefined;
|
|
167
|
+
return { version: 2, tunnels: rest };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** All tunnel records, in name-sorted order so output is deterministic. */
|
|
171
|
+
export function listTunnelRecords(state: CloudflaredState | undefined): CloudflaredTunnelRecord[] {
|
|
172
|
+
if (!state) return [];
|
|
173
|
+
return Object.keys(state.tunnels)
|
|
174
|
+
.sort()
|
|
175
|
+
.map((k) => state.tunnels[k]) as CloudflaredTunnelRecord[];
|
|
176
|
+
}
|