@rigkit/provider-freestyle 0.2.9 → 0.2.11
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/LICENSE +21 -0
- package/package.json +7 -6
- package/src/host-auth.test.ts +281 -12
- package/src/host-auth.ts +392 -22
- package/src/index.ts +17 -5
- package/src/provider.ts +42 -0
- package/src/version.ts +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rigkit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigkit/provider-freestyle",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
@@ -17,16 +18,16 @@
|
|
|
17
18
|
],
|
|
18
19
|
"dependencies": {
|
|
19
20
|
"zod": "^4",
|
|
20
|
-
"@rigkit/sdk": "0.2.
|
|
21
|
-
"@rigkit/engine": "0.2.
|
|
22
|
-
"@rigkit/provider-cmux": "0.2.
|
|
21
|
+
"@rigkit/sdk": "0.2.11",
|
|
22
|
+
"@rigkit/engine": "0.2.11",
|
|
23
|
+
"@rigkit/provider-cmux": "0.2.11"
|
|
23
24
|
},
|
|
24
25
|
"peerDependencies": {
|
|
25
|
-
"freestyle": "^0.1.
|
|
26
|
+
"freestyle": "^0.1.52"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/bun": "latest",
|
|
29
|
-
"freestyle": "^0.1.
|
|
30
|
+
"freestyle": "^0.1.52",
|
|
30
31
|
"typescript": "latest"
|
|
31
32
|
},
|
|
32
33
|
"publishConfig": {
|
package/src/host-auth.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import type {
|
|
3
3
|
JsonValue,
|
|
4
|
+
ProviderCheckContext,
|
|
4
5
|
ProviderRuntimeContext,
|
|
5
6
|
ProviderStorage,
|
|
6
7
|
ProviderStorageRecord,
|
|
@@ -8,7 +9,7 @@ import type {
|
|
|
8
9
|
WorkflowEvent,
|
|
9
10
|
} from "@rigkit/engine";
|
|
10
11
|
import { FREESTYLE_PROVIDER_ID, freestyle, freestyleProviderPlugin } from "./index.ts";
|
|
11
|
-
import { createFreestyleProxyFetch } from "./host-auth.ts";
|
|
12
|
+
import { createFreestyleProxyFetch, createFreestyleSdkFetch } from "./host-auth.ts";
|
|
12
13
|
import type { FreestyleRuntime } from "./provider.ts";
|
|
13
14
|
import { RIGKIT_PROVIDER_FREESTYLE_VERSION } from "./version.ts";
|
|
14
15
|
|
|
@@ -92,8 +93,8 @@ describe("Freestyle provider host auth", () => {
|
|
|
92
93
|
});
|
|
93
94
|
|
|
94
95
|
expect(projectStorage.entries()).toEqual([]);
|
|
95
|
-
expect(hostStorage.entries("identity:")).toHaveLength(
|
|
96
|
-
expect(requests).toHaveLength(
|
|
96
|
+
expect(hostStorage.entries("identity:")).toHaveLength(0);
|
|
97
|
+
expect(requests).toHaveLength(0);
|
|
97
98
|
|
|
98
99
|
const runtime = await (controller as WorkflowProviderController<FreestyleRuntime>).runtime(providerContext([]));
|
|
99
100
|
|
|
@@ -174,6 +175,8 @@ describe("Freestyle provider host auth", () => {
|
|
|
174
175
|
},
|
|
175
176
|
});
|
|
176
177
|
|
|
178
|
+
await controller.checks?.(providerCheckContext("require"));
|
|
179
|
+
|
|
177
180
|
expect(opened).toEqual([
|
|
178
181
|
"https://dash.freestyle.sh/handler/cli-auth-confirm?login_code=login-code",
|
|
179
182
|
]);
|
|
@@ -222,6 +225,147 @@ describe("Freestyle provider host auth", () => {
|
|
|
222
225
|
}
|
|
223
226
|
});
|
|
224
227
|
|
|
228
|
+
test("reports a required browser auth check during plan without starting OAuth", async () => {
|
|
229
|
+
delete process.env.FREESTYLE_API_KEY;
|
|
230
|
+
delete process.env.FREESTYLE_TEAM_ID;
|
|
231
|
+
|
|
232
|
+
const projectStorage = new MemoryProviderStorage(FREESTYLE_PROVIDER_ID);
|
|
233
|
+
const hostStorage = new MemoryProviderStorage(FREESTYLE_PROVIDER_ID);
|
|
234
|
+
const previousFetch = globalThis.fetch;
|
|
235
|
+
globalThis.fetch = testFetch(async () =>
|
|
236
|
+
Response.json({ error: "plan should not fetch" }, { status: 500 })
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const controller = await freestyleProviderPlugin.createProvider({
|
|
241
|
+
provider: {
|
|
242
|
+
providerId: FREESTYLE_PROVIDER_ID,
|
|
243
|
+
config: {},
|
|
244
|
+
},
|
|
245
|
+
storage: projectStorage,
|
|
246
|
+
hostStorage,
|
|
247
|
+
local: { open: async () => {} },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(await controller.checks?.(providerCheckContext("plan"))).toEqual([{
|
|
251
|
+
id: "auth",
|
|
252
|
+
label: "Freestyle auth",
|
|
253
|
+
status: "required",
|
|
254
|
+
value: "login required",
|
|
255
|
+
message: "Run rig apply, rig create, or rig run to authenticate with Freestyle.",
|
|
256
|
+
fingerprint: "browser:default:auth:missing",
|
|
257
|
+
}]);
|
|
258
|
+
expect(hostStorage.entries()).toEqual([]);
|
|
259
|
+
} finally {
|
|
260
|
+
globalThis.fetch = previousFetch;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("prompts for and persists a Freestyle team when browser auth has multiple teams", async () => {
|
|
265
|
+
delete process.env.FREESTYLE_API_KEY;
|
|
266
|
+
delete process.env.FREESTYLE_TEAM_ID;
|
|
267
|
+
|
|
268
|
+
const projectStorage = new MemoryProviderStorage(FREESTYLE_PROVIDER_ID);
|
|
269
|
+
const hostStorage = new MemoryProviderStorage(FREESTYLE_PROVIDER_ID);
|
|
270
|
+
const selectPrompts: unknown[] = [];
|
|
271
|
+
const proxyRequests: unknown[] = [];
|
|
272
|
+
const previousFetch = globalThis.fetch;
|
|
273
|
+
globalThis.fetch = testFetch(async (resource, init) => {
|
|
274
|
+
const url = resourceUrl(resource);
|
|
275
|
+
if (url.href === "https://api.stack-auth.com/api/v1/auth/cli") {
|
|
276
|
+
return Response.json({ polling_code: "poll-code", login_code: "login-code" });
|
|
277
|
+
}
|
|
278
|
+
if (url.href === "https://api.stack-auth.com/api/v1/auth/cli/poll") {
|
|
279
|
+
return Response.json({ status: "completed", refresh_token: "refresh-token" });
|
|
280
|
+
}
|
|
281
|
+
if (url.href === "https://api.stack-auth.com/api/v1/auth/sessions/current/refresh") {
|
|
282
|
+
return Response.json({ access_token: "stack-access-token", refresh_token: "refresh-token" });
|
|
283
|
+
}
|
|
284
|
+
if (url.href === "https://dash.freestyle.sh/api/cli/teams") {
|
|
285
|
+
return Response.json([
|
|
286
|
+
{ id: "team_alpha", displayName: "Alpha" },
|
|
287
|
+
{ id: "team_beta", displayName: "Beta", sandboxAccountId: "sandbox-beta" },
|
|
288
|
+
]);
|
|
289
|
+
}
|
|
290
|
+
if (url.href === "https://dash.freestyle.sh/api/proxy/request") {
|
|
291
|
+
const body = JSON.parse(String(init?.body));
|
|
292
|
+
proxyRequests.push(body);
|
|
293
|
+
if (body.data.path === "identity/v1/identities") {
|
|
294
|
+
return Response.json({ id: "identity-browser" });
|
|
295
|
+
}
|
|
296
|
+
if (body.data.path === "identity/v1/identities/identity-browser/tokens") {
|
|
297
|
+
return Response.json({ id: "token-id-browser", token: "ssh-token-browser" });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return Response.json({ error: "unexpected request", url: url.href }, { status: 500 });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const controller = await freestyleProviderPlugin.createProvider({
|
|
305
|
+
provider: {
|
|
306
|
+
providerId: FREESTYLE_PROVIDER_ID,
|
|
307
|
+
config: {},
|
|
308
|
+
},
|
|
309
|
+
storage: projectStorage,
|
|
310
|
+
hostStorage,
|
|
311
|
+
local: {
|
|
312
|
+
open: async () => {},
|
|
313
|
+
prompt: {
|
|
314
|
+
message: async () => {},
|
|
315
|
+
text: async () => "",
|
|
316
|
+
confirm: async () => true,
|
|
317
|
+
select: async (prompt) => {
|
|
318
|
+
selectPrompts.push(prompt);
|
|
319
|
+
return "team_beta";
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
const checks = await controller.checks?.(providerCheckContext("require"));
|
|
325
|
+
|
|
326
|
+
expect(selectPrompts).toEqual([{
|
|
327
|
+
message: "Choose Freestyle team",
|
|
328
|
+
options: [
|
|
329
|
+
{ value: "team_alpha", label: "Alpha (team_alpha)", description: undefined },
|
|
330
|
+
{ value: "team_beta", label: "Beta (team_beta)", description: "sandbox sandbox-beta" },
|
|
331
|
+
],
|
|
332
|
+
}]);
|
|
333
|
+
expect(hostStorage.entries("stack-auth:")[0]?.value).toMatchObject({
|
|
334
|
+
refreshToken: "refresh-token",
|
|
335
|
+
accessToken: "stack-access-token",
|
|
336
|
+
defaultTeamId: "team_beta",
|
|
337
|
+
defaultTeamName: "Beta",
|
|
338
|
+
});
|
|
339
|
+
expect(proxyRequests).toEqual([
|
|
340
|
+
expect.objectContaining({
|
|
341
|
+
data: expect.objectContaining({
|
|
342
|
+
teamId: "team_beta",
|
|
343
|
+
path: "identity/v1/identities",
|
|
344
|
+
}),
|
|
345
|
+
}),
|
|
346
|
+
expect.objectContaining({
|
|
347
|
+
data: expect.objectContaining({
|
|
348
|
+
teamId: "team_beta",
|
|
349
|
+
path: "identity/v1/identities/identity-browser/tokens",
|
|
350
|
+
}),
|
|
351
|
+
}),
|
|
352
|
+
]);
|
|
353
|
+
expect(checks).toContainEqual(expect.objectContaining({
|
|
354
|
+
id: "team",
|
|
355
|
+
label: "Freestyle team",
|
|
356
|
+
status: "ok",
|
|
357
|
+
value: "Beta (team_beta)",
|
|
358
|
+
detail: "team_beta",
|
|
359
|
+
metadata: {
|
|
360
|
+
teamId: "team_beta",
|
|
361
|
+
teamName: "Beta",
|
|
362
|
+
},
|
|
363
|
+
}));
|
|
364
|
+
} finally {
|
|
365
|
+
globalThis.fetch = previousFetch;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
225
369
|
test("ignores ambient FREESTYLE_API_KEY unless API-key auth is configured", async () => {
|
|
226
370
|
process.env.FREESTYLE_API_KEY = "stale-api-key";
|
|
227
371
|
delete process.env.FREESTYLE_TEAM_ID;
|
|
@@ -265,7 +409,7 @@ describe("Freestyle provider host auth", () => {
|
|
|
265
409
|
});
|
|
266
410
|
|
|
267
411
|
try {
|
|
268
|
-
await freestyleProviderPlugin.createProvider({
|
|
412
|
+
const controller = await freestyleProviderPlugin.createProvider({
|
|
269
413
|
provider: {
|
|
270
414
|
providerId: FREESTYLE_PROVIDER_ID,
|
|
271
415
|
config: {},
|
|
@@ -278,6 +422,7 @@ describe("Freestyle provider host auth", () => {
|
|
|
278
422
|
},
|
|
279
423
|
},
|
|
280
424
|
});
|
|
425
|
+
await controller.checks?.(providerCheckContext("require"));
|
|
281
426
|
|
|
282
427
|
expect(opened).toEqual([
|
|
283
428
|
"https://dash.freestyle.sh/handler/cli-auth-confirm?login_code=login-code",
|
|
@@ -293,6 +438,102 @@ describe("Freestyle provider host auth", () => {
|
|
|
293
438
|
});
|
|
294
439
|
|
|
295
440
|
describe("Freestyle provider proxy fetch", () => {
|
|
441
|
+
test("logs a replayable API-key fetch with the Freestyle API key redacted", async () => {
|
|
442
|
+
const sdkFetch = createFreestyleSdkFetch(testFetch(async () =>
|
|
443
|
+
Response.json({
|
|
444
|
+
code: "INTERNAL_ERROR",
|
|
445
|
+
message: "Internal server error",
|
|
446
|
+
}, { status: 500, statusText: "Internal Server Error" })
|
|
447
|
+
));
|
|
448
|
+
|
|
449
|
+
const messages = await captureConsoleError(async () => {
|
|
450
|
+
const response = await sdkFetch("https://api.freestyle.sh/v1/vms", {
|
|
451
|
+
method: "POST",
|
|
452
|
+
headers: {
|
|
453
|
+
Authorization: "Bearer real-api-key",
|
|
454
|
+
"Content-Type": "application/json",
|
|
455
|
+
},
|
|
456
|
+
body: JSON.stringify({
|
|
457
|
+
image: "ubuntu-24.04",
|
|
458
|
+
apiKey: "body-api-key",
|
|
459
|
+
}),
|
|
460
|
+
});
|
|
461
|
+
expect(response.status).toBe(500);
|
|
462
|
+
await response.text();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
expect(messages).toHaveLength(1);
|
|
466
|
+
expect(messages[0]).toContain('await fetch("https://api.freestyle.sh/v1/vms", {');
|
|
467
|
+
expect(messages[0]).toContain('"Authorization": "Bearer <redacted FREESTYLE_API_KEY>"');
|
|
468
|
+
expect(messages[0]).toContain('"image": "ubuntu-24.04"');
|
|
469
|
+
expect(messages[0]).toContain('"apiKey": "[redacted]"');
|
|
470
|
+
expect(messages[0]).toContain('Response: 500 Internal Server Error');
|
|
471
|
+
expect(messages[0]).not.toContain("real-api-key");
|
|
472
|
+
expect(messages[0]).not.toContain("body-api-key");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("logs the original replayable request when a background request fails through the proxy", async () => {
|
|
476
|
+
const proxyFetch = createFreestyleProxyFetch({
|
|
477
|
+
dashboardUrl: "https://dash.freestyle.sh",
|
|
478
|
+
accessToken: "stack-access-token",
|
|
479
|
+
teamId: "team_123",
|
|
480
|
+
fetch: testFetch(async (resource, init) => {
|
|
481
|
+
const url = resourceUrl(resource);
|
|
482
|
+
expect(url.href).toBe("https://dash.freestyle.sh/api/proxy/request");
|
|
483
|
+
const body = JSON.parse(String(init?.body));
|
|
484
|
+
if (body.data.path === "v1/vms") {
|
|
485
|
+
return Response.json({
|
|
486
|
+
requestId: "ri_test_123",
|
|
487
|
+
status: "pending",
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (body.data.path === "auth/v1/background-requests/ri_test_123") {
|
|
491
|
+
return Response.json({
|
|
492
|
+
code: "INTERNAL_ERROR",
|
|
493
|
+
message: "Internal server error",
|
|
494
|
+
accessToken: "should-redact",
|
|
495
|
+
}, { status: 500, statusText: "Internal Server Error" });
|
|
496
|
+
}
|
|
497
|
+
return Response.json({ error: "unexpected request", body }, { status: 500 });
|
|
498
|
+
}),
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const first = await proxyFetch("https://api.freestyle.sh/v1/vms", {
|
|
502
|
+
method: "POST",
|
|
503
|
+
headers: {
|
|
504
|
+
Authorization: "Bearer rigkit-browser-auth",
|
|
505
|
+
"Content-Type": "application/json",
|
|
506
|
+
},
|
|
507
|
+
body: JSON.stringify({
|
|
508
|
+
image: "ubuntu-24.04",
|
|
509
|
+
}),
|
|
510
|
+
});
|
|
511
|
+
expect(first.status).toBe(202);
|
|
512
|
+
|
|
513
|
+
const messages = await captureConsoleError(async () => {
|
|
514
|
+
const failed = await proxyFetch("https://api.freestyle.sh/auth/v1/background-requests/ri_test_123", {
|
|
515
|
+
method: "GET",
|
|
516
|
+
headers: {
|
|
517
|
+
Authorization: "Bearer rigkit-browser-auth",
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
expect(failed.status).toBe(500);
|
|
521
|
+
await failed.text();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
expect(messages).toHaveLength(1);
|
|
525
|
+
expect(messages[0]).toContain("Freestyle background request ri_test_123 failed. Original API request:");
|
|
526
|
+
expect(messages[0]).toContain('await fetch("https://api.freestyle.sh/v1/vms", {');
|
|
527
|
+
expect(messages[0]).toContain('method: "POST"');
|
|
528
|
+
expect(messages[0]).toContain('"Authorization": "Bearer <redacted FREESTYLE_API_KEY>"');
|
|
529
|
+
expect(messages[0]).toContain('"image": "ubuntu-24.04"');
|
|
530
|
+
expect(messages[0]).toContain('Response: 500 Internal Server Error');
|
|
531
|
+
expect(messages[0]).toContain('"accessToken":"[redacted]"');
|
|
532
|
+
expect(messages[0]).not.toContain("stack-access-token");
|
|
533
|
+
expect(messages[0]).not.toContain("rigkit-browser-auth");
|
|
534
|
+
expect(messages[0]).not.toContain("should-redact");
|
|
535
|
+
});
|
|
536
|
+
|
|
296
537
|
test("preserves Freestyle background request semantics through the browser-auth proxy", async () => {
|
|
297
538
|
const proxyFetch = createFreestyleProxyFetch({
|
|
298
539
|
dashboardUrl: "https://dash.freestyle.sh",
|
|
@@ -356,13 +597,16 @@ describe("Freestyle provider proxy fetch", () => {
|
|
|
356
597
|
),
|
|
357
598
|
});
|
|
358
599
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
600
|
+
let response: Response | undefined;
|
|
601
|
+
await captureConsoleError(async () => {
|
|
602
|
+
response = await proxyFetch("https://api.freestyle.sh/v1/vms", {
|
|
603
|
+
method: "POST",
|
|
604
|
+
body: "{}",
|
|
605
|
+
});
|
|
362
606
|
});
|
|
363
607
|
|
|
364
|
-
expect(response
|
|
365
|
-
await expect(response
|
|
608
|
+
expect(response?.status).toBe(500);
|
|
609
|
+
await expect(response?.json()).resolves.toEqual({
|
|
366
610
|
code: "INTERNAL_ERROR",
|
|
367
611
|
message: "VM setup failed",
|
|
368
612
|
details: {
|
|
@@ -389,10 +633,13 @@ describe("Freestyle provider proxy fetch", () => {
|
|
|
389
633
|
),
|
|
390
634
|
});
|
|
391
635
|
|
|
392
|
-
|
|
636
|
+
let response: Response | undefined;
|
|
637
|
+
await captureConsoleError(async () => {
|
|
638
|
+
response = await proxyFetch("https://api.freestyle.sh/v1/vms");
|
|
639
|
+
});
|
|
393
640
|
|
|
394
|
-
expect(response
|
|
395
|
-
await expect(response
|
|
641
|
+
expect(response?.status).toBe(500);
|
|
642
|
+
await expect(response?.json()).resolves.toEqual({
|
|
396
643
|
code: "INTERNAL_ERROR",
|
|
397
644
|
message: "Internal server error",
|
|
398
645
|
requestId: "req_123",
|
|
@@ -458,6 +705,14 @@ function providerContext(
|
|
|
458
705
|
};
|
|
459
706
|
}
|
|
460
707
|
|
|
708
|
+
function providerCheckContext(mode: "plan" | "require"): ProviderCheckContext {
|
|
709
|
+
return {
|
|
710
|
+
mode,
|
|
711
|
+
workflow: "workflow",
|
|
712
|
+
local: providerContext([]).local,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
461
716
|
function testFetch(
|
|
462
717
|
handler: (
|
|
463
718
|
resource: Parameters<typeof fetch>[0],
|
|
@@ -475,6 +730,20 @@ function resourceUrl(resource: Parameters<typeof fetch>[0]): URL {
|
|
|
475
730
|
return new URL(resource.url);
|
|
476
731
|
}
|
|
477
732
|
|
|
733
|
+
async function captureConsoleError(action: () => Promise<void>): Promise<string[]> {
|
|
734
|
+
const previous = console.error;
|
|
735
|
+
const messages: string[] = [];
|
|
736
|
+
console.error = (...args: unknown[]) => {
|
|
737
|
+
messages.push(args.map((arg) => String(arg)).join(" "));
|
|
738
|
+
};
|
|
739
|
+
try {
|
|
740
|
+
await action();
|
|
741
|
+
} finally {
|
|
742
|
+
console.error = previous;
|
|
743
|
+
}
|
|
744
|
+
return messages;
|
|
745
|
+
}
|
|
746
|
+
|
|
478
747
|
function setEnv(name: string, value: string | undefined): void {
|
|
479
748
|
if (value === undefined) {
|
|
480
749
|
delete process.env[name];
|
package/src/host-auth.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { Freestyle } from "freestyle";
|
|
3
|
-
import type { LocalWorkspaceRuntime, ProviderStorage } from "@rigkit/engine";
|
|
3
|
+
import type { LocalWorkspaceRuntime, ProviderStorage, WorkflowProviderCheckResult } from "@rigkit/engine";
|
|
4
4
|
import type { JsonValue } from "@rigkit/sdk";
|
|
5
5
|
import { freestyleIdentityId, freestyleToken, freestyleTokenId } from "./auth.ts";
|
|
6
6
|
import { createFreestyleStore } from "./store.ts";
|
|
@@ -8,6 +8,7 @@ import { RIGKIT_PROVIDER_FREESTYLE_VERSION } from "./version.ts";
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_STACK_API_URL = "https://api.stack-auth.com";
|
|
10
10
|
const DEFAULT_STACK_APP_URL = "https://dash.freestyle.sh";
|
|
11
|
+
const DEFAULT_FREESTYLE_API_URL = "https://api.freestyle.sh";
|
|
11
12
|
const DEFAULT_STACK_PROJECT_ID = "0edf478c-f123-46fb-818f-34c0024a9f35";
|
|
12
13
|
const DEFAULT_STACK_PUBLISHABLE_CLIENT_KEY = "pck_h2aft7g9pqjzrkdnzs199h1may5wjtdtdxeex7m2wzp1r";
|
|
13
14
|
const DEFAULT_CLI_AUTH_TIMEOUT_MILLIS = 10 * 60 * 1000;
|
|
@@ -28,6 +29,7 @@ export type FreestyleAuthenticatedClient = {
|
|
|
28
29
|
identityId: ReturnType<typeof freestyleIdentityId>;
|
|
29
30
|
tokenId: ReturnType<typeof freestyleTokenId>;
|
|
30
31
|
token: ReturnType<typeof freestyleToken>;
|
|
32
|
+
team?: FreestyleResolvedTeam;
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
type CreateFreestyleAuthenticatedClientInput = {
|
|
@@ -42,6 +44,7 @@ type CreateFreestyleAuthenticatedClientInput = {
|
|
|
42
44
|
type ResolvedClientAuth = {
|
|
43
45
|
client: Freestyle;
|
|
44
46
|
identityKey: string;
|
|
47
|
+
team?: FreestyleResolvedTeam;
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
type StackAuthConfig = {
|
|
@@ -57,6 +60,7 @@ type StackAuthState = {
|
|
|
57
60
|
refreshToken: string;
|
|
58
61
|
updatedAt: number;
|
|
59
62
|
defaultTeamId?: string;
|
|
63
|
+
defaultTeamName?: string;
|
|
60
64
|
accessToken?: string;
|
|
61
65
|
accessTokenUpdatedAt?: number;
|
|
62
66
|
};
|
|
@@ -72,6 +76,11 @@ type FreestyleTeam = {
|
|
|
72
76
|
sandboxAccountId?: string | null;
|
|
73
77
|
};
|
|
74
78
|
|
|
79
|
+
export type FreestyleResolvedTeam = {
|
|
80
|
+
id: string;
|
|
81
|
+
displayName?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
75
84
|
export async function createFreestyleAuthenticatedClient(
|
|
76
85
|
input: CreateFreestyleAuthenticatedClientInput,
|
|
77
86
|
): Promise<FreestyleAuthenticatedClient> {
|
|
@@ -84,6 +93,7 @@ export async function createFreestyleAuthenticatedClient(
|
|
|
84
93
|
identityId: savedIdentity.identityId,
|
|
85
94
|
tokenId: savedIdentity.tokenId,
|
|
86
95
|
token: savedIdentity.token,
|
|
96
|
+
team: auth.team,
|
|
87
97
|
};
|
|
88
98
|
}
|
|
89
99
|
|
|
@@ -101,9 +111,103 @@ export async function createFreestyleAuthenticatedClient(
|
|
|
101
111
|
identityId: createdIdentity.identityId,
|
|
102
112
|
tokenId: createdIdentity.tokenId,
|
|
103
113
|
token: createdIdentity.token,
|
|
114
|
+
team: auth.team,
|
|
104
115
|
};
|
|
105
116
|
}
|
|
106
117
|
|
|
118
|
+
export function checkFreestyleProviderAuth(input: {
|
|
119
|
+
config?: FreestyleProviderConfig;
|
|
120
|
+
hostStorage: ProviderStorage;
|
|
121
|
+
}): WorkflowProviderCheckResult[] {
|
|
122
|
+
const apiKey = nonEmpty(input.config?.apiKey);
|
|
123
|
+
const apiUrl = nonEmpty(input.config?.apiUrl) ?? nonEmpty(process.env.FREESTYLE_API_URL);
|
|
124
|
+
const store = createFreestyleStore(input.hostStorage);
|
|
125
|
+
|
|
126
|
+
if (apiKey) {
|
|
127
|
+
const identityKey = apiKeyIdentityKey({ apiUrl, apiKey });
|
|
128
|
+
return [{
|
|
129
|
+
id: "auth",
|
|
130
|
+
label: "Freestyle auth",
|
|
131
|
+
status: "ok",
|
|
132
|
+
value: "API key",
|
|
133
|
+
fingerprint: providerIdentityFingerprint(identityKey, store.getIdentity(identityKey)),
|
|
134
|
+
}];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const stack = resolveStackAuthConfig(input.config);
|
|
138
|
+
const stored = readStackAuthState(input.hostStorage.get(stackAuthStateKey(stack))?.value);
|
|
139
|
+
const configuredTeamId = nonEmpty(input.config?.teamId) ?? nonEmpty(process.env.FREESTYLE_TEAM_ID);
|
|
140
|
+
|
|
141
|
+
if (!stored?.refreshToken) {
|
|
142
|
+
return [{
|
|
143
|
+
id: "auth",
|
|
144
|
+
label: "Freestyle auth",
|
|
145
|
+
status: "required",
|
|
146
|
+
value: "login required",
|
|
147
|
+
message: "Run rig apply, rig create, or rig run to authenticate with Freestyle.",
|
|
148
|
+
fingerprint: `browser:${stack.profile}:auth:missing`,
|
|
149
|
+
}];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const teamId = configuredTeamId ?? stored.defaultTeamId;
|
|
153
|
+
if (!teamId) {
|
|
154
|
+
return [{
|
|
155
|
+
id: "team",
|
|
156
|
+
label: "Freestyle team",
|
|
157
|
+
status: "required",
|
|
158
|
+
value: "team selection required",
|
|
159
|
+
message: "Run rig apply, rig create, or rig run to choose a Freestyle team.",
|
|
160
|
+
fingerprint: `browser:${stack.profile}:team:missing`,
|
|
161
|
+
}];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const identityKey = browserIdentityKey({
|
|
165
|
+
apiUrl,
|
|
166
|
+
dashboardUrl: stack.dashboardUrl,
|
|
167
|
+
profile: stack.profile,
|
|
168
|
+
teamId,
|
|
169
|
+
});
|
|
170
|
+
const storedTeamName = teamId === stored.defaultTeamId ? stored.defaultTeamName : undefined;
|
|
171
|
+
return [{
|
|
172
|
+
id: "team",
|
|
173
|
+
label: "Freestyle team",
|
|
174
|
+
status: "ok",
|
|
175
|
+
value: formatFreestyleTeam({ id: teamId, displayName: storedTeamName }),
|
|
176
|
+
detail: teamId,
|
|
177
|
+
fingerprint: providerIdentityFingerprint(identityKey, store.getIdentity(identityKey)),
|
|
178
|
+
metadata: {
|
|
179
|
+
teamId,
|
|
180
|
+
...(storedTeamName ? { teamName: storedTeamName } : {}),
|
|
181
|
+
},
|
|
182
|
+
}];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function freestyleProviderChecksFromAuthenticated(
|
|
186
|
+
auth: FreestyleAuthenticatedClient,
|
|
187
|
+
): WorkflowProviderCheckResult[] {
|
|
188
|
+
if (auth.team) {
|
|
189
|
+
return [{
|
|
190
|
+
id: "team",
|
|
191
|
+
label: "Freestyle team",
|
|
192
|
+
status: "ok",
|
|
193
|
+
value: formatFreestyleTeam(auth.team),
|
|
194
|
+
detail: auth.team.id,
|
|
195
|
+
fingerprint: `identity:${auth.identityId}`,
|
|
196
|
+
metadata: {
|
|
197
|
+
teamId: auth.team.id,
|
|
198
|
+
...(auth.team.displayName ? { teamName: auth.team.displayName } : {}),
|
|
199
|
+
},
|
|
200
|
+
}];
|
|
201
|
+
}
|
|
202
|
+
return [{
|
|
203
|
+
id: "auth",
|
|
204
|
+
label: "Freestyle auth",
|
|
205
|
+
status: "ok",
|
|
206
|
+
value: "API key",
|
|
207
|
+
fingerprint: `identity:${auth.identityId}`,
|
|
208
|
+
}];
|
|
209
|
+
}
|
|
210
|
+
|
|
107
211
|
export function createFreestyleProxyFetch(input: {
|
|
108
212
|
dashboardUrl: string;
|
|
109
213
|
accessToken: string;
|
|
@@ -112,10 +216,15 @@ export function createFreestyleProxyFetch(input: {
|
|
|
112
216
|
}): typeof fetch {
|
|
113
217
|
const fetchFn = input.fetch ?? globalThis.fetch;
|
|
114
218
|
const dashboardUrl = trimTrailingSlash(input.dashboardUrl);
|
|
219
|
+
const backgroundRequests = new Map<string, string>();
|
|
115
220
|
|
|
116
221
|
const proxyFetch = async (resource: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
|
|
117
222
|
const url = resourceUrl(resource);
|
|
118
223
|
const path = `${url.pathname}${url.search}`.replace(/^\/+/, "");
|
|
224
|
+
const freestyleRequestInit: RequestInit = {
|
|
225
|
+
...init,
|
|
226
|
+
headers: withRigkitHeaders(init?.headers),
|
|
227
|
+
};
|
|
119
228
|
const proxyResponse = await fetchFn(`${dashboardUrl}/api/proxy/request`, {
|
|
120
229
|
method: "POST",
|
|
121
230
|
headers: withRigkitHeaders({
|
|
@@ -126,8 +235,8 @@ export function createFreestyleProxyFetch(input: {
|
|
|
126
235
|
accessToken: input.accessToken,
|
|
127
236
|
teamId: input.teamId,
|
|
128
237
|
path,
|
|
129
|
-
method: init
|
|
130
|
-
headers: Object.fromEntries(
|
|
238
|
+
method: resolveRequestMethod(resource, init),
|
|
239
|
+
headers: Object.fromEntries(new Headers(freestyleRequestInit.headers).entries()),
|
|
131
240
|
body: init?.body ? String(init.body) : undefined,
|
|
132
241
|
},
|
|
133
242
|
}),
|
|
@@ -135,6 +244,13 @@ export function createFreestyleProxyFetch(input: {
|
|
|
135
244
|
|
|
136
245
|
if (!proxyResponse.ok) {
|
|
137
246
|
const errorText = await proxyResponse.text();
|
|
247
|
+
await logFreestyleApiRequestFailure({
|
|
248
|
+
backgroundRequests,
|
|
249
|
+
resource,
|
|
250
|
+
init: freestyleRequestInit,
|
|
251
|
+
response: proxyResponse,
|
|
252
|
+
responseText: errorText,
|
|
253
|
+
});
|
|
138
254
|
const normalized = normalizeProxyError(errorText, proxyResponse.status);
|
|
139
255
|
return new Response(normalized.body, {
|
|
140
256
|
status: proxyResponse.status,
|
|
@@ -146,6 +262,9 @@ export function createFreestyleProxyFetch(input: {
|
|
|
146
262
|
const data = await proxyResponse.json();
|
|
147
263
|
if (isBackgroundRequestPending(data)) {
|
|
148
264
|
const requestId = backgroundRequestId(data);
|
|
265
|
+
if (requestId) {
|
|
266
|
+
backgroundRequests.set(requestId, formatReplayableFetchRequest(resource, freestyleRequestInit));
|
|
267
|
+
}
|
|
149
268
|
return Response.json(data, {
|
|
150
269
|
status: 202,
|
|
151
270
|
headers: {
|
|
@@ -162,11 +281,24 @@ export function createFreestyleProxyFetch(input: {
|
|
|
162
281
|
}
|
|
163
282
|
|
|
164
283
|
export function createFreestyleSdkFetch(fetchFn: typeof fetch = globalThis.fetch): typeof fetch {
|
|
165
|
-
const
|
|
166
|
-
|
|
284
|
+
const backgroundRequests = new Map<string, string>();
|
|
285
|
+
const rigkitFetch = (async (resource, init) => {
|
|
286
|
+
const requestInit: RequestInit = {
|
|
167
287
|
...init,
|
|
168
288
|
headers: withRigkitHeaders(init?.headers),
|
|
169
|
-
}
|
|
289
|
+
};
|
|
290
|
+
const response = await fetchFn(resource, requestInit);
|
|
291
|
+
await rememberBackgroundRequest(backgroundRequests, resource, requestInit, response);
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
await logFreestyleApiRequestFailure({
|
|
294
|
+
backgroundRequests,
|
|
295
|
+
resource,
|
|
296
|
+
init: requestInit,
|
|
297
|
+
response,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return response;
|
|
301
|
+
}) as typeof fetch;
|
|
170
302
|
return Object.assign(rigkitFetch, {
|
|
171
303
|
preconnect: fetchFn.preconnect?.bind(fetchFn) ?? (() => {}),
|
|
172
304
|
}) as typeof fetch;
|
|
@@ -184,7 +316,7 @@ async function resolveClientAuth(input: CreateFreestyleAuthenticatedClientInput)
|
|
|
184
316
|
...(apiUrl ? { baseUrl: apiUrl } : {}),
|
|
185
317
|
fetch: createFreestyleSdkFetch(fetchFn),
|
|
186
318
|
}),
|
|
187
|
-
identityKey:
|
|
319
|
+
identityKey: apiKeyIdentityKey({ apiUrl, apiKey }),
|
|
188
320
|
};
|
|
189
321
|
}
|
|
190
322
|
|
|
@@ -201,7 +333,7 @@ async function resolveClientAuth(input: CreateFreestyleAuthenticatedClientInput)
|
|
|
201
333
|
timeoutMs: input.timeoutMs ?? DEFAULT_CLI_AUTH_TIMEOUT_MILLIS,
|
|
202
334
|
pollIntervalMs: input.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MILLIS,
|
|
203
335
|
});
|
|
204
|
-
const
|
|
336
|
+
const team = await resolveTeam({
|
|
205
337
|
configuredTeamId: input.config?.teamId,
|
|
206
338
|
stored: readStackAuthState(input.hostStorage.get(stackStateKey)?.value) ?? stored,
|
|
207
339
|
accessToken: refreshed.accessToken,
|
|
@@ -209,6 +341,7 @@ async function resolveClientAuth(input: CreateFreestyleAuthenticatedClientInput)
|
|
|
209
341
|
storage: input.hostStorage,
|
|
210
342
|
storageKey: stackStateKey,
|
|
211
343
|
fetch: fetchFn,
|
|
344
|
+
local: input.local,
|
|
212
345
|
});
|
|
213
346
|
const client = new Freestyle({
|
|
214
347
|
apiKey: "rigkit-browser-auth",
|
|
@@ -216,19 +349,20 @@ async function resolveClientAuth(input: CreateFreestyleAuthenticatedClientInput)
|
|
|
216
349
|
fetch: createFreestyleProxyFetch({
|
|
217
350
|
dashboardUrl: stack.dashboardUrl,
|
|
218
351
|
accessToken: refreshed.accessToken,
|
|
219
|
-
teamId,
|
|
352
|
+
teamId: team.id,
|
|
220
353
|
fetch: fetchFn,
|
|
221
354
|
}),
|
|
222
355
|
});
|
|
223
356
|
|
|
224
357
|
return {
|
|
225
358
|
client,
|
|
226
|
-
identityKey:
|
|
227
|
-
apiUrl
|
|
359
|
+
identityKey: browserIdentityKey({
|
|
360
|
+
apiUrl,
|
|
228
361
|
dashboardUrl: stack.dashboardUrl,
|
|
229
362
|
profile: stack.profile,
|
|
230
|
-
teamId,
|
|
231
|
-
})
|
|
363
|
+
teamId: team.id,
|
|
364
|
+
}),
|
|
365
|
+
team,
|
|
232
366
|
};
|
|
233
367
|
}
|
|
234
368
|
|
|
@@ -269,6 +403,162 @@ function withRigkitHeaders(headers: HeadersInit | undefined): Headers {
|
|
|
269
403
|
return next;
|
|
270
404
|
}
|
|
271
405
|
|
|
406
|
+
async function rememberBackgroundRequest(
|
|
407
|
+
backgroundRequests: Map<string, string>,
|
|
408
|
+
resource: Parameters<typeof fetch>[0],
|
|
409
|
+
init: RequestInit,
|
|
410
|
+
response: Response,
|
|
411
|
+
): Promise<void> {
|
|
412
|
+
if (response.status !== 202) return;
|
|
413
|
+
const requestId = await responseBackgroundRequestId(response);
|
|
414
|
+
if (!requestId) return;
|
|
415
|
+
backgroundRequests.set(requestId, formatReplayableFetchRequest(resource, init));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function logFreestyleApiRequestFailure(input: {
|
|
419
|
+
backgroundRequests: Map<string, string>;
|
|
420
|
+
resource: Parameters<typeof fetch>[0];
|
|
421
|
+
init: RequestInit;
|
|
422
|
+
response: Response;
|
|
423
|
+
responseText?: string;
|
|
424
|
+
}): Promise<void> {
|
|
425
|
+
if (isFreestyleBackgroundLogRequest(input.resource)) return;
|
|
426
|
+
|
|
427
|
+
const requestId = backgroundRequestIdFromResource(input.resource);
|
|
428
|
+
const replayRequest = requestId ? input.backgroundRequests.get(requestId) : undefined;
|
|
429
|
+
const responseSummary = await formatResponseSummary(input.response, input.responseText);
|
|
430
|
+
const heading = requestId
|
|
431
|
+
? `Freestyle background request ${requestId} failed. Original API request:`
|
|
432
|
+
: "Freestyle API request failed. Replay request:";
|
|
433
|
+
const request = replayRequest ?? formatReplayableFetchRequest(input.resource, input.init);
|
|
434
|
+
console.error(`${heading}\n${request}\n${responseSummary}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function responseBackgroundRequestId(response: Response): Promise<string | undefined> {
|
|
438
|
+
const header = response.headers.get("x-freestyle-background-request-id");
|
|
439
|
+
if (header) return header;
|
|
440
|
+
const data = await response.clone().json().catch(() => undefined);
|
|
441
|
+
return backgroundRequestId(data);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function backgroundRequestIdFromResource(resource: Parameters<typeof fetch>[0]): string | undefined {
|
|
445
|
+
const path = resourceUrl(resource).pathname;
|
|
446
|
+
const match = path.match(/\/auth\/v1\/background-requests\/([^/]+)$/);
|
|
447
|
+
return match?.[1] ? decodeURIComponent(match[1]) : undefined;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isFreestyleBackgroundLogRequest(resource: Parameters<typeof fetch>[0]): boolean {
|
|
451
|
+
return resourceUrl(resource).pathname === "/observability/v1/logs";
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function formatResponseSummary(response: Response, responseText?: string): Promise<string> {
|
|
455
|
+
const text = responseText ?? await response.clone().text().catch(() => "");
|
|
456
|
+
const status = [response.status, response.statusText].filter(Boolean).join(" ");
|
|
457
|
+
const redactedBody = formatRedactedResponseBody(text);
|
|
458
|
+
return [
|
|
459
|
+
`Response: ${status}`,
|
|
460
|
+
...(redactedBody ? [`Response body: ${redactedBody}`] : []),
|
|
461
|
+
].join("\n");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function formatRedactedResponseBody(text: string): string | undefined {
|
|
465
|
+
if (!text) return undefined;
|
|
466
|
+
try {
|
|
467
|
+
return JSON.stringify(redactSensitiveFields(JSON.parse(text)));
|
|
468
|
+
} catch {
|
|
469
|
+
return text;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function formatReplayableFetchRequest(resource: Parameters<typeof fetch>[0], init: RequestInit): string {
|
|
474
|
+
const lines = [
|
|
475
|
+
`await fetch(${JSON.stringify(resourceUrl(resource).href)}, {`,
|
|
476
|
+
` method: ${JSON.stringify(resolveRequestMethod(resource, init))},`,
|
|
477
|
+
];
|
|
478
|
+
const headers = replayableHeaders(resource, init);
|
|
479
|
+
if (Object.keys(headers).length > 0) {
|
|
480
|
+
lines.push(` headers: ${indentContinuation(JSON.stringify(headers, null, 2), 2)},`);
|
|
481
|
+
}
|
|
482
|
+
const body = replayableBody(init.body);
|
|
483
|
+
if (body) {
|
|
484
|
+
lines.push(` body: ${indentContinuation(body, 2)},`);
|
|
485
|
+
}
|
|
486
|
+
lines.push("});");
|
|
487
|
+
return lines.join("\n");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function replayableHeaders(resource: Parameters<typeof fetch>[0], init: RequestInit): Record<string, string> {
|
|
491
|
+
const headers = resource instanceof Request ? new Headers(resource.headers) : new Headers();
|
|
492
|
+
new Headers(init.headers).forEach((value, key) => {
|
|
493
|
+
headers.set(key, value);
|
|
494
|
+
});
|
|
495
|
+
const result: Record<string, string> = {};
|
|
496
|
+
headers.forEach((value, key) => {
|
|
497
|
+
result[displayHeaderName(key)] = redactHeaderValue(key, value);
|
|
498
|
+
});
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function resolveRequestMethod(resource: Parameters<typeof fetch>[0], init?: RequestInit): string {
|
|
503
|
+
return init?.method ?? (resource instanceof Request ? resource.method : "GET");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function replayableBody(body: BodyInit | null | undefined): string | undefined {
|
|
507
|
+
if (body === undefined || body === null) return undefined;
|
|
508
|
+
if (typeof body === "string") {
|
|
509
|
+
try {
|
|
510
|
+
const parsed = JSON.parse(body) as unknown;
|
|
511
|
+
return `JSON.stringify(${JSON.stringify(redactSensitiveFields(parsed), null, 2)})`;
|
|
512
|
+
} catch {
|
|
513
|
+
return JSON.stringify(body);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (body instanceof URLSearchParams) {
|
|
517
|
+
return `new URLSearchParams(${JSON.stringify(body.toString())})`;
|
|
518
|
+
}
|
|
519
|
+
return JSON.stringify(String(body));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function indentContinuation(value: string, spaces: number): string {
|
|
523
|
+
const lines = value.split("\n");
|
|
524
|
+
const indent = " ".repeat(spaces);
|
|
525
|
+
return [lines[0], ...lines.slice(1).map((line) => `${indent}${line}`)].join("\n");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function displayHeaderName(name: string): string {
|
|
529
|
+
const normalized = name.toLowerCase();
|
|
530
|
+
return {
|
|
531
|
+
authorization: "Authorization",
|
|
532
|
+
"content-type": "Content-Type",
|
|
533
|
+
"user-agent": "User-Agent",
|
|
534
|
+
"x-freestyle-identity-access-token": "X-Freestyle-Identity-Access-Token",
|
|
535
|
+
[RIGKIT_HEADER]: RIGKIT_HEADER,
|
|
536
|
+
[RIGKIT_VERSION_HEADER]: RIGKIT_VERSION_HEADER,
|
|
537
|
+
}[normalized] ?? name;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function redactHeaderValue(name: string, value: string): string {
|
|
541
|
+
if (name.toLowerCase() === "authorization" && /^Bearer\s+/i.test(value)) {
|
|
542
|
+
return "Bearer <redacted FREESTYLE_API_KEY>";
|
|
543
|
+
}
|
|
544
|
+
return isSensitiveFieldName(name) ? "[redacted]" : value;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function redactSensitiveFields(value: unknown): unknown {
|
|
548
|
+
if (Array.isArray(value)) return value.map(redactSensitiveFields);
|
|
549
|
+
if (!value || typeof value !== "object") return value;
|
|
550
|
+
const next: Record<string, unknown> = {};
|
|
551
|
+
for (const [key, field] of Object.entries(value)) {
|
|
552
|
+
next[key] = isSensitiveFieldName(key) ? "[redacted]" : redactSensitiveFields(field);
|
|
553
|
+
}
|
|
554
|
+
return next;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function isSensitiveFieldName(name: string): boolean {
|
|
558
|
+
return /authorization|api[-_]?key|access[-_]?token|refresh[-_]?token|password|secret|credential|cookie/i
|
|
559
|
+
.test(name);
|
|
560
|
+
}
|
|
561
|
+
|
|
272
562
|
async function resolveStackAccessToken(input: {
|
|
273
563
|
config: StackAuthConfig;
|
|
274
564
|
storage: ProviderStorage;
|
|
@@ -285,6 +575,7 @@ async function resolveStackAccessToken(input: {
|
|
|
285
575
|
saveStackAuthState(input.storage, input.storageKey, {
|
|
286
576
|
refreshToken,
|
|
287
577
|
defaultTeamId: input.stored?.defaultTeamId,
|
|
578
|
+
defaultTeamName: input.stored?.defaultTeamName,
|
|
288
579
|
updatedAt: Date.now(),
|
|
289
580
|
});
|
|
290
581
|
}
|
|
@@ -296,6 +587,7 @@ async function resolveStackAccessToken(input: {
|
|
|
296
587
|
saveStackAuthState(input.storage, input.storageKey, {
|
|
297
588
|
refreshToken,
|
|
298
589
|
defaultTeamId: input.stored?.defaultTeamId,
|
|
590
|
+
defaultTeamName: input.stored?.defaultTeamName,
|
|
299
591
|
updatedAt: Date.now(),
|
|
300
592
|
});
|
|
301
593
|
refreshed = await refreshStackAccessToken(input.config, refreshToken, input.fetch);
|
|
@@ -309,6 +601,7 @@ async function resolveStackAccessToken(input: {
|
|
|
309
601
|
saveStackAuthState(input.storage, input.storageKey, {
|
|
310
602
|
refreshToken: nextRefreshToken,
|
|
311
603
|
defaultTeamId: input.stored?.defaultTeamId,
|
|
604
|
+
defaultTeamName: input.stored?.defaultTeamName,
|
|
312
605
|
accessToken: refreshed.accessToken,
|
|
313
606
|
accessTokenUpdatedAt: Date.now(),
|
|
314
607
|
updatedAt: Date.now(),
|
|
@@ -404,7 +697,7 @@ async function refreshStackAccessToken(
|
|
|
404
697
|
};
|
|
405
698
|
}
|
|
406
699
|
|
|
407
|
-
async function
|
|
700
|
+
async function resolveTeam(input: {
|
|
408
701
|
configuredTeamId: string | undefined;
|
|
409
702
|
stored: StackAuthState | undefined;
|
|
410
703
|
accessToken: string;
|
|
@@ -412,19 +705,27 @@ async function resolveTeamId(input: {
|
|
|
412
705
|
storage: ProviderStorage;
|
|
413
706
|
storageKey: string;
|
|
414
707
|
fetch: typeof fetch;
|
|
415
|
-
|
|
708
|
+
local: LocalWorkspaceRuntime;
|
|
709
|
+
}): Promise<FreestyleResolvedTeam> {
|
|
416
710
|
const teamId =
|
|
417
711
|
nonEmpty(input.configuredTeamId) ??
|
|
418
712
|
nonEmpty(process.env.FREESTYLE_TEAM_ID) ??
|
|
419
713
|
nonEmpty(input.stored?.defaultTeamId);
|
|
420
714
|
if (teamId) {
|
|
715
|
+
const storedTeamName = teamId === input.stored?.defaultTeamId
|
|
716
|
+
? input.stored?.defaultTeamName
|
|
717
|
+
: undefined;
|
|
421
718
|
saveStackAuthState(input.storage, input.storageKey, {
|
|
422
719
|
...input.stored,
|
|
423
720
|
refreshToken: input.stored?.refreshToken ?? "",
|
|
424
721
|
defaultTeamId: teamId,
|
|
722
|
+
defaultTeamName: storedTeamName,
|
|
425
723
|
updatedAt: Date.now(),
|
|
426
724
|
});
|
|
427
|
-
return
|
|
725
|
+
return {
|
|
726
|
+
id: teamId,
|
|
727
|
+
...(storedTeamName ? { displayName: storedTeamName } : {}),
|
|
728
|
+
};
|
|
428
729
|
}
|
|
429
730
|
|
|
430
731
|
const teams = await listTeams(input.config, input.accessToken, input.fetch);
|
|
@@ -434,19 +735,68 @@ async function resolveTeamId(input: {
|
|
|
434
735
|
...input.stored,
|
|
435
736
|
refreshToken: input.stored?.refreshToken ?? "",
|
|
436
737
|
defaultTeamId: onlyTeam.id,
|
|
738
|
+
defaultTeamName: nonEmpty(onlyTeam.displayName),
|
|
437
739
|
updatedAt: Date.now(),
|
|
438
740
|
});
|
|
439
|
-
return onlyTeam
|
|
741
|
+
return freestyleResolvedTeam(onlyTeam);
|
|
440
742
|
}
|
|
441
743
|
|
|
442
744
|
if (teams.length === 0) {
|
|
443
745
|
throw new Error("Freestyle authentication succeeded, but no teams were available for this account.");
|
|
444
746
|
}
|
|
445
747
|
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
748
|
+
const selectedTeamId = await selectFreestyleTeam(input.local, teams);
|
|
749
|
+
const selectedTeam = teams.find((team) => team.id === selectedTeamId);
|
|
750
|
+
if (!selectedTeam) {
|
|
751
|
+
throw new Error(`Freestyle team selection returned unknown team ${selectedTeamId}.`);
|
|
752
|
+
}
|
|
753
|
+
saveStackAuthState(input.storage, input.storageKey, {
|
|
754
|
+
...input.stored,
|
|
755
|
+
refreshToken: input.stored?.refreshToken ?? "",
|
|
756
|
+
defaultTeamId: selectedTeam.id,
|
|
757
|
+
defaultTeamName: nonEmpty(selectedTeam.displayName),
|
|
758
|
+
updatedAt: Date.now(),
|
|
759
|
+
});
|
|
760
|
+
return freestyleResolvedTeam(selectedTeam);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function selectFreestyleTeam(
|
|
764
|
+
local: LocalWorkspaceRuntime,
|
|
765
|
+
teams: FreestyleTeam[],
|
|
766
|
+
): Promise<string> {
|
|
767
|
+
if (!local.prompt?.select) {
|
|
768
|
+
const choices = teams.map((team) => `${team.displayName ?? team.id} (${team.id})`).join(", ");
|
|
769
|
+
throw new Error(
|
|
770
|
+
`Freestyle authentication found multiple teams. Set freestyle.provider({ teamId }) or FREESTYLE_TEAM_ID. Teams: ${choices}`,
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
return await local.prompt.select({
|
|
774
|
+
message: "Choose Freestyle team",
|
|
775
|
+
options: teams.map((team) => ({
|
|
776
|
+
value: team.id,
|
|
777
|
+
label: team.displayName ? `${team.displayName} (${team.id})` : team.id,
|
|
778
|
+
description: team.sandboxAccountId ? `sandbox ${team.sandboxAccountId}` : undefined,
|
|
779
|
+
})),
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function freestyleResolvedTeam(team: FreestyleTeam): FreestyleResolvedTeam {
|
|
784
|
+
const displayName = nonEmpty(team.displayName);
|
|
785
|
+
return {
|
|
786
|
+
id: team.id,
|
|
787
|
+
...(displayName ? { displayName } : {}),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function formatFreestyleTeam(team: FreestyleResolvedTeam): string {
|
|
792
|
+
return team.displayName ? `${team.displayName} (${team.id})` : team.id;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function providerIdentityFingerprint(
|
|
796
|
+
identityKey: string,
|
|
797
|
+
identity: { identityId: string } | undefined,
|
|
798
|
+
): string {
|
|
799
|
+
return `${identityKey}:${identity?.identityId ?? "missing-identity"}`;
|
|
450
800
|
}
|
|
451
801
|
|
|
452
802
|
async function listTeams(config: StackAuthConfig, accessToken: string, fetchFn: typeof fetch): Promise<FreestyleTeam[]> {
|
|
@@ -487,6 +837,24 @@ function stackAuthStateKey(config: StackAuthConfig): string {
|
|
|
487
837
|
})}`;
|
|
488
838
|
}
|
|
489
839
|
|
|
840
|
+
function apiKeyIdentityKey(input: { apiUrl: string | undefined; apiKey: string }): string {
|
|
841
|
+
return `api-key:${fingerprint({ apiUrl: input.apiUrl ?? "default", apiKey: input.apiKey })}`;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function browserIdentityKey(input: {
|
|
845
|
+
apiUrl: string | undefined;
|
|
846
|
+
dashboardUrl: string;
|
|
847
|
+
profile: string;
|
|
848
|
+
teamId: string;
|
|
849
|
+
}): string {
|
|
850
|
+
return `browser:${fingerprint({
|
|
851
|
+
apiUrl: input.apiUrl ?? "default",
|
|
852
|
+
dashboardUrl: input.dashboardUrl,
|
|
853
|
+
profile: input.profile,
|
|
854
|
+
teamId: input.teamId,
|
|
855
|
+
})}`;
|
|
856
|
+
}
|
|
857
|
+
|
|
490
858
|
function readStackAuthState(value: JsonValue | undefined): StackAuthState | undefined {
|
|
491
859
|
if (!isRecord(value)) return undefined;
|
|
492
860
|
const refreshToken = typeof value.refreshToken === "string" ? value.refreshToken : undefined;
|
|
@@ -495,6 +863,7 @@ function readStackAuthState(value: JsonValue | undefined): StackAuthState | unde
|
|
|
495
863
|
refreshToken,
|
|
496
864
|
updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : Date.now(),
|
|
497
865
|
defaultTeamId: typeof value.defaultTeamId === "string" ? value.defaultTeamId : undefined,
|
|
866
|
+
defaultTeamName: typeof value.defaultTeamName === "string" ? value.defaultTeamName : undefined,
|
|
498
867
|
accessToken: typeof value.accessToken === "string" ? value.accessToken : undefined,
|
|
499
868
|
accessTokenUpdatedAt: typeof value.accessTokenUpdatedAt === "number" ? value.accessTokenUpdatedAt : undefined,
|
|
500
869
|
};
|
|
@@ -506,13 +875,14 @@ function saveStackAuthState(storage: ProviderStorage, key: string, state: StackA
|
|
|
506
875
|
refreshToken: state.refreshToken,
|
|
507
876
|
updatedAt: state.updatedAt,
|
|
508
877
|
...(state.defaultTeamId ? { defaultTeamId: state.defaultTeamId } : {}),
|
|
878
|
+
...(state.defaultTeamName ? { defaultTeamName: state.defaultTeamName } : {}),
|
|
509
879
|
...(state.accessToken ? { accessToken: state.accessToken } : {}),
|
|
510
880
|
...(state.accessTokenUpdatedAt ? { accessTokenUpdatedAt: state.accessTokenUpdatedAt } : {}),
|
|
511
881
|
});
|
|
512
882
|
}
|
|
513
883
|
|
|
514
884
|
function resourceUrl(resource: Parameters<typeof fetch>[0]): URL {
|
|
515
|
-
if (typeof resource === "string") return new URL(resource);
|
|
885
|
+
if (typeof resource === "string") return new URL(resource, DEFAULT_FREESTYLE_API_URL);
|
|
516
886
|
if (resource instanceof URL) return resource;
|
|
517
887
|
return new URL(resource.url);
|
|
518
888
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,13 +6,16 @@ import type { BaseProviderPlugin } from "@rigkit/engine";
|
|
|
6
6
|
import * as z from "zod/v4-mini";
|
|
7
7
|
import { freestyleIdentityId, freestyleToken, freestyleTokenId } from "./auth.ts";
|
|
8
8
|
import {
|
|
9
|
+
checkFreestyleProviderAuth,
|
|
9
10
|
createFreestyleAuthenticatedClient,
|
|
10
11
|
createFreestyleProxyFetch,
|
|
12
|
+
freestyleProviderChecksFromAuthenticated,
|
|
11
13
|
type FreestyleProviderConfig,
|
|
12
14
|
} from "./host-auth.ts";
|
|
13
15
|
import {
|
|
14
16
|
FREESTYLE_PROVIDER_ID,
|
|
15
17
|
FREESTYLE_TERMINAL_PROVIDER_ID,
|
|
18
|
+
createLazyFreestyleWorkflowController,
|
|
16
19
|
createFreestyleTerminalController,
|
|
17
20
|
createFreestyleWorkflowProvider,
|
|
18
21
|
} from "./provider.ts";
|
|
@@ -65,15 +68,20 @@ export const freestyleProviderPlugin: BaseProviderPlugin = {
|
|
|
65
68
|
providerId: FREESTYLE_PROVIDER_ID,
|
|
66
69
|
async createProvider({ provider, hostStorage, local }) {
|
|
67
70
|
const config = parseFreestyleProviderConfig(provider.config);
|
|
68
|
-
|
|
71
|
+
let authenticated: ReturnType<typeof createFreestyleAuthenticatedClient> | undefined;
|
|
72
|
+
const authenticate = () => authenticated ??= createFreestyleAuthenticatedClient({
|
|
69
73
|
config,
|
|
70
74
|
hostStorage,
|
|
71
75
|
local,
|
|
72
76
|
});
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
return createLazyFreestyleWorkflowController({
|
|
78
|
+
authenticate,
|
|
79
|
+
checks: async ({ mode }) => {
|
|
80
|
+
if (mode === "require") {
|
|
81
|
+
return freestyleProviderChecksFromAuthenticated(await authenticate());
|
|
82
|
+
}
|
|
83
|
+
return checkFreestyleProviderAuth({ config, hostStorage });
|
|
84
|
+
},
|
|
77
85
|
});
|
|
78
86
|
},
|
|
79
87
|
};
|
|
@@ -86,9 +94,11 @@ export const freestyleTerminalPlugin: BaseProviderPlugin = {
|
|
|
86
94
|
};
|
|
87
95
|
|
|
88
96
|
export {
|
|
97
|
+
checkFreestyleProviderAuth,
|
|
89
98
|
createFreestyleAuthenticatedClient,
|
|
90
99
|
createFreestyleProxyFetch,
|
|
91
100
|
createFreestyleSdkFetch,
|
|
101
|
+
freestyleProviderChecksFromAuthenticated,
|
|
92
102
|
} from "./host-auth.ts";
|
|
93
103
|
export {
|
|
94
104
|
freestyleIdentityId,
|
|
@@ -102,6 +112,7 @@ export {
|
|
|
102
112
|
FREESTYLE_PROVIDER_ID,
|
|
103
113
|
FREESTYLE_TERMINAL_PROVIDER_ID,
|
|
104
114
|
createFreestyleTerminalController,
|
|
115
|
+
createLazyFreestyleWorkflowController,
|
|
105
116
|
createFreestyleWorkflowController,
|
|
106
117
|
createFreestyleWorkflowProvider,
|
|
107
118
|
} from "./provider.ts";
|
|
@@ -120,6 +131,7 @@ export type {
|
|
|
120
131
|
FreestyleVscodeUrlOptions,
|
|
121
132
|
} from "./provider.ts";
|
|
122
133
|
export type { FreestyleGitRelationship, FreestyleIdentity } from "./store.ts";
|
|
134
|
+
export type { FreestyleResolvedTeam } from "./host-auth.ts";
|
|
123
135
|
|
|
124
136
|
function parseFreestyleProviderConfig(value: unknown): FreestyleProviderConfig {
|
|
125
137
|
const result = z.safeParse(freestyleProviderConfigSchema, normalizeFreestyleProviderOptions(value));
|
package/src/provider.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { Freestyle } from "freestyle";
|
|
|
2
2
|
import type {
|
|
3
3
|
SshConnection,
|
|
4
4
|
SshOptions,
|
|
5
|
+
WorkflowProviderCheckResult,
|
|
5
6
|
WorkflowProviderController,
|
|
6
7
|
} from "@rigkit/engine";
|
|
7
8
|
import type { CmuxOpenSshInput } from "@rigkit/provider-cmux";
|
|
8
9
|
import type { FreestyleIdentityId, FreestyleToken } from "./auth.ts";
|
|
10
|
+
import type { FreestyleResolvedTeam } from "./host-auth.ts";
|
|
9
11
|
import { createFreestyleTerminalSession } from "./terminal-session.ts";
|
|
10
12
|
|
|
11
13
|
export const FREESTYLE_PROVIDER_ID = "freestyle";
|
|
@@ -55,6 +57,7 @@ export function createFreestyleWorkflowProvider(input: {
|
|
|
55
57
|
client: Freestyle;
|
|
56
58
|
identityId: FreestyleIdentityId;
|
|
57
59
|
token: FreestyleToken;
|
|
60
|
+
team?: FreestyleResolvedTeam;
|
|
58
61
|
}): WorkflowProviderController<FreestyleRuntime> {
|
|
59
62
|
return createFreestyleWorkflowController(input);
|
|
60
63
|
}
|
|
@@ -63,15 +66,50 @@ export function createFreestyleWorkflowController(input: {
|
|
|
63
66
|
client: Freestyle;
|
|
64
67
|
identityId: FreestyleIdentityId;
|
|
65
68
|
token: FreestyleToken;
|
|
69
|
+
team?: FreestyleResolvedTeam;
|
|
66
70
|
}): WorkflowProviderController<FreestyleRuntime> {
|
|
67
71
|
return {
|
|
68
72
|
providerId: FREESTYLE_PROVIDER_ID,
|
|
73
|
+
checks() {
|
|
74
|
+
if (!input.team) return undefined;
|
|
75
|
+
return {
|
|
76
|
+
id: "team",
|
|
77
|
+
label: "Freestyle team",
|
|
78
|
+
status: "ok",
|
|
79
|
+
value: formatFreestyleTeam(input.team),
|
|
80
|
+
detail: input.team.id,
|
|
81
|
+
fingerprint: `identity:${input.identityId}`,
|
|
82
|
+
metadata: {
|
|
83
|
+
teamId: input.team.id,
|
|
84
|
+
...(input.team.displayName ? { teamName: input.team.displayName } : {}),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
69
88
|
runtime() {
|
|
70
89
|
return createFreestyleRuntime(input);
|
|
71
90
|
},
|
|
72
91
|
};
|
|
73
92
|
}
|
|
74
93
|
|
|
94
|
+
export function createLazyFreestyleWorkflowController(input: {
|
|
95
|
+
authenticate(): Promise<{
|
|
96
|
+
client: Freestyle;
|
|
97
|
+
identityId: FreestyleIdentityId;
|
|
98
|
+
token: FreestyleToken;
|
|
99
|
+
team?: FreestyleResolvedTeam;
|
|
100
|
+
}>;
|
|
101
|
+
checks(context: { mode: "plan" | "require" }): Promise<WorkflowProviderCheckResult[]>;
|
|
102
|
+
}): WorkflowProviderController<FreestyleRuntime> {
|
|
103
|
+
return {
|
|
104
|
+
providerId: FREESTYLE_PROVIDER_ID,
|
|
105
|
+
checks: input.checks,
|
|
106
|
+
async runtime() {
|
|
107
|
+
const authenticated = await input.authenticate();
|
|
108
|
+
return createFreestyleRuntime(authenticated);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
75
113
|
export function createFreestyleTerminalController(): WorkflowProviderController<FreestyleTerminalRuntime> {
|
|
76
114
|
return {
|
|
77
115
|
providerId: FREESTYLE_TERMINAL_PROVIDER_ID,
|
|
@@ -140,6 +178,10 @@ function createFreestyleRuntime(input: {
|
|
|
140
178
|
|
|
141
179
|
const defaultFreestyleVmUser = "root";
|
|
142
180
|
|
|
181
|
+
function formatFreestyleTeam(team: FreestyleResolvedTeam): string {
|
|
182
|
+
return team.displayName ? `${team.displayName} (${team.id})` : team.id;
|
|
183
|
+
}
|
|
184
|
+
|
|
143
185
|
function freestyleSshConnection(vmId: string, token: FreestyleToken, user: string | undefined): SshConnection {
|
|
144
186
|
const userPart = `+${user ?? defaultFreestyleVmUser}`;
|
|
145
187
|
const username = `${vmId}${userPart}`;
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const RIGKIT_PROVIDER_FREESTYLE_VERSION = "0.2.
|
|
1
|
+
export const RIGKIT_PROVIDER_FREESTYLE_VERSION = "0.2.11";
|