@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 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.9",
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.9",
21
- "@rigkit/engine": "0.2.9",
22
- "@rigkit/provider-cmux": "0.2.9"
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.51"
26
+ "freestyle": "^0.1.52"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@types/bun": "latest",
29
- "freestyle": "^0.1.51",
30
+ "freestyle": "^0.1.52",
30
31
  "typescript": "latest"
31
32
  },
32
33
  "publishConfig": {
@@ -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(1);
96
- expect(requests).toHaveLength(2);
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
- const response = await proxyFetch("https://api.freestyle.sh/v1/vms", {
360
- method: "POST",
361
- body: "{}",
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.status).toBe(500);
365
- await expect(response.json()).resolves.toEqual({
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
- const response = await proxyFetch("https://api.freestyle.sh/v1/vms");
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.status).toBe(500);
395
- await expect(response.json()).resolves.toEqual({
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?.method ?? "GET",
130
- headers: Object.fromEntries(withRigkitHeaders(init?.headers).entries()),
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 rigkitFetch = (async (resource, init) =>
166
- await fetchFn(resource, {
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
- })) as typeof fetch;
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: `api-key:${fingerprint({ apiUrl: apiUrl ?? "default", apiKey })}`,
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 teamId = await resolveTeamId({
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: `browser:${fingerprint({
227
- apiUrl: apiUrl ?? "default",
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 resolveTeamId(input: {
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
- }): Promise<string> {
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 teamId;
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.id;
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 choices = teams.map((team) => `${team.displayName ?? team.id} (${team.id})`).join(", ");
447
- throw new Error(
448
- `Freestyle authentication found multiple teams. Set freestyle.provider({ teamId }) or FREESTYLE_TEAM_ID. Teams: ${choices}`,
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
- const authenticated = await createFreestyleAuthenticatedClient({
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 createFreestyleWorkflowProvider({
74
- client: authenticated.client,
75
- identityId: authenticated.identityId,
76
- token: authenticated.token,
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.9";
1
+ export const RIGKIT_PROVIDER_FREESTYLE_VERSION = "0.2.11";