@sandbox0/dashboard-core 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +53 -0
- package/src/auth.ts +344 -0
- package/src/config.ts +60 -0
- package/src/index.ts +5 -0
- package/src/sdk.ts +65 -0
- package/src/session.ts +298 -0
- package/src/types.ts +75 -0
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sandbox0/dashboard-core",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Shared Sandbox0 dashboard auth and control-plane core",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"src/auth.ts",
|
|
10
|
+
"src/config.ts",
|
|
11
|
+
"src/index.ts",
|
|
12
|
+
"src/sdk.ts",
|
|
13
|
+
"src/session.ts",
|
|
14
|
+
"src/types.ts"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./src/index.ts",
|
|
19
|
+
"import": "./src/index.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"test": "tsx --test src/**/*.test.ts"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/sandbox0-ai/sandbox0",
|
|
32
|
+
"directory": "sandbox0-ui/components/dashboard-core"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"provenance": true
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"next": "^15.1.3",
|
|
40
|
+
"react": "^19.0.0",
|
|
41
|
+
"react-dom": "^19.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22.10.2",
|
|
45
|
+
"@types/react": "^19.0.2",
|
|
46
|
+
"@types/react-dom": "^19.0.2",
|
|
47
|
+
"tsx": "^4.21.0",
|
|
48
|
+
"typescript": "^5.7.2"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"sandbox0": "^0.1.5"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import { resolveDashboardControlPlaneURL } from "./config";
|
|
4
|
+
import { createDashboardControlPlaneSDK, resolveSDKErrorMessage } from "./sdk";
|
|
5
|
+
import type { DashboardAuthProvider, DashboardRuntimeConfig } from "./types";
|
|
6
|
+
|
|
7
|
+
interface LoginResponse {
|
|
8
|
+
access_token: string;
|
|
9
|
+
refresh_token: string;
|
|
10
|
+
expires_at: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const dashboardAccessTokenCookieName = "sandbox0_access_token";
|
|
14
|
+
export const dashboardRefreshTokenCookieName = "sandbox0_refresh_token";
|
|
15
|
+
|
|
16
|
+
function joinURL(baseURL: string, path: string): string {
|
|
17
|
+
const base = new URL(baseURL);
|
|
18
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
19
|
+
return new URL(
|
|
20
|
+
normalizedPath,
|
|
21
|
+
`${base.toString().replace(/\/$/, "")}/`,
|
|
22
|
+
).toString();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toLoginResponse(data: {
|
|
26
|
+
accessToken: string;
|
|
27
|
+
refreshToken: string;
|
|
28
|
+
expiresAt: number;
|
|
29
|
+
}): LoginResponse {
|
|
30
|
+
return {
|
|
31
|
+
access_token: data.accessToken,
|
|
32
|
+
refresh_token: data.refreshToken,
|
|
33
|
+
expires_at: data.expiresAt,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function dashboardCookieNames() {
|
|
38
|
+
return {
|
|
39
|
+
accessToken: dashboardAccessTokenCookieName,
|
|
40
|
+
refreshToken: dashboardRefreshTokenCookieName,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function resolveDashboardAuthProviders(
|
|
45
|
+
config: DashboardRuntimeConfig,
|
|
46
|
+
fetchImpl: typeof fetch = fetch,
|
|
47
|
+
): Promise<{ providers: DashboardAuthProvider[]; errors: string[] }> {
|
|
48
|
+
const baseURL = resolveDashboardControlPlaneURL(config);
|
|
49
|
+
if (!baseURL) {
|
|
50
|
+
return {
|
|
51
|
+
providers: [],
|
|
52
|
+
errors: ["dashboard auth is missing a control-plane base URL"],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const sdk = await createDashboardControlPlaneSDK(baseURL, {
|
|
58
|
+
fetch: fetchImpl,
|
|
59
|
+
});
|
|
60
|
+
const response = await sdk.auth.authProvidersGet();
|
|
61
|
+
const providers = (response.data?.providers ?? []).flatMap((provider) => {
|
|
62
|
+
if (provider.type !== "oidc" && provider.type !== "builtin") {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
id: provider.id,
|
|
69
|
+
name: provider.name,
|
|
70
|
+
type: provider.type,
|
|
71
|
+
} satisfies DashboardAuthProvider,
|
|
72
|
+
];
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return { providers, errors: [] };
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
providers: [],
|
|
79
|
+
errors: [
|
|
80
|
+
await resolveSDKErrorMessage(
|
|
81
|
+
error,
|
|
82
|
+
"failed to resolve auth providers",
|
|
83
|
+
),
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function exchangeBuiltinLogin(
|
|
90
|
+
config: DashboardRuntimeConfig,
|
|
91
|
+
email: string,
|
|
92
|
+
password: string,
|
|
93
|
+
fetchImpl: typeof fetch = fetch,
|
|
94
|
+
): Promise<{ tokens?: LoginResponse; error?: string }> {
|
|
95
|
+
const baseURL = resolveDashboardControlPlaneURL(config);
|
|
96
|
+
if (!baseURL) {
|
|
97
|
+
return { error: "dashboard auth is missing a control-plane base URL" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const sdk = await createDashboardControlPlaneSDK(baseURL, {
|
|
102
|
+
fetch: fetchImpl,
|
|
103
|
+
});
|
|
104
|
+
const response = await sdk.auth.authLoginPost({
|
|
105
|
+
loginRequest: { email, password },
|
|
106
|
+
});
|
|
107
|
+
if (!response.data) {
|
|
108
|
+
return { error: "/auth/login returned an empty response" };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { tokens: toLoginResponse(response.data) };
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
error: await resolveSDKErrorMessage(error, "failed to complete login"),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function exchangeRefreshToken(
|
|
120
|
+
config: DashboardRuntimeConfig,
|
|
121
|
+
refreshToken: string,
|
|
122
|
+
fetchImpl: typeof fetch = fetch,
|
|
123
|
+
): Promise<{ tokens?: LoginResponse; error?: string }> {
|
|
124
|
+
const baseURL = resolveDashboardControlPlaneURL(config);
|
|
125
|
+
if (!baseURL) {
|
|
126
|
+
return { error: "dashboard auth is missing a control-plane base URL" };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const sdk = await createDashboardControlPlaneSDK(baseURL, {
|
|
131
|
+
fetch: fetchImpl,
|
|
132
|
+
});
|
|
133
|
+
const response = await sdk.auth.authRefreshPost({
|
|
134
|
+
refreshRequest: { refreshToken },
|
|
135
|
+
});
|
|
136
|
+
if (!response.data) {
|
|
137
|
+
return { error: "/auth/refresh returned an empty response" };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { tokens: toLoginResponse(response.data) };
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return {
|
|
143
|
+
error: await resolveSDKErrorMessage(error, "failed to refresh session"),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function updateDefaultTeam(
|
|
149
|
+
config: DashboardRuntimeConfig,
|
|
150
|
+
accessToken: string,
|
|
151
|
+
teamID: string,
|
|
152
|
+
fetchImpl: typeof fetch = fetch,
|
|
153
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
154
|
+
const baseURL = resolveDashboardControlPlaneURL(config);
|
|
155
|
+
if (!baseURL) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
error: "dashboard auth is missing a control-plane base URL",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const sdk = await createDashboardControlPlaneSDK(baseURL, {
|
|
164
|
+
token: accessToken,
|
|
165
|
+
fetch: fetchImpl,
|
|
166
|
+
});
|
|
167
|
+
await sdk.users.tenantActivePut({
|
|
168
|
+
updateUserRequest: { defaultTeamId: teamID },
|
|
169
|
+
});
|
|
170
|
+
return { ok: true };
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
error: await resolveSDKErrorMessage(
|
|
175
|
+
error,
|
|
176
|
+
"failed to update default team",
|
|
177
|
+
),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function exchangeOIDCCallback(
|
|
183
|
+
config: DashboardRuntimeConfig,
|
|
184
|
+
providerID: string,
|
|
185
|
+
rawQuery: string,
|
|
186
|
+
fetchImpl: typeof fetch = fetch,
|
|
187
|
+
): Promise<{ tokens?: LoginResponse; error?: string }> {
|
|
188
|
+
const baseURL = resolveDashboardControlPlaneURL(config);
|
|
189
|
+
if (!baseURL) {
|
|
190
|
+
return { error: "dashboard auth is missing a control-plane base URL" };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const path = `/auth/oidc/${encodeURIComponent(providerID)}/callback${rawQuery}`;
|
|
195
|
+
const response = await fetchImpl(joinURL(baseURL, path), {
|
|
196
|
+
method: "GET",
|
|
197
|
+
cache: "no-store",
|
|
198
|
+
});
|
|
199
|
+
const payload = (await response.json().catch(() => null)) as
|
|
200
|
+
| {
|
|
201
|
+
data?: LoginResponse;
|
|
202
|
+
error?: {
|
|
203
|
+
message?: string;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
| null;
|
|
207
|
+
if (!response.ok || !payload?.data) {
|
|
208
|
+
return {
|
|
209
|
+
error:
|
|
210
|
+
payload?.error?.message ??
|
|
211
|
+
`/auth/oidc/${providerID}/callback returned ${response.status}`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { tokens: payload.data };
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return {
|
|
218
|
+
error:
|
|
219
|
+
error instanceof Error
|
|
220
|
+
? error.message
|
|
221
|
+
: "failed to complete oidc login",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function resolveOIDCLoginLocation(
|
|
227
|
+
config: DashboardRuntimeConfig,
|
|
228
|
+
providerID: string,
|
|
229
|
+
fetchImpl: typeof fetch = fetch,
|
|
230
|
+
): Promise<{ location?: string; error?: string }> {
|
|
231
|
+
const baseURL = resolveDashboardControlPlaneURL(config);
|
|
232
|
+
if (!baseURL) {
|
|
233
|
+
return { error: "dashboard auth is missing a control-plane base URL" };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const response = await fetchImpl(
|
|
238
|
+
joinURL(
|
|
239
|
+
baseURL,
|
|
240
|
+
`/auth/oidc/${encodeURIComponent(providerID)}/login?return_url=${encodeURIComponent(config.dashboardBasePath)}`,
|
|
241
|
+
),
|
|
242
|
+
{
|
|
243
|
+
method: "GET",
|
|
244
|
+
redirect: "manual",
|
|
245
|
+
cache: "no-store",
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (response.status < 300 || response.status >= 400) {
|
|
250
|
+
const payload = (await response.json().catch(() => null)) as
|
|
251
|
+
| {
|
|
252
|
+
error?: {
|
|
253
|
+
message?: string;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
| null;
|
|
257
|
+
return {
|
|
258
|
+
error:
|
|
259
|
+
payload?.error?.message ??
|
|
260
|
+
`/auth/oidc/${providerID}/login returned ${response.status}`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const location = response.headers.get("location");
|
|
265
|
+
if (!location) {
|
|
266
|
+
return { error: "oidc login did not return a redirect location" };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { location };
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return {
|
|
272
|
+
error:
|
|
273
|
+
error instanceof Error
|
|
274
|
+
? error.message
|
|
275
|
+
: "failed to initiate oidc login",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function forwardLogout(
|
|
281
|
+
config: DashboardRuntimeConfig,
|
|
282
|
+
accessToken: string | undefined,
|
|
283
|
+
fetchImpl: typeof fetch = fetch,
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
const baseURL = resolveDashboardControlPlaneURL(config);
|
|
286
|
+
if (!baseURL || !accessToken) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const sdk = await createDashboardControlPlaneSDK(baseURL, {
|
|
292
|
+
token: accessToken,
|
|
293
|
+
fetch: fetchImpl,
|
|
294
|
+
});
|
|
295
|
+
await sdk.auth.authLogoutPost();
|
|
296
|
+
} catch {
|
|
297
|
+
// Ignore upstream logout failures and clear browser cookies locally.
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function setDashboardAuthCookies(
|
|
302
|
+
response: NextResponse,
|
|
303
|
+
config: DashboardRuntimeConfig,
|
|
304
|
+
tokens: LoginResponse,
|
|
305
|
+
): void {
|
|
306
|
+
const secure = config.siteURL.startsWith("https://");
|
|
307
|
+
const maxAge = Math.max(0, tokens.expires_at - Math.floor(Date.now() / 1000));
|
|
308
|
+
|
|
309
|
+
response.cookies.set(dashboardAccessTokenCookieName, tokens.access_token, {
|
|
310
|
+
httpOnly: true,
|
|
311
|
+
sameSite: "lax",
|
|
312
|
+
secure,
|
|
313
|
+
path: config.dashboardBasePath,
|
|
314
|
+
maxAge,
|
|
315
|
+
});
|
|
316
|
+
response.cookies.set(dashboardRefreshTokenCookieName, tokens.refresh_token, {
|
|
317
|
+
httpOnly: true,
|
|
318
|
+
sameSite: "lax",
|
|
319
|
+
secure,
|
|
320
|
+
path: config.dashboardBasePath,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function clearDashboardAuthCookies(
|
|
325
|
+
response: NextResponse,
|
|
326
|
+
config: DashboardRuntimeConfig,
|
|
327
|
+
): void {
|
|
328
|
+
const secure = config.siteURL.startsWith("https://");
|
|
329
|
+
|
|
330
|
+
response.cookies.set(dashboardAccessTokenCookieName, "", {
|
|
331
|
+
httpOnly: true,
|
|
332
|
+
sameSite: "lax",
|
|
333
|
+
secure,
|
|
334
|
+
path: config.dashboardBasePath,
|
|
335
|
+
maxAge: 0,
|
|
336
|
+
});
|
|
337
|
+
response.cookies.set(dashboardRefreshTokenCookieName, "", {
|
|
338
|
+
httpOnly: true,
|
|
339
|
+
sameSite: "lax",
|
|
340
|
+
secure,
|
|
341
|
+
path: config.dashboardBasePath,
|
|
342
|
+
maxAge: 0,
|
|
343
|
+
});
|
|
344
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DashboardControlPlaneMode,
|
|
3
|
+
DashboardRuntimeConfig,
|
|
4
|
+
} from "./types";
|
|
5
|
+
|
|
6
|
+
const defaultDashboardBasePath = "/dashboard";
|
|
7
|
+
|
|
8
|
+
function normalizeMode(value: string | undefined): DashboardControlPlaneMode {
|
|
9
|
+
if (value === "global-directory") {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
return "single-cluster";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function defaultSiteURL(nodeEnv: string | undefined): string {
|
|
16
|
+
if (nodeEnv === "development") {
|
|
17
|
+
return "http://localhost:4300";
|
|
18
|
+
}
|
|
19
|
+
return "https://sandbox0.ai";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveDashboardRuntimeConfig(
|
|
23
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
24
|
+
): DashboardRuntimeConfig {
|
|
25
|
+
const mode = normalizeMode(env.SANDBOX0_DASHBOARD_MODE);
|
|
26
|
+
const siteURL =
|
|
27
|
+
env.SANDBOX0_DASHBOARD_SITE_URL ?? defaultSiteURL(env.NODE_ENV);
|
|
28
|
+
|
|
29
|
+
if (mode === "global-directory") {
|
|
30
|
+
return {
|
|
31
|
+
mode,
|
|
32
|
+
dashboardBasePath: defaultDashboardBasePath,
|
|
33
|
+
siteURL,
|
|
34
|
+
globalDirectoryURL:
|
|
35
|
+
env.SANDBOX0_DASHBOARD_GLOBAL_DIRECTORY_URL ??
|
|
36
|
+
env.SANDBOX0_BASE_URL ??
|
|
37
|
+
"https://api.sandbox0.ai",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
mode,
|
|
43
|
+
dashboardBasePath: defaultDashboardBasePath,
|
|
44
|
+
siteURL,
|
|
45
|
+
singleClusterURL:
|
|
46
|
+
env.SANDBOX0_DASHBOARD_SINGLE_CLUSTER_URL ??
|
|
47
|
+
env.SANDBOX0_BASE_URL ??
|
|
48
|
+
"http://localhost:30080",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveDashboardControlPlaneURL(
|
|
53
|
+
config: DashboardRuntimeConfig,
|
|
54
|
+
): string | undefined {
|
|
55
|
+
if (config.mode === "global-directory") {
|
|
56
|
+
return config.globalDirectoryURL;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return config.singleClusterURL;
|
|
60
|
+
}
|
package/src/index.ts
ADDED
package/src/sdk.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
type FetchLike = typeof fetch;
|
|
2
|
+
type Sandbox0Module = typeof import("sandbox0");
|
|
3
|
+
|
|
4
|
+
export interface DashboardControlPlaneSDK {
|
|
5
|
+
auth: InstanceType<Sandbox0Module["apis"]["AuthApi"]>;
|
|
6
|
+
users: InstanceType<Sandbox0Module["apis"]["UsersApi"]>;
|
|
7
|
+
teams: InstanceType<Sandbox0Module["apis"]["TeamsApi"]>;
|
|
8
|
+
tenant: InstanceType<Sandbox0Module["apis"]["TenantApi"]>;
|
|
9
|
+
regions: InstanceType<Sandbox0Module["apis"]["RegionsApi"]>;
|
|
10
|
+
sandboxes: InstanceType<Sandbox0Module["apis"]["SandboxesApi"]>;
|
|
11
|
+
templates: InstanceType<Sandbox0Module["apis"]["TemplatesApi"]>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function loadSandbox0(): Promise<Sandbox0Module> {
|
|
15
|
+
return import("sandbox0");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function createDashboardControlPlaneSDK(
|
|
19
|
+
baseURL: string,
|
|
20
|
+
options?: {
|
|
21
|
+
token?: string;
|
|
22
|
+
fetch?: FetchLike;
|
|
23
|
+
},
|
|
24
|
+
): Promise<DashboardControlPlaneSDK> {
|
|
25
|
+
const { apis, runtime } = await loadSandbox0();
|
|
26
|
+
const configuration = new runtime.Configuration({
|
|
27
|
+
basePath: baseURL,
|
|
28
|
+
accessToken: options?.token ? async () => options.token ?? "" : undefined,
|
|
29
|
+
fetchApi: options?.fetch,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
auth: new apis.AuthApi(configuration),
|
|
34
|
+
users: new apis.UsersApi(configuration),
|
|
35
|
+
teams: new apis.TeamsApi(configuration),
|
|
36
|
+
tenant: new apis.TenantApi(configuration),
|
|
37
|
+
regions: new apis.RegionsApi(configuration),
|
|
38
|
+
sandboxes: new apis.SandboxesApi(configuration),
|
|
39
|
+
templates: new apis.TemplatesApi(configuration),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function resolveSDKErrorMessage(
|
|
44
|
+
error: unknown,
|
|
45
|
+
fallback: string,
|
|
46
|
+
): Promise<string> {
|
|
47
|
+
const { runtime } = await loadSandbox0();
|
|
48
|
+
|
|
49
|
+
if (error instanceof runtime.ResponseError) {
|
|
50
|
+
const payload = (await error.response
|
|
51
|
+
.clone()
|
|
52
|
+
.json()
|
|
53
|
+
.catch(() => null)) as
|
|
54
|
+
| {
|
|
55
|
+
error?: {
|
|
56
|
+
message?: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
| null;
|
|
60
|
+
|
|
61
|
+
return payload?.error?.message ?? fallback;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return error instanceof Error ? error.message : fallback;
|
|
65
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DashboardActiveTeam,
|
|
3
|
+
DashboardRuntimeConfig,
|
|
4
|
+
DashboardSandboxSummary,
|
|
5
|
+
DashboardSession,
|
|
6
|
+
DashboardTeam,
|
|
7
|
+
DashboardTemplateSummary,
|
|
8
|
+
DashboardUser,
|
|
9
|
+
} from "./types";
|
|
10
|
+
import { dashboardAccessTokenCookieName } from "./auth";
|
|
11
|
+
import { createDashboardControlPlaneSDK, resolveSDKErrorMessage } from "./sdk";
|
|
12
|
+
|
|
13
|
+
export interface SessionAuthInput {
|
|
14
|
+
bearerToken?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type FetchLike = typeof fetch;
|
|
18
|
+
|
|
19
|
+
function toUser(user: {
|
|
20
|
+
id: string;
|
|
21
|
+
email: string;
|
|
22
|
+
name: string;
|
|
23
|
+
avatarUrl?: string | null;
|
|
24
|
+
defaultTeamId?: string | null;
|
|
25
|
+
emailVerified: boolean;
|
|
26
|
+
isAdmin: boolean;
|
|
27
|
+
}): DashboardUser {
|
|
28
|
+
return {
|
|
29
|
+
id: user.id,
|
|
30
|
+
email: user.email,
|
|
31
|
+
name: user.name,
|
|
32
|
+
avatarUrl: user.avatarUrl ?? null,
|
|
33
|
+
defaultTeamID: user.defaultTeamId ?? null,
|
|
34
|
+
emailVerified: user.emailVerified,
|
|
35
|
+
isAdmin: user.isAdmin,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toTeams(
|
|
40
|
+
teams: Array<{
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
slug: string;
|
|
44
|
+
ownerId?: string | null;
|
|
45
|
+
homeRegionId?: string | null;
|
|
46
|
+
}> = [],
|
|
47
|
+
): DashboardTeam[] {
|
|
48
|
+
return teams.map((team) => ({
|
|
49
|
+
id: team.id,
|
|
50
|
+
name: team.name,
|
|
51
|
+
slug: team.slug,
|
|
52
|
+
ownerID: team.ownerId ?? null,
|
|
53
|
+
homeRegionID: team.homeRegionId ?? null,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toSandboxes(
|
|
58
|
+
sandboxes: Array<{
|
|
59
|
+
id: string;
|
|
60
|
+
templateId: string;
|
|
61
|
+
status: string;
|
|
62
|
+
paused: boolean;
|
|
63
|
+
clusterId?: string | null;
|
|
64
|
+
createdAt: Date;
|
|
65
|
+
expiresAt: Date;
|
|
66
|
+
}> = [],
|
|
67
|
+
): DashboardSandboxSummary[] {
|
|
68
|
+
return sandboxes.map((sandbox) => ({
|
|
69
|
+
id: sandbox.id,
|
|
70
|
+
templateID: sandbox.templateId,
|
|
71
|
+
status: sandbox.status,
|
|
72
|
+
paused: sandbox.paused,
|
|
73
|
+
clusterID: sandbox.clusterId ?? null,
|
|
74
|
+
createdAt: sandbox.createdAt.toISOString(),
|
|
75
|
+
expiresAt: sandbox.expiresAt.toISOString(),
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toTemplates(
|
|
80
|
+
templates: Array<{
|
|
81
|
+
templateId: string;
|
|
82
|
+
scope: string;
|
|
83
|
+
createdAt: Date;
|
|
84
|
+
}> = [],
|
|
85
|
+
): DashboardTemplateSummary[] {
|
|
86
|
+
return templates.map((template) => ({
|
|
87
|
+
templateID: template.templateId,
|
|
88
|
+
scope: template.scope,
|
|
89
|
+
createdAt: template.createdAt.toISOString(),
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function deriveSingleClusterActiveTeam(
|
|
94
|
+
user: DashboardUser,
|
|
95
|
+
teams: DashboardTeam[],
|
|
96
|
+
regionalURL: string,
|
|
97
|
+
): DashboardActiveTeam | undefined {
|
|
98
|
+
const defaultTeam =
|
|
99
|
+
teams.find((team) => team.id === user.defaultTeamID) ?? teams[0];
|
|
100
|
+
if (!defaultTeam) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
userID: user.id,
|
|
106
|
+
teamID: defaultTeam.id,
|
|
107
|
+
homeRegionID: defaultTeam.homeRegionID ?? "local",
|
|
108
|
+
defaultTeam: defaultTeam.id === user.defaultTeamID,
|
|
109
|
+
edgeGatewayURL: regionalURL,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function readBearerToken(
|
|
114
|
+
authorizationHeader: string | null,
|
|
115
|
+
cookies: Pick<{ get(name: string): { value: string } | undefined }, "get">,
|
|
116
|
+
): string | undefined {
|
|
117
|
+
if (authorizationHeader?.startsWith("Bearer ")) {
|
|
118
|
+
return authorizationHeader.slice("Bearer ".length).trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const cookieNames = [
|
|
122
|
+
"__Host-sandbox0_access_token",
|
|
123
|
+
dashboardAccessTokenCookieName,
|
|
124
|
+
"sandbox0_token",
|
|
125
|
+
];
|
|
126
|
+
for (const cookieName of cookieNames) {
|
|
127
|
+
const token = cookies.get(cookieName)?.value?.trim();
|
|
128
|
+
if (token) {
|
|
129
|
+
return token;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function resolveDashboardSession(
|
|
137
|
+
config: DashboardRuntimeConfig,
|
|
138
|
+
auth: SessionAuthInput,
|
|
139
|
+
fetchImpl: FetchLike = fetch,
|
|
140
|
+
): Promise<DashboardSession> {
|
|
141
|
+
const baseSession: DashboardSession = {
|
|
142
|
+
authenticated: false,
|
|
143
|
+
mode: config.mode,
|
|
144
|
+
dashboardBasePath: config.dashboardBasePath,
|
|
145
|
+
siteURL: config.siteURL,
|
|
146
|
+
configuredGlobalURL: config.globalDirectoryURL,
|
|
147
|
+
configuredRegionalURL:
|
|
148
|
+
config.mode === "single-cluster" ? config.singleClusterURL : undefined,
|
|
149
|
+
teams: [],
|
|
150
|
+
sandboxes: [],
|
|
151
|
+
templates: [],
|
|
152
|
+
errors: [],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const token = auth.bearerToken?.trim();
|
|
156
|
+
if (!token) {
|
|
157
|
+
return baseSession;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (config.mode === "single-cluster") {
|
|
161
|
+
const baseURL = config.singleClusterURL;
|
|
162
|
+
if (!baseURL) {
|
|
163
|
+
return {
|
|
164
|
+
...baseSession,
|
|
165
|
+
errors: ["single-cluster mode is missing a control-plane base URL"],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const sdk = await createDashboardControlPlaneSDK(baseURL, {
|
|
171
|
+
token,
|
|
172
|
+
fetch: fetchImpl,
|
|
173
|
+
});
|
|
174
|
+
const [userResponse, teamResponse, sandboxResponse, templateResponse] =
|
|
175
|
+
await Promise.all([
|
|
176
|
+
sdk.users.usersMeGet(),
|
|
177
|
+
sdk.teams.teamsGet(),
|
|
178
|
+
sdk.sandboxes.apiV1SandboxesGet({ limit: 5 }),
|
|
179
|
+
sdk.templates.apiV1TemplatesGet(),
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
const userData = userResponse.data;
|
|
183
|
+
if (!userData) {
|
|
184
|
+
throw new Error("/users/me returned an empty response");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const user = toUser(userData);
|
|
188
|
+
const teams = toTeams(teamResponse.data?.teams);
|
|
189
|
+
const activeTeam = deriveSingleClusterActiveTeam(user, teams, baseURL);
|
|
190
|
+
const sandboxes = toSandboxes(sandboxResponse.data?.sandboxes);
|
|
191
|
+
const templates = toTemplates(templateResponse.data?.templates);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
...baseSession,
|
|
195
|
+
authenticated: true,
|
|
196
|
+
configuredRegionalURL: baseURL,
|
|
197
|
+
user,
|
|
198
|
+
teams,
|
|
199
|
+
activeTeam,
|
|
200
|
+
sandboxes,
|
|
201
|
+
templates,
|
|
202
|
+
};
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return {
|
|
205
|
+
...baseSession,
|
|
206
|
+
errors: [await resolveSDKErrorMessage(error, "failed to resolve session")],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const globalURL = config.globalDirectoryURL;
|
|
212
|
+
if (!globalURL) {
|
|
213
|
+
return {
|
|
214
|
+
...baseSession,
|
|
215
|
+
errors: ["global-directory mode is missing a global base URL"],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const globalSDK = await createDashboardControlPlaneSDK(globalURL, {
|
|
221
|
+
token,
|
|
222
|
+
fetch: fetchImpl,
|
|
223
|
+
});
|
|
224
|
+
const [userResponse, teamResponse, activeTeamResponse] = await Promise.all([
|
|
225
|
+
globalSDK.users.usersMeGet(),
|
|
226
|
+
globalSDK.teams.teamsGet(),
|
|
227
|
+
globalSDK.tenant.tenantActiveGet(),
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
const userData = userResponse.data;
|
|
231
|
+
const activeTeamData = activeTeamResponse.data;
|
|
232
|
+
if (!userData) {
|
|
233
|
+
throw new Error("/users/me returned an empty response");
|
|
234
|
+
}
|
|
235
|
+
if (!activeTeamData) {
|
|
236
|
+
throw new Error("/tenant/active returned an empty response");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const user = toUser(userData);
|
|
240
|
+
const teams = toTeams(teamResponse.data?.teams);
|
|
241
|
+
const activeTeam: DashboardActiveTeam = {
|
|
242
|
+
userID: activeTeamData.userId,
|
|
243
|
+
teamID: activeTeamData.teamId,
|
|
244
|
+
teamRole: activeTeamData.teamRole,
|
|
245
|
+
homeRegionID: activeTeamData.homeRegionId,
|
|
246
|
+
defaultTeam: Boolean(activeTeamData.defaultTeam),
|
|
247
|
+
edgeGatewayURL: activeTeamData.edgeGatewayUrl ?? null,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
let regionalURL = activeTeam.edgeGatewayURL ?? undefined;
|
|
251
|
+
let regionToken = token;
|
|
252
|
+
if (regionalURL) {
|
|
253
|
+
const regionTokenResponse = await globalSDK.tenant.authRegionTokenPost({
|
|
254
|
+
issueRegionTokenRequest: { teamId: activeTeam.teamID },
|
|
255
|
+
});
|
|
256
|
+
const regionTokenData = regionTokenResponse.data;
|
|
257
|
+
if (!regionTokenData) {
|
|
258
|
+
throw new Error("/auth/region-token returned an empty response");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
regionalURL = regionTokenData.edgeGatewayUrl ?? regionalURL;
|
|
262
|
+
regionToken = regionTokenData.token;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const sandboxes = regionalURL
|
|
266
|
+
? await (
|
|
267
|
+
await createDashboardControlPlaneSDK(regionalURL, {
|
|
268
|
+
token: regionToken,
|
|
269
|
+
fetch: fetchImpl,
|
|
270
|
+
})
|
|
271
|
+
).sandboxes.apiV1SandboxesGet({ limit: 5 })
|
|
272
|
+
: undefined;
|
|
273
|
+
const templates = regionalURL
|
|
274
|
+
? await (
|
|
275
|
+
await createDashboardControlPlaneSDK(regionalURL, {
|
|
276
|
+
token: regionToken,
|
|
277
|
+
fetch: fetchImpl,
|
|
278
|
+
})
|
|
279
|
+
).templates.apiV1TemplatesGet()
|
|
280
|
+
: undefined;
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
...baseSession,
|
|
284
|
+
authenticated: true,
|
|
285
|
+
configuredRegionalURL: regionalURL,
|
|
286
|
+
user,
|
|
287
|
+
teams,
|
|
288
|
+
activeTeam,
|
|
289
|
+
sandboxes: toSandboxes(sandboxes?.data?.sandboxes),
|
|
290
|
+
templates: toTemplates(templates?.data?.templates),
|
|
291
|
+
};
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return {
|
|
294
|
+
...baseSession,
|
|
295
|
+
errors: [await resolveSDKErrorMessage(error, "failed to resolve session")],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type DashboardControlPlaneMode = "single-cluster" | "global-directory";
|
|
2
|
+
|
|
3
|
+
export interface DashboardRuntimeConfig {
|
|
4
|
+
mode: DashboardControlPlaneMode;
|
|
5
|
+
dashboardBasePath: string;
|
|
6
|
+
siteURL: string;
|
|
7
|
+
singleClusterURL?: string;
|
|
8
|
+
globalDirectoryURL?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type DashboardAuthProviderType = "oidc" | "builtin";
|
|
12
|
+
|
|
13
|
+
export interface DashboardAuthProvider {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
type: DashboardAuthProviderType;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DashboardUser {
|
|
20
|
+
id: string;
|
|
21
|
+
email: string;
|
|
22
|
+
name: string;
|
|
23
|
+
avatarUrl?: string | null;
|
|
24
|
+
defaultTeamID?: string | null;
|
|
25
|
+
emailVerified: boolean;
|
|
26
|
+
isAdmin: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DashboardTeam {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
slug: string;
|
|
33
|
+
ownerID?: string | null;
|
|
34
|
+
homeRegionID?: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DashboardActiveTeam {
|
|
38
|
+
userID: string;
|
|
39
|
+
teamID: string;
|
|
40
|
+
teamRole?: string;
|
|
41
|
+
homeRegionID: string;
|
|
42
|
+
defaultTeam: boolean;
|
|
43
|
+
edgeGatewayURL?: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DashboardSandboxSummary {
|
|
47
|
+
id: string;
|
|
48
|
+
templateID: string;
|
|
49
|
+
status: string;
|
|
50
|
+
paused: boolean;
|
|
51
|
+
clusterID?: string | null;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
expiresAt: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DashboardTemplateSummary {
|
|
57
|
+
templateID: string;
|
|
58
|
+
scope: string;
|
|
59
|
+
createdAt: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DashboardSession {
|
|
63
|
+
authenticated: boolean;
|
|
64
|
+
mode: DashboardControlPlaneMode;
|
|
65
|
+
dashboardBasePath: string;
|
|
66
|
+
siteURL: string;
|
|
67
|
+
configuredGlobalURL?: string;
|
|
68
|
+
configuredRegionalURL?: string;
|
|
69
|
+
user?: DashboardUser;
|
|
70
|
+
teams: DashboardTeam[];
|
|
71
|
+
activeTeam?: DashboardActiveTeam;
|
|
72
|
+
sandboxes: DashboardSandboxSummary[];
|
|
73
|
+
templates: DashboardTemplateSummary[];
|
|
74
|
+
errors: string[];
|
|
75
|
+
}
|