@openparachute/hub 0.6.3 → 0.6.4-rc.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/package.json +1 -1
- package/src/__tests__/account-setup.test.ts +609 -0
- package/src/__tests__/account-usage.test.ts +137 -0
- package/src/__tests__/account-vault-admin-token.test.ts +301 -0
- package/src/__tests__/account-vault-token.test.ts +53 -1
- package/src/__tests__/admin-vault-admin-token.test.ts +17 -0
- package/src/__tests__/admin-vaults.test.ts +20 -0
- package/src/__tests__/api-account.test.ts +125 -4
- package/src/__tests__/api-invites.test.ts +180 -0
- package/src/__tests__/api-mint-token.test.ts +259 -10
- package/src/__tests__/api-modules-ops.test.ts +187 -1
- package/src/__tests__/api-modules.test.ts +40 -4
- package/src/__tests__/api-settings-hub-origin.test.ts +13 -8
- package/src/__tests__/auto-wire.test.ts +101 -1
- package/src/__tests__/cli.test.ts +188 -2
- package/src/__tests__/expose-2fa-warning.test.ts +11 -8
- package/src/__tests__/expose-cloudflare.test.ts +5 -4
- package/src/__tests__/expose.test.ts +10 -5
- package/src/__tests__/hub-origin-resolution.test.ts +179 -25
- package/src/__tests__/hub-server.test.ts +628 -13
- package/src/__tests__/hub-unit.test.ts +4 -0
- package/src/__tests__/invites.test.ts +220 -0
- package/src/__tests__/launchctl-guard.test.ts +185 -0
- package/src/__tests__/migrate-cutover.test.ts +32 -0
- package/src/__tests__/module-ops-client.test.ts +68 -0
- package/src/__tests__/scope-explanations.test.ts +16 -0
- package/src/__tests__/serve-boot.test.ts +74 -1
- package/src/__tests__/serve.test.ts +121 -7
- package/src/__tests__/spawn-path.test.ts +191 -0
- package/src/__tests__/status.test.ts +64 -0
- package/src/__tests__/supervisor.test.ts +177 -0
- package/src/__tests__/users.test.ts +27 -0
- package/src/account-home-ui.ts +82 -9
- package/src/account-setup.ts +342 -0
- package/src/account-usage.ts +118 -0
- package/src/account-vault-admin-token.ts +242 -0
- package/src/account-vault-token.ts +27 -2
- package/src/admin-login-ui.ts +94 -0
- package/src/admin-vault-admin-token.ts +8 -2
- package/src/admin-vaults.ts +137 -29
- package/src/api-account.ts +54 -1
- package/src/api-invites.ts +347 -0
- package/src/api-mint-token.ts +81 -0
- package/src/api-modules-ops.ts +168 -53
- package/src/api-modules.ts +36 -0
- package/src/auto-wire.ts +87 -0
- package/src/cli.ts +122 -32
- package/src/commands/expose-2fa-warning.ts +17 -13
- package/src/commands/migrate-cutover.ts +12 -5
- package/src/commands/serve-boot.ts +33 -3
- package/src/commands/serve.ts +158 -37
- package/src/commands/status.ts +9 -1
- package/src/hub-db.ts +70 -2
- package/src/hub-server.ts +399 -41
- package/src/hub-unit.ts +4 -9
- package/src/invites.ts +291 -0
- package/src/launchctl-guard.ts +131 -0
- package/src/managed-unit.ts +13 -3
- package/src/migrate-offer.ts +15 -6
- package/src/module-ops-client.ts +47 -22
- package/src/scope-attenuation.ts +19 -0
- package/src/scope-explanations.ts +9 -1
- package/src/service-spec.ts +8 -3
- package/src/spawn-path.ts +148 -0
- package/src/supervisor.ts +84 -7
- package/src/users.ts +42 -4
- package/src/vault-hub-origin-env.ts +28 -0
- package/src/vault-name.ts +13 -1
- package/web/ui/dist/assets/{index-mz8XcVPP.css → index-BYYUeLGA.css} +1 -1
- package/web/ui/dist/assets/index-D3cDUOOj.js +61 -0
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-D_0TRjeo.js +0 -61
|
@@ -87,13 +87,16 @@ describe("printPublic2FAWarning", () => {
|
|
|
87
87
|
});
|
|
88
88
|
expect(fired).toBe(true);
|
|
89
89
|
const joined = logs.join("\n");
|
|
90
|
-
// hub#473: real hub-login 2FA. The
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
90
|
+
// hub#473: real hub-login 2FA. The recommendation now leads with the
|
|
91
|
+
// friendly "strongly recommended" framing, points at both the /account/2fa
|
|
92
|
+
// browser path and the `parachute auth 2fa enroll` CLI path, makes clear
|
|
93
|
+
// it's not a requirement, and still nudges a strong owner password.
|
|
94
|
+
expect(joined).toContain("Strongly recommended: turn on two-factor authentication");
|
|
95
|
+
expect(joined).toContain("reachable from the public internet");
|
|
94
96
|
expect(joined).toContain("https://vault.example.com/login");
|
|
97
|
+
expect(joined).toContain("https://vault.example.com/account/2fa");
|
|
95
98
|
expect(joined).toContain("parachute auth 2fa enroll");
|
|
96
|
-
expect(joined).toContain("
|
|
99
|
+
expect(joined).toContain("It's a recommendation, not a requirement");
|
|
97
100
|
expect(joined).toContain("parachute auth set-password");
|
|
98
101
|
});
|
|
99
102
|
|
|
@@ -120,9 +123,9 @@ describe("printPublic2FAWarning", () => {
|
|
|
120
123
|
publicUrl: "https://vault.example.com",
|
|
121
124
|
});
|
|
122
125
|
expect(fired).toBe(true);
|
|
123
|
-
expect(
|
|
124
|
-
|
|
125
|
-
);
|
|
126
|
+
expect(
|
|
127
|
+
logs.some((l) => l.includes("Strongly recommended: turn on two-factor authentication")),
|
|
128
|
+
).toBe(true);
|
|
126
129
|
});
|
|
127
130
|
|
|
128
131
|
test("embeds the supplied publicUrl into the /login pointer", () => {
|
|
@@ -1230,9 +1230,10 @@ describe("exposeCloudflareUp", () => {
|
|
|
1230
1230
|
|
|
1231
1231
|
expect(code).toBe(0);
|
|
1232
1232
|
const joined = logs.join("\n");
|
|
1233
|
-
// hub#473: real hub-login 2FA — the
|
|
1234
|
-
//
|
|
1235
|
-
|
|
1233
|
+
// hub#473: real hub-login 2FA — the recommendation now leads with the
|
|
1234
|
+
// friendly "strongly recommended" framing and the real `parachute auth
|
|
1235
|
+
// 2fa enroll` / `/account/2fa` paths.
|
|
1236
|
+
expect(joined).toContain("Strongly recommended: turn on two-factor authentication");
|
|
1236
1237
|
expect(joined).toContain("https://vault.example.com/login");
|
|
1237
1238
|
expect(joined).toContain("parachute auth 2fa enroll");
|
|
1238
1239
|
} finally {
|
|
@@ -1281,7 +1282,7 @@ describe("exposeCloudflareUp", () => {
|
|
|
1281
1282
|
|
|
1282
1283
|
expect(code).toBe(0);
|
|
1283
1284
|
const joined = logs.join("\n");
|
|
1284
|
-
expect(joined).not.toContain("
|
|
1285
|
+
expect(joined).not.toContain("Strongly recommended: turn on two-factor authentication");
|
|
1285
1286
|
// The contextual 2FA warning is suppressed (2FA already enrolled); the
|
|
1286
1287
|
// always-shown owner-password guidance from `printAuthGuidance` still
|
|
1287
1288
|
// appears, and it now (hub#473) also surfaces the real `2fa enroll`
|
|
@@ -558,7 +558,9 @@ describe("expose tailnet up", () => {
|
|
|
558
558
|
},
|
|
559
559
|
});
|
|
560
560
|
expect(code).toBe(0);
|
|
561
|
-
expect(logs.join("\n")).not.toContain(
|
|
561
|
+
expect(logs.join("\n")).not.toContain(
|
|
562
|
+
"Strongly recommended: turn on two-factor authentication",
|
|
563
|
+
);
|
|
562
564
|
} finally {
|
|
563
565
|
h.cleanup();
|
|
564
566
|
}
|
|
@@ -931,9 +933,10 @@ describe("expose public up", () => {
|
|
|
931
933
|
});
|
|
932
934
|
expect(code).toBe(0);
|
|
933
935
|
const joined = logs.join("\n");
|
|
934
|
-
// hub#473: real hub-login 2FA. The
|
|
935
|
-
//
|
|
936
|
-
|
|
936
|
+
// hub#473: real hub-login 2FA. The recommendation now leads with the
|
|
937
|
+
// friendly "strongly recommended" framing and the real `parachute auth
|
|
938
|
+
// 2fa enroll` path.
|
|
939
|
+
expect(joined).toContain("Strongly recommended: turn on two-factor authentication");
|
|
937
940
|
expect(joined).toContain("parachute auth 2fa enroll");
|
|
938
941
|
// /login pointer uses the canonical https://<fqdn> origin.
|
|
939
942
|
expect(joined).toContain("https://parachute.taildf9ce2.ts.net/login");
|
|
@@ -966,7 +969,9 @@ describe("expose public up", () => {
|
|
|
966
969
|
},
|
|
967
970
|
});
|
|
968
971
|
expect(code).toBe(0);
|
|
969
|
-
expect(logs.join("\n")).not.toContain(
|
|
972
|
+
expect(logs.join("\n")).not.toContain(
|
|
973
|
+
"Strongly recommended: turn on two-factor authentication",
|
|
974
|
+
);
|
|
970
975
|
} finally {
|
|
971
976
|
h.cleanup();
|
|
972
977
|
}
|
|
@@ -31,9 +31,20 @@ function req(url: string): Request {
|
|
|
31
31
|
return new Request(url, { method: "GET" });
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Stub the expose-state reader to "no exposure recorded" so these
|
|
36
|
+
* settings/env/request-tier tests are isolated from the host's real
|
|
37
|
+
* `~/.parachute/expose-state.json`. Without this, the default reader picks
|
|
38
|
+
* up a live exposure on the dev box and the expose tier shadows the
|
|
39
|
+
* request-origin fallback these tests assert. (The expose tier itself is
|
|
40
|
+
* exercised in the dedicated describe blocks below with its own injected
|
|
41
|
+
* origins.)
|
|
42
|
+
*/
|
|
43
|
+
const noExpose = (): string | undefined => undefined;
|
|
44
|
+
|
|
34
45
|
describe("resolveIssuer — precedence chain", () => {
|
|
35
46
|
test("falls back to request origin when no settings + no env", () => {
|
|
36
|
-
const got = resolveIssuer(req("http://127.0.0.1:1939/oauth/token"), db, undefined);
|
|
47
|
+
const got = resolveIssuer(req("http://127.0.0.1:1939/oauth/token"), db, undefined, noExpose);
|
|
37
48
|
expect(got).toBe("http://127.0.0.1:1939");
|
|
38
49
|
});
|
|
39
50
|
|
|
@@ -42,6 +53,7 @@ describe("resolveIssuer — precedence chain", () => {
|
|
|
42
53
|
req("http://127.0.0.1:1939/oauth/token"),
|
|
43
54
|
db,
|
|
44
55
|
"https://hub.from-env.example",
|
|
56
|
+
noExpose,
|
|
45
57
|
);
|
|
46
58
|
expect(got).toBe("https://hub.from-env.example");
|
|
47
59
|
});
|
|
@@ -52,13 +64,14 @@ describe("resolveIssuer — precedence chain", () => {
|
|
|
52
64
|
req("http://127.0.0.1:1939/oauth/token"),
|
|
53
65
|
db,
|
|
54
66
|
"https://hub.from-env.example",
|
|
67
|
+
noExpose,
|
|
55
68
|
);
|
|
56
69
|
expect(got).toBe("https://hub.from-settings.example");
|
|
57
70
|
});
|
|
58
71
|
|
|
59
72
|
test("hub_settings wins over request origin (no env)", () => {
|
|
60
73
|
setHubOrigin(db, "https://hub.from-settings.example");
|
|
61
|
-
const got = resolveIssuer(req("http://127.0.0.1:1939/oauth/token"), db, undefined);
|
|
74
|
+
const got = resolveIssuer(req("http://127.0.0.1:1939/oauth/token"), db, undefined, noExpose);
|
|
62
75
|
expect(got).toBe("https://hub.from-settings.example");
|
|
63
76
|
});
|
|
64
77
|
|
|
@@ -69,6 +82,7 @@ describe("resolveIssuer — precedence chain", () => {
|
|
|
69
82
|
req("http://127.0.0.1:1939/oauth/token"),
|
|
70
83
|
db,
|
|
71
84
|
"https://hub.from-env.example",
|
|
85
|
+
noExpose,
|
|
72
86
|
);
|
|
73
87
|
expect(got).toBe("https://hub.from-env.example");
|
|
74
88
|
});
|
|
@@ -76,7 +90,7 @@ describe("resolveIssuer — precedence chain", () => {
|
|
|
76
90
|
test("clearing hub_settings + no env reverts to request origin", () => {
|
|
77
91
|
setHubOrigin(db, "https://hub.from-settings.example");
|
|
78
92
|
setHubOrigin(db, null);
|
|
79
|
-
const got = resolveIssuer(req("http://127.0.0.1:1939/oauth/token"), db, undefined);
|
|
93
|
+
const got = resolveIssuer(req("http://127.0.0.1:1939/oauth/token"), db, undefined, noExpose);
|
|
80
94
|
expect(got).toBe("http://127.0.0.1:1939");
|
|
81
95
|
});
|
|
82
96
|
|
|
@@ -84,13 +98,14 @@ describe("resolveIssuer — precedence chain", () => {
|
|
|
84
98
|
// The wellknown / discovery surfaces may hit oauthDeps before a DB
|
|
85
99
|
// is wired; resolveIssuer must not throw — just skip the settings
|
|
86
100
|
// layer.
|
|
87
|
-
const got = resolveIssuer(req("http://127.0.0.1:1939/"), undefined, undefined);
|
|
101
|
+
const got = resolveIssuer(req("http://127.0.0.1:1939/"), undefined, undefined, noExpose);
|
|
88
102
|
expect(got).toBe("http://127.0.0.1:1939");
|
|
89
103
|
|
|
90
104
|
const gotEnv = resolveIssuer(
|
|
91
105
|
req("http://127.0.0.1:1939/"),
|
|
92
106
|
undefined,
|
|
93
107
|
"https://hub.from-env.example",
|
|
108
|
+
noExpose,
|
|
94
109
|
);
|
|
95
110
|
expect(gotEnv).toBe("https://hub.from-env.example");
|
|
96
111
|
});
|
|
@@ -103,19 +118,19 @@ describe("resolveIssuer — precedence chain", () => {
|
|
|
103
118
|
const baseUrl = "http://127.0.0.1:1939/oauth/token";
|
|
104
119
|
|
|
105
120
|
// Pass 1 — no settings, no env → request origin.
|
|
106
|
-
expect(resolveIssuer(req(baseUrl), db, undefined)).toBe("http://127.0.0.1:1939");
|
|
121
|
+
expect(resolveIssuer(req(baseUrl), db, undefined, noExpose)).toBe("http://127.0.0.1:1939");
|
|
107
122
|
|
|
108
123
|
// Mid-flight write.
|
|
109
124
|
setHubOrigin(db, "https://hub.example.com");
|
|
110
125
|
|
|
111
126
|
// Pass 2 — settings wins immediately.
|
|
112
|
-
expect(resolveIssuer(req(baseUrl), db, undefined)).toBe("https://hub.example.com");
|
|
127
|
+
expect(resolveIssuer(req(baseUrl), db, undefined, noExpose)).toBe("https://hub.example.com");
|
|
113
128
|
|
|
114
129
|
// Mid-flight clear.
|
|
115
130
|
setHubOrigin(db, null);
|
|
116
131
|
|
|
117
132
|
// Pass 3 — back to request origin.
|
|
118
|
-
expect(resolveIssuer(req(baseUrl), db, undefined)).toBe("http://127.0.0.1:1939");
|
|
133
|
+
expect(resolveIssuer(req(baseUrl), db, undefined, noExpose)).toBe("http://127.0.0.1:1939");
|
|
119
134
|
});
|
|
120
135
|
|
|
121
136
|
test("X-Forwarded-Proto: https upgrades the request-origin fallback", () => {
|
|
@@ -124,11 +139,14 @@ describe("resolveIssuer — precedence chain", () => {
|
|
|
124
139
|
// `http://...` in OAuth discovery — mixed-content blocked when the
|
|
125
140
|
// page loaded over https://. See hub#355 (the notes app's
|
|
126
141
|
// /oauth/register call surfaced this).
|
|
127
|
-
const r = new Request(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
const r = new Request(
|
|
143
|
+
"http://parachute-hub.onrender.com/.well-known/oauth-authorization-server",
|
|
144
|
+
{
|
|
145
|
+
method: "GET",
|
|
146
|
+
headers: { "X-Forwarded-Proto": "https" },
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
expect(resolveIssuer(r, db, undefined, noExpose)).toBe("https://parachute-hub.onrender.com");
|
|
132
150
|
});
|
|
133
151
|
|
|
134
152
|
test("X-Forwarded-Proto with comma-separated values takes the first", () => {
|
|
@@ -138,14 +156,14 @@ describe("resolveIssuer — precedence chain", () => {
|
|
|
138
156
|
method: "GET",
|
|
139
157
|
headers: { "X-Forwarded-Proto": "https, http" },
|
|
140
158
|
});
|
|
141
|
-
expect(resolveIssuer(r, db, undefined)).toBe("https://hub.internal");
|
|
159
|
+
expect(resolveIssuer(r, db, undefined, noExpose)).toBe("https://hub.internal");
|
|
142
160
|
});
|
|
143
161
|
|
|
144
162
|
test("missing X-Forwarded-Proto leaves the URL scheme as-is (localhost dev)", () => {
|
|
145
163
|
// No reverse proxy → no header → keep http for the local-dev shape.
|
|
146
164
|
// Operators on plain HTTP localhost depend on this.
|
|
147
165
|
const r = new Request("http://127.0.0.1:1939/oauth/token", { method: "GET" });
|
|
148
|
-
expect(resolveIssuer(r, db, undefined)).toBe("http://127.0.0.1:1939");
|
|
166
|
+
expect(resolveIssuer(r, db, undefined, noExpose)).toBe("http://127.0.0.1:1939");
|
|
149
167
|
});
|
|
150
168
|
|
|
151
169
|
test("X-Forwarded-Proto is IGNORED when hub_settings or env wins", () => {
|
|
@@ -161,26 +179,28 @@ describe("resolveIssuer — precedence chain", () => {
|
|
|
161
179
|
|
|
162
180
|
// Env layer wins, even though the header says https — the env value
|
|
163
181
|
// is returned verbatim (preserving whatever scheme the operator set).
|
|
164
|
-
expect(resolveIssuer(r, db, "http://configured.example")).toBe(
|
|
182
|
+
expect(resolveIssuer(r, db, "http://configured.example", noExpose)).toBe(
|
|
183
|
+
"http://configured.example",
|
|
184
|
+
);
|
|
165
185
|
|
|
166
186
|
// Settings layer wins above env, also verbatim.
|
|
167
187
|
setHubOrigin(db, "http://settings.example");
|
|
168
|
-
expect(resolveIssuer(r, db, "https://env.example")).toBe("http://settings.example");
|
|
188
|
+
expect(resolveIssuer(r, db, "https://env.example", noExpose)).toBe("http://settings.example");
|
|
169
189
|
});
|
|
170
190
|
});
|
|
171
191
|
|
|
172
192
|
describe("resolveIssuerSource — attribution for SPA", () => {
|
|
173
193
|
test('"request" when nothing is configured', () => {
|
|
174
|
-
expect(resolveIssuerSource(db, undefined)).toBe("request");
|
|
194
|
+
expect(resolveIssuerSource(db, undefined, noExpose)).toBe("request");
|
|
175
195
|
});
|
|
176
196
|
|
|
177
197
|
test('"env" when configuredIssuer is set + no settings row', () => {
|
|
178
|
-
expect(resolveIssuerSource(db, "https://hub.from-env.example")).toBe("env");
|
|
198
|
+
expect(resolveIssuerSource(db, "https://hub.from-env.example", noExpose)).toBe("env");
|
|
179
199
|
});
|
|
180
200
|
|
|
181
201
|
test('"settings" when hub_settings row is set, even if env is also set', () => {
|
|
182
202
|
setHubOrigin(db, "https://hub.from-settings.example");
|
|
183
|
-
expect(resolveIssuerSource(db, "https://hub.from-env.example")).toBe("settings");
|
|
203
|
+
expect(resolveIssuerSource(db, "https://hub.from-env.example", noExpose)).toBe("settings");
|
|
184
204
|
});
|
|
185
205
|
|
|
186
206
|
test("attribution matches resolved value across the chain", () => {
|
|
@@ -189,16 +209,150 @@ describe("resolveIssuerSource — attribution for SPA", () => {
|
|
|
189
209
|
// settings layer is what got returned.
|
|
190
210
|
setHubOrigin(db, "https://hub.example.com");
|
|
191
211
|
const r1 = req("http://127.0.0.1:1939/oauth/token");
|
|
192
|
-
expect(resolveIssuer(r1, db, "https://hub.from-env.example")).toBe(
|
|
193
|
-
|
|
212
|
+
expect(resolveIssuer(r1, db, "https://hub.from-env.example", noExpose)).toBe(
|
|
213
|
+
"https://hub.example.com",
|
|
214
|
+
);
|
|
215
|
+
expect(resolveIssuerSource(db, "https://hub.from-env.example", noExpose)).toBe("settings");
|
|
194
216
|
|
|
195
217
|
setHubOrigin(db, null);
|
|
196
|
-
expect(resolveIssuer(r1, db, "https://hub.from-env.example")).toBe(
|
|
218
|
+
expect(resolveIssuer(r1, db, "https://hub.from-env.example", noExpose)).toBe(
|
|
197
219
|
"https://hub.from-env.example",
|
|
198
220
|
);
|
|
199
|
-
expect(resolveIssuerSource(db, "https://hub.from-env.example")).toBe("env");
|
|
221
|
+
expect(resolveIssuerSource(db, "https://hub.from-env.example", noExpose)).toBe("env");
|
|
222
|
+
|
|
223
|
+
expect(resolveIssuer(r1, db, undefined, noExpose)).toBe("http://127.0.0.1:1939");
|
|
224
|
+
expect(resolveIssuerSource(db, undefined, noExpose)).toBe("request");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The expose-state tier (#531). On the reboot-persistent owner-operated
|
|
230
|
+
* path the launchd plist / systemd unit carries no PARACHUTE_HUB_ORIGIN, so
|
|
231
|
+
* the hub boots with no `configuredIssuer`. Without this tier it would stamp
|
|
232
|
+
* `iss` from the per-request origin (loopback) and exposed resource servers
|
|
233
|
+
* (vault) reject the token with `unexpected "iss" claim value`. The exposed
|
|
234
|
+
* origin recorded in expose-state.json's hubOrigin is consulted between the
|
|
235
|
+
* env tier and the request-origin fallback. The `readExpose` seam (4th /
|
|
236
|
+
* 3rd param) drives this without touching the real ~/.parachute.
|
|
237
|
+
*/
|
|
238
|
+
describe("resolveIssuer — expose-state tier (#531)", () => {
|
|
239
|
+
const EXPOSED = "https://parachute.taildf9ce2.ts.net";
|
|
240
|
+
// Simulates the reported bug: token minted under loopback, request arrives
|
|
241
|
+
// at loopback, but the canonical exposed origin lives in expose-state.
|
|
242
|
+
const loopbackReq = () => req("http://127.0.0.1:1939/oauth/token");
|
|
243
|
+
|
|
244
|
+
test("REGRESSION: expose origin used (NOT request origin) when settings+env both absent", () => {
|
|
245
|
+
const got = resolveIssuer(loopbackReq(), db, undefined, () => EXPOSED);
|
|
246
|
+
expect(got).toBe(EXPOSED);
|
|
247
|
+
expect(got).not.toBe("http://127.0.0.1:1939");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("settings wins over expose", () => {
|
|
251
|
+
setHubOrigin(db, "https://hub.from-settings.example");
|
|
252
|
+
const got = resolveIssuer(loopbackReq(), db, undefined, () => EXPOSED);
|
|
253
|
+
expect(got).toBe("https://hub.from-settings.example");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("env wins over expose", () => {
|
|
257
|
+
const got = resolveIssuer(loopbackReq(), db, "https://hub.from-env.example", () => EXPOSED);
|
|
258
|
+
expect(got).toBe("https://hub.from-env.example");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("expose wins over request origin", () => {
|
|
262
|
+
// settings + env both absent → expose beats the per-request loopback origin.
|
|
263
|
+
const got = resolveIssuer(loopbackReq(), db, undefined, () => EXPOSED);
|
|
264
|
+
expect(got).toBe(EXPOSED);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("full precedence: settings > env > expose > request", () => {
|
|
268
|
+
// request-only
|
|
269
|
+
expect(resolveIssuer(loopbackReq(), db, undefined, () => undefined)).toBe(
|
|
270
|
+
"http://127.0.0.1:1939",
|
|
271
|
+
);
|
|
272
|
+
// expose beats request
|
|
273
|
+
expect(resolveIssuer(loopbackReq(), db, undefined, () => EXPOSED)).toBe(EXPOSED);
|
|
274
|
+
// env beats expose
|
|
275
|
+
expect(resolveIssuer(loopbackReq(), db, "https://env.example", () => EXPOSED)).toBe(
|
|
276
|
+
"https://env.example",
|
|
277
|
+
);
|
|
278
|
+
// settings beats env (and expose)
|
|
279
|
+
setHubOrigin(db, "https://settings.example");
|
|
280
|
+
expect(resolveIssuer(loopbackReq(), db, "https://env.example", () => EXPOSED)).toBe(
|
|
281
|
+
"https://settings.example",
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("malformed expose-state falls through to request without throwing", () => {
|
|
286
|
+
// A reader that throws simulates a corrupt expose-state.json. The
|
|
287
|
+
// `exposeIssuerOrigin` wrapper guards the `readExpose()` call itself in
|
|
288
|
+
// try/catch, so even an injected non-swallowing reader can NEVER
|
|
289
|
+
// propagate into the request path — resolveIssuer falls through to the
|
|
290
|
+
// request origin instead of 500ing the hub.
|
|
291
|
+
const throwing = () => {
|
|
292
|
+
throw new Error("malformed expose-state.json");
|
|
293
|
+
};
|
|
294
|
+
expect(() => resolveIssuer(loopbackReq(), db, undefined, throwing)).not.toThrow();
|
|
295
|
+
expect(resolveIssuer(loopbackReq(), db, undefined, throwing)).toBe("http://127.0.0.1:1939");
|
|
296
|
+
// A reader that returns undefined (the default's post-swallow shape) also
|
|
297
|
+
// yields the request origin.
|
|
298
|
+
expect(resolveIssuer(loopbackReq(), db, undefined, () => undefined)).toBe(
|
|
299
|
+
"http://127.0.0.1:1939",
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("loopback expose origin ignored (never re-pin the degraded mode)", () => {
|
|
304
|
+
expect(resolveIssuer(loopbackReq(), db, undefined, () => "http://127.0.0.1:1939")).toBe(
|
|
305
|
+
"http://127.0.0.1:1939",
|
|
306
|
+
);
|
|
307
|
+
expect(resolveIssuer(loopbackReq(), db, undefined, () => "http://localhost:1939")).toBe(
|
|
308
|
+
"http://127.0.0.1:1939",
|
|
309
|
+
);
|
|
310
|
+
expect(resolveIssuer(loopbackReq(), db, undefined, () => "http://0.0.0.0:1939")).toBe(
|
|
311
|
+
"http://127.0.0.1:1939",
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("non-http(s) / empty expose origin ignored", () => {
|
|
316
|
+
expect(resolveIssuer(loopbackReq(), db, undefined, () => "ftp://x.example")).toBe(
|
|
317
|
+
"http://127.0.0.1:1939",
|
|
318
|
+
);
|
|
319
|
+
expect(resolveIssuer(loopbackReq(), db, undefined, () => "")).toBe("http://127.0.0.1:1939");
|
|
320
|
+
expect(resolveIssuer(loopbackReq(), db, undefined, () => "not-a-url")).toBe(
|
|
321
|
+
"http://127.0.0.1:1939",
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("undefined db (pre-config gate) still consults expose before request", () => {
|
|
326
|
+
const got = resolveIssuer(loopbackReq(), undefined, undefined, () => EXPOSED);
|
|
327
|
+
expect(got).toBe(EXPOSED);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("resolveIssuerSource — expose attribution (#531)", () => {
|
|
332
|
+
const EXPOSED = "https://parachute.taildf9ce2.ts.net";
|
|
333
|
+
|
|
334
|
+
test('"expose" when resolved from expose-state (settings+env absent)', () => {
|
|
335
|
+
expect(resolveIssuerSource(db, undefined, () => EXPOSED)).toBe("expose");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('"settings" wins over expose', () => {
|
|
339
|
+
setHubOrigin(db, "https://settings.example");
|
|
340
|
+
expect(resolveIssuerSource(db, undefined, () => EXPOSED)).toBe("settings");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('"env" wins over expose', () => {
|
|
344
|
+
expect(resolveIssuerSource(db, "https://env.example", () => EXPOSED)).toBe("env");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('"request" when no settings/env and no (valid) expose origin', () => {
|
|
348
|
+
expect(resolveIssuerSource(db, undefined, () => undefined)).toBe("request");
|
|
349
|
+
expect(resolveIssuerSource(db, undefined, () => "http://127.0.0.1:1939")).toBe("request");
|
|
350
|
+
});
|
|
200
351
|
|
|
201
|
-
|
|
202
|
-
|
|
352
|
+
test("attribution matches resolved value for the expose tier", () => {
|
|
353
|
+
// Pair the source label with the resolved value so they can't drift.
|
|
354
|
+
const r = req("http://127.0.0.1:1939/oauth/token");
|
|
355
|
+
expect(resolveIssuer(r, db, undefined, () => EXPOSED)).toBe(EXPOSED);
|
|
356
|
+
expect(resolveIssuerSource(db, undefined, () => EXPOSED)).toBe("expose");
|
|
203
357
|
});
|
|
204
358
|
});
|