@rigkit/provider-freestyle 0.2.10 → 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.10",
3
+ "version": "0.2.11",
4
+ "license": "MIT",
4
5
  "type": "module",
5
6
  "repository": {
6
7
  "type": "git",
@@ -17,9 +18,9 @@
17
18
  ],
18
19
  "dependencies": {
19
20
  "zod": "^4",
20
- "@rigkit/sdk": "0.2.10",
21
- "@rigkit/engine": "0.2.10",
22
- "@rigkit/provider-cmux": "0.2.10"
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
26
  "freestyle": "^0.1.52"
@@ -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,
@@ -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",
@@ -560,6 +705,14 @@ function providerContext(
560
705
  };
561
706
  }
562
707
 
708
+ function providerCheckContext(mode: "plan" | "require"): ProviderCheckContext {
709
+ return {
710
+ mode,
711
+ workflow: "workflow",
712
+ local: providerContext([]).local,
713
+ };
714
+ }
715
+
563
716
  function testFetch(
564
717
  handler: (
565
718
  resource: Parameters<typeof fetch>[0],
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";
@@ -29,6 +29,7 @@ export type FreestyleAuthenticatedClient = {
29
29
  identityId: ReturnType<typeof freestyleIdentityId>;
30
30
  tokenId: ReturnType<typeof freestyleTokenId>;
31
31
  token: ReturnType<typeof freestyleToken>;
32
+ team?: FreestyleResolvedTeam;
32
33
  };
33
34
 
34
35
  type CreateFreestyleAuthenticatedClientInput = {
@@ -43,6 +44,7 @@ type CreateFreestyleAuthenticatedClientInput = {
43
44
  type ResolvedClientAuth = {
44
45
  client: Freestyle;
45
46
  identityKey: string;
47
+ team?: FreestyleResolvedTeam;
46
48
  };
47
49
 
48
50
  type StackAuthConfig = {
@@ -58,6 +60,7 @@ type StackAuthState = {
58
60
  refreshToken: string;
59
61
  updatedAt: number;
60
62
  defaultTeamId?: string;
63
+ defaultTeamName?: string;
61
64
  accessToken?: string;
62
65
  accessTokenUpdatedAt?: number;
63
66
  };
@@ -73,6 +76,11 @@ type FreestyleTeam = {
73
76
  sandboxAccountId?: string | null;
74
77
  };
75
78
 
79
+ export type FreestyleResolvedTeam = {
80
+ id: string;
81
+ displayName?: string;
82
+ };
83
+
76
84
  export async function createFreestyleAuthenticatedClient(
77
85
  input: CreateFreestyleAuthenticatedClientInput,
78
86
  ): Promise<FreestyleAuthenticatedClient> {
@@ -85,6 +93,7 @@ export async function createFreestyleAuthenticatedClient(
85
93
  identityId: savedIdentity.identityId,
86
94
  tokenId: savedIdentity.tokenId,
87
95
  token: savedIdentity.token,
96
+ team: auth.team,
88
97
  };
89
98
  }
90
99
 
@@ -102,9 +111,103 @@ export async function createFreestyleAuthenticatedClient(
102
111
  identityId: createdIdentity.identityId,
103
112
  tokenId: createdIdentity.tokenId,
104
113
  token: createdIdentity.token,
114
+ team: auth.team,
105
115
  };
106
116
  }
107
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
+
108
211
  export function createFreestyleProxyFetch(input: {
109
212
  dashboardUrl: string;
110
213
  accessToken: string;
@@ -213,7 +316,7 @@ async function resolveClientAuth(input: CreateFreestyleAuthenticatedClientInput)
213
316
  ...(apiUrl ? { baseUrl: apiUrl } : {}),
214
317
  fetch: createFreestyleSdkFetch(fetchFn),
215
318
  }),
216
- identityKey: `api-key:${fingerprint({ apiUrl: apiUrl ?? "default", apiKey })}`,
319
+ identityKey: apiKeyIdentityKey({ apiUrl, apiKey }),
217
320
  };
218
321
  }
219
322
 
@@ -230,7 +333,7 @@ async function resolveClientAuth(input: CreateFreestyleAuthenticatedClientInput)
230
333
  timeoutMs: input.timeoutMs ?? DEFAULT_CLI_AUTH_TIMEOUT_MILLIS,
231
334
  pollIntervalMs: input.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MILLIS,
232
335
  });
