@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 +21 -0
- package/package.json +5 -4
- package/src/host-auth.test.ts +156 -3
- package/src/host-auth.ts +201 -16
- 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,9 +18,9 @@
|
|
|
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
26
|
"freestyle": "^0.1.52"
|
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,
|
|
@@ -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",
|
|
@@ -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:
|
|
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
|
|
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:
|
|
256
|
-
apiUrl
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
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";
|