233
- const teamId = await resolveTeamId({
336
+ const team = await resolveTeam({
234
337
  configuredTeamId: input.config?.teamId,
235
338
  stored: readStackAuthState(input.hostStorage.get(stackStateKey)?.value) ?? stored,
236
339
  accessToken: refreshed.accessToken,
@@ -238,6 +341,7 @@ async function resolveClientAuth(input: CreateFreestyleAuthenticatedClientInput)
238
341
  storage: input.hostStorage,
239
342
  storageKey: stackStateKey,
240
343
  fetch: fetchFn,
344
+ local: input.local,
241
345
  });
242
346
  const client = new Freestyle({
243
347
  apiKey: "rigkit-browser-auth",
@@ -245,19 +349,20 @@ async function resolveClientAuth(input: CreateFreestyleAuthenticatedClientInput)
245
349
  fetch: createFreestyleProxyFetch({
246
350
  dashboardUrl: stack.dashboardUrl,
247
351
  accessToken: refreshed.accessToken,
248
- teamId,
352
+ teamId: team.id,
249
353
  fetch: fetchFn,
250
354
  }),
251
355
  });
252
356
 
253
357
  return {
254
358
  client,
255
- identityKey: `browser:${fingerprint({
256
- apiUrl: apiUrl ?? "default",
359
+ identityKey: browserIdentityKey({
360
+ apiUrl,
257
361
  dashboardUrl: stack.dashboardUrl,
258
362
  profile: stack.profile,
259
- teamId,
260
- })}`,
363
+ teamId: team.id,
364
+ }),
365
+ team,
261
366
  };
262
367
  }
263
368
 
@@ -470,6 +575,7 @@ async function resolveStackAccessToken(input: {
470
575
  saveStackAuthState(input.storage, input.storageKey, {
471
576
  refreshToken,
472
577
  defaultTeamId: input.stored?.defaultTeamId,
578
+ defaultTeamName: input.stored?.defaultTeamName,
473
579
  updatedAt: Date.now(),
474
580
  });
475
581
  }
@@ -481,6 +587,7 @@ async function resolveStackAccessToken(input: {
481
587
  saveStackAuthState(input.storage, input.storageKey, {
482
588
  refreshToken,
483
589
  defaultTeamId: input.stored?.defaultTeamId,
590
+ defaultTeamName: input.stored?.defaultTeamName,
484
591
  updatedAt: Date.now(),
485
592
  });
486
593
  refreshed = await refreshStackAccessToken(input.config, refreshToken, input.fetch);
@@ -494,6 +601,7 @@ async function resolveStackAccessToken(input: {
494
601
  saveStackAuthState(input.storage, input.storageKey, {
495
602
  refreshToken: nextRefreshToken,
496
603
  defaultTeamId: input.stored?.defaultTeamId,
604
+ defaultTeamName: input.stored?.defaultTeamName,
497
605
  accessToken: refreshed.accessToken,
498
606
  accessTokenUpdatedAt: Date.now(),
499
607
  updatedAt: Date.now(),
@@ -589,7 +697,7 @@ async function refreshStackAccessToken(
589
697
  };
590
698
  }
591
699
 
592
- async function resolveTeamId(input: {
700
+ async function resolveTeam(input: {
593
701
  configuredTeamId: string | undefined;
594
702
  stored: StackAuthState | undefined;
595
703
  accessToken: string;
@@ -597,19 +705,27 @@ async function resolveTeamId(input: {
597
705
  storage: ProviderStorage;
598
706
  storageKey: string;
599
707
  fetch: typeof fetch;
600
- }): Promise<string> {
708
+ local: LocalWorkspaceRuntime;
709
+ }): Promise<FreestyleResolvedTeam> {
601
710
  const teamId =
602
711
  nonEmpty(input.configuredTeamId) ??
603
712
  nonEmpty(process.env.FREESTYLE_TEAM_ID) ??
604
713
  nonEmpty(input.stored?.defaultTeamId);
605
714
  if (teamId) {
715
+ const storedTeamName = teamId === input.stored?.defaultTeamId
716
+ ? input.stored?.defaultTeamName
717
+ : undefined;
606
718
  saveStackAuthState(input.storage, input.storageKey, {
607
719
  ...input.stored,
608
720
  refreshToken: input.stored?.refreshToken ?? "",
609
721
  defaultTeamId: teamId,
722
+ defaultTeamName: storedTeamName,
610
723
  updatedAt: Date.now(),
611
724
  });
612
- return teamId;
725
+ return {
726
+ id: teamId,
727
+ ...(storedTeamName ? { displayName: storedTeamName } : {}),
728
+ };
613
729
  }
614
730
 
615
731
  const teams = await listTeams(input.config, input.accessToken, input.fetch);
@@ -619,19 +735,68 @@ async function resolveTeamId(input: {
619
735
  ...input.stored,
620
736
  refreshToken: input.stored?.refreshToken ?? "",
621
737
  defaultTeamId: onlyTeam.id,
738
+ defaultTeamName: nonEmpty(onlyTeam.displayName),
622
739
  updatedAt: Date.now(),
623
740
  });
624
- return onlyTeam.id;
741
+ return freestyleResolvedTeam(onlyTeam);
625
742
  }
626
743
 
627
744
  if (teams.length === 0) {
628
745
  throw new Error("Freestyle authentication succeeded, but no teams were available for this account.");
629
746
  }
630
747
 
631
- const choices = teams.map((team) => `${team.displayName ?? team.id} (${team.id})`).join(", ");
632
- throw new Error(
633
- `Freestyle authentication found multiple teams. Set freestyle.provider({ teamId }) or FREESTYLE_TEAM_ID. Teams: ${choices}`,
634
- );
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"}`;
635
800
  }
636
801
 
637
802
  async function listTeams(config: StackAuthConfig, accessToken: string, fetchFn: typeof fetch): Promise<FreestyleTeam[]> {
@@ -672,6 +837,24 @@ function stackAuthStateKey(config: StackAuthConfig): string {
672
837
  })}`;
673
838
  }
674
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
+
675
858
  function readStackAuthState(value: JsonValue | undefined): StackAuthState | undefined {
676
859
  if (!isRecord(value)) return undefined;
677
860
  const refreshToken = typeof value.refreshToken === "string" ? value.refreshToken : undefined;
@@ -680,6 +863,7 @@ function readStackAuthState(value: JsonValue | undefined): StackAuthState | unde
680
863
  refreshToken,
681
864
  updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : Date.now(),
682
865
  defaultTeamId: typeof value.defaultTeamId === "string" ? value.defaultTeamId : undefined,
866
+ defaultTeamName: typeof value.defaultTeamName === "string" ? value.defaultTeamName : undefined,
683
867
  accessToken: typeof value.accessToken === "string" ? value.accessToken : undefined,
684
868
  accessTokenUpdatedAt: typeof value.accessTokenUpdatedAt === "number" ? value.accessTokenUpdatedAt : undefined,
685
869
  };
@@ -691,6 +875,7 @@ function saveStackAuthState(storage: ProviderStorage, key: string, state: StackA
691
875
  refreshToken: state.refreshToken,
692
876
  updatedAt: state.updatedAt,
693
877
  ...(state.defaultTeamId ? { defaultTeamId: state.defaultTeamId } : {}),
878
+ ...(state.defaultTeamName ? { defaultTeamName: state.defaultTeamName } : {}),
694
879
  ...(state.accessToken ? { accessToken: state.accessToken } : {}),
695
880
  ...(state.accessTokenUpdatedAt ? { accessTokenUpdatedAt: state.accessTokenUpdatedAt } : {}),
696
881
  });
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.10";
1
+ export const RIGKIT_PROVIDER_FREESTYLE_VERSION = "0.2.11";