forgeos 0.1.0-alpha.56 → 0.1.0-alpha.57
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/AGENTS.md +1 -1
- package/CHANGELOG.md +14 -3
- package/docs/changelog.md +16 -0
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/cli/auth.ts +10 -2
- package/src/forge/cli/commands.ts +1 -0
- package/src/forge/cli/dev.ts +5 -2
- package/src/forge/cli/main.ts +2 -0
- package/src/forge/cli/parse.ts +4 -0
- package/src/forge/cli/workos.ts +69 -11
- package/src/forge/compiler/integration/add.ts +64 -23
- package/src/forge/compiler/integration/templates/workos.ts +65 -30
- package/src/forge/compiler/orchestrator/run.ts +4 -2
- package/src/forge/compiler/recipes/profiles.ts +81 -0
- package/src/forge/dev/server.ts +163 -11
- package/src/forge/ui/index.ts +3 -21
- package/src/forge/version.ts +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.57 input=7128597ce3f6cd158bf4361cebb1eef8678c19ff9b2cb60c454a0876356d37c6 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
|
|
2
2
|
# AGENTS.md
|
|
3
3
|
|
|
4
4
|
<!-- forge-generated:start -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# forgeos
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.57
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fix the WorkOS AuthKit local and tunneled development flow.
|
|
8
|
+
|
|
9
|
+
- Add `response_type=code` to Forge-owned WorkOS authorization URLs and avoid sending a default `organization_id` during login so AuthKit can present normal sign-in and organization selection.
|
|
10
|
+
- Resolve WorkOS organization and membership claims during callback when AuthKit returns only the user/token, then expose Forge-normalized session claims through `/session`.
|
|
11
|
+
- Add `forge dev --public-api-url` for browser-facing API URLs behind tunnels and update the WorkOS React bridge to use backend-owned `/login`, `/session`, and `/logout` routes through same-origin/proxy-aware URL resolution.
|
|
12
|
+
- Sync public WorkOS browser env into `web/.env.local` during `forge add auth workos` and `forge workos setup --real`, and update WorkOS doctor/UI audit checks for the backend-owned AuthKit session flow.
|
|
13
|
+
|
|
3
14
|
## 0.1.0-alpha.56
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
|
@@ -22,7 +33,7 @@
|
|
|
22
33
|
`/ready`, `/live/status`, `/outbox/status`, and `/webhooks/workos`.
|
|
23
34
|
- Production docs now present the canonical flow:
|
|
24
35
|
`deploy init -> env doctor -> auth/auth.md/WorkOS proof -> field-test ->
|
|
25
|
-
|
|
36
|
+
readiness -> check -> package -> verify`, while keeping WorkOS FGA
|
|
26
37
|
explicitly optional unless the app opts into FGA artifacts.
|
|
27
38
|
|
|
28
39
|
## 0.1.0-alpha.55
|
|
@@ -115,7 +126,7 @@
|
|
|
115
126
|
- Tighten the no-dashboard WorkOS real setup/proof handoff.
|
|
116
127
|
|
|
117
128
|
- WorkOS hosted setup now records explicit `WORKOS_MODE=agent npx --yes
|
|
118
|
-
|
|
129
|
+
workos@latest auth login --json` shell guidance when CLI login is required.
|
|
119
130
|
- Failed `forge workos setup --real` login attempts now tell the operator to
|
|
120
131
|
rerun the same setup command after completing OAuth/device-code login,
|
|
121
132
|
instead of pointing generically at `prove --real`.
|
|
@@ -276,7 +287,7 @@
|
|
|
276
287
|
`forge dev --seed --all-tenants`, and seed readiness evidence in dev and
|
|
277
288
|
field-test summaries.
|
|
278
289
|
- Update templates, docs, and tests around the new `create app -> add auth ->
|
|
279
|
-
|
|
290
|
+
field-test -> deploy` path.
|
|
280
291
|
|
|
281
292
|
## 0.1.0-alpha.41
|
|
282
293
|
|
package/docs/changelog.md
CHANGED
|
@@ -6,6 +6,22 @@ The canonical source file in the repository is `CHANGELOG.md`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.1.0-alpha.57
|
|
10
|
+
|
|
11
|
+
- Fixed the WorkOS/AuthKit login path used by local and tunneled development:
|
|
12
|
+
Forge-owned authorization URLs now include `response_type=code` and no longer
|
|
13
|
+
send a default `organization_id` unless explicitly requested.
|
|
14
|
+
- The WorkOS callback now resolves organization and membership claims when
|
|
15
|
+
AuthKit returns only the user/token, then exposes Forge-normalized session
|
|
16
|
+
claims through `/session`.
|
|
17
|
+
- Added `forge dev --public-api-url` so browser-facing API URLs can be injected
|
|
18
|
+
cleanly for SSH tunnels, Docker Desktop, and remote dev hosts.
|
|
19
|
+
- The generated WorkOS React bridge now uses backend-owned `/login`, `/session`,
|
|
20
|
+
and `/logout` routes through same-origin/proxy-aware URL resolution.
|
|
21
|
+
- `forge add auth workos`, `forge workos setup --real`, `workos doctor`, and
|
|
22
|
+
`inspect ui --ergonomics` now understand the backend-owned AuthKit session
|
|
23
|
+
flow and sync public browser env into `web/.env.local`.
|
|
24
|
+
|
|
9
25
|
## 0.1.0-alpha.56
|
|
10
26
|
|
|
11
27
|
- Added `forge deploy init --target docker` as the canonical production Docker
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.
|
|
1
|
+
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.57","releaseId":"forgeos@0.1.0-alpha.57+unknown","schemaVersion":"0.1.0"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.57 input=7128597ce3f6cd158bf4361cebb1eef8678c19ff9b2cb60c454a0876356d37c6 content=40c7132e14bfeffd8018f04de93db9ad294cfca4e190b21106d86bb80bc3bc03
|
|
2
2
|
export const releaseManifest = {
|
|
3
3
|
"defaultProvider": "local",
|
|
4
4
|
"diagnostics": [],
|
|
@@ -19,7 +19,7 @@ export const releaseManifest = {
|
|
|
19
19
|
"custom"
|
|
20
20
|
],
|
|
21
21
|
"packageName": "forgeos",
|
|
22
|
-
"packageVersion": "0.1.0-alpha.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
22
|
+
"packageVersion": "0.1.0-alpha.57",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.57+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
package/src/forge/cli/auth.ts
CHANGED
|
@@ -180,11 +180,16 @@ function permissionSubsetForRestrictedTenant(permissions: string[]): string[] {
|
|
|
180
180
|
function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof detectWorkOS>, requiresTenant: boolean) {
|
|
181
181
|
const rootSeedPresent = existsSync(join(workspaceRoot, "workos-seed.yml"));
|
|
182
182
|
const generatedSeedPresent = existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/workos-seed.yml"));
|
|
183
|
+
const fgaArtifactsPresent =
|
|
184
|
+
existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/fga.ts")) ||
|
|
185
|
+
existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/resource-map.ts")) ||
|
|
186
|
+
existsSync(join(workspaceRoot, ".workos-fga-state.json"));
|
|
183
187
|
const authMdPresent = existsSync(join(workspaceRoot, "public/auth.md"));
|
|
184
188
|
const metadataPresent = existsSync(join(workspaceRoot, "public/.well-known/oauth-protected-resource"));
|
|
185
189
|
const activePermissions = collectPolicyPermissions(workspaceRoot);
|
|
186
190
|
const expectedResourceTypes = collectExpectedResourceTypes(workspaceRoot);
|
|
187
191
|
const seed = parseSeedFile(workspaceRoot);
|
|
192
|
+
const fgaEnabled = fgaArtifactsPresent || seed.resourceTypes.length > 0;
|
|
188
193
|
const missingSeedPermissions = missingValues(activePermissions, seed.permissions);
|
|
189
194
|
const missingSeedResources = missingValues(expectedResourceTypes, seed.resourceTypes);
|
|
190
195
|
const acmePermissions = activePermissions;
|
|
@@ -223,8 +228,10 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
|
|
|
223
228
|
},
|
|
224
229
|
{
|
|
225
230
|
id: "seed-resource-coverage",
|
|
226
|
-
ok: missingSeedResources.length === 0,
|
|
227
|
-
evidence:
|
|
231
|
+
ok: !fgaEnabled || missingSeedResources.length === 0,
|
|
232
|
+
evidence: !fgaEnabled
|
|
233
|
+
? "FGA is not enabled; seed resource type coverage is not required for AuthKit/RBAC tenant isolation."
|
|
234
|
+
: missingSeedResources.length === 0
|
|
228
235
|
? `seed covers expected app resource type(s): ${expectedResourceTypes.join(", ") || "none required"}`
|
|
229
236
|
: `seed missing expected app resource type(s): ${missingSeedResources.join(", ")}`,
|
|
230
237
|
},
|
|
@@ -244,6 +251,7 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
|
|
|
244
251
|
appContract: {
|
|
245
252
|
activePermissions,
|
|
246
253
|
expectedResourceTypes,
|
|
254
|
+
fgaEnabled,
|
|
247
255
|
seedPath: seed.path,
|
|
248
256
|
seedPermissions: seed.permissions,
|
|
249
257
|
seedResourceTypes: seed.resourceTypes,
|
|
@@ -2709,6 +2709,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
2709
2709
|
webOnly: command.webOnly,
|
|
2710
2710
|
open: command.open,
|
|
2711
2711
|
webPort: command.webPort,
|
|
2712
|
+
publicApiUrl: command.publicApiUrl,
|
|
2712
2713
|
telemetry: command.telemetry,
|
|
2713
2714
|
envFile: command.envFile,
|
|
2714
2715
|
skipStartupConsole: command.skipStartupConsole,
|
package/src/forge/cli/dev.ts
CHANGED
|
@@ -53,6 +53,7 @@ export interface DevCommandOptions {
|
|
|
53
53
|
webOnly?: boolean;
|
|
54
54
|
open?: boolean;
|
|
55
55
|
webPort?: number;
|
|
56
|
+
publicApiUrl?: string;
|
|
56
57
|
telemetry: string[];
|
|
57
58
|
envFile?: string;
|
|
58
59
|
mode?: "dev" | "serve";
|
|
@@ -305,6 +306,7 @@ function buildDetachedDevArgs(options: DevCommandOptions): string[] {
|
|
|
305
306
|
if (options.webOnly) args.push("--web-only");
|
|
306
307
|
if (options.open) args.push("--open");
|
|
307
308
|
if (options.webPort !== undefined) args.push("--web-port", String(options.webPort));
|
|
309
|
+
if (options.publicApiUrl) args.push("--public-api-url", options.publicApiUrl);
|
|
308
310
|
if (options.telemetry.length > 0) args.push("--telemetry", options.telemetry.join(","));
|
|
309
311
|
if (options.envFile) args.push("--env-file", options.envFile);
|
|
310
312
|
if (options.seed) args.push("--seed");
|
|
@@ -1343,13 +1345,14 @@ export async function runDevCommand(
|
|
|
1343
1345
|
|
|
1344
1346
|
if (options.webOnly) {
|
|
1345
1347
|
const apiUrl = `http://${host}:${port}`;
|
|
1348
|
+
const publicApiUrl = options.publicApiUrl ?? apiUrl;
|
|
1346
1349
|
const webHandle = startWebDevServer({
|
|
1347
1350
|
workspaceRoot,
|
|
1348
1351
|
host,
|
|
1349
1352
|
port: webPort,
|
|
1350
1353
|
requestedPort: webPortSelection.requestedPort,
|
|
1351
1354
|
autoPortSelected: webPortSelection.autoPortSelected,
|
|
1352
|
-
apiUrl,
|
|
1355
|
+
apiUrl: publicApiUrl,
|
|
1353
1356
|
json: options.json,
|
|
1354
1357
|
});
|
|
1355
1358
|
if (!webHandle) {
|
|
@@ -1451,7 +1454,7 @@ export async function runDevCommand(
|
|
|
1451
1454
|
port: webPort,
|
|
1452
1455
|
requestedPort: webPortSelection.requestedPort,
|
|
1453
1456
|
autoPortSelected: webPortSelection.autoPortSelected,
|
|
1454
|
-
apiUrl: handle.url,
|
|
1457
|
+
apiUrl: options.publicApiUrl ?? handle.url,
|
|
1455
1458
|
json: options.json,
|
|
1456
1459
|
});
|
|
1457
1460
|
|
package/src/forge/cli/main.ts
CHANGED
|
@@ -110,6 +110,7 @@ function formatDevHelp(): string {
|
|
|
110
110
|
" --db memory|pglite|postgres|none Choose the development database adapter",
|
|
111
111
|
" --port <port> API runtime port; use 0 for an ephemeral port",
|
|
112
112
|
" --web-port <port> Web dev server port; use 0 for an ephemeral port",
|
|
113
|
+
" --public-api-url <url> Browser-facing API URL injected into web env, useful for tunnels",
|
|
113
114
|
" --host <host> Bind host, default 127.0.0.1",
|
|
114
115
|
" --no-web Start API/runtime only",
|
|
115
116
|
" --api-only Start API/runtime only",
|
|
@@ -124,6 +125,7 @@ function formatDevHelp(): string {
|
|
|
124
125
|
"",
|
|
125
126
|
"Examples:",
|
|
126
127
|
" forge dev --db memory --port 3777 --web-port 5174",
|
|
128
|
+
" forge dev --public-api-url http://localhost:3765",
|
|
127
129
|
" forge dev --db memory --port 0 --web-port 0",
|
|
128
130
|
" forge dev --seed --db pglite",
|
|
129
131
|
" forge dev --seed --all-tenants --db pglite",
|
package/src/forge/cli/parse.ts
CHANGED
|
@@ -336,6 +336,7 @@ export type ForgeCommand =
|
|
|
336
336
|
webOnly: boolean;
|
|
337
337
|
open: boolean;
|
|
338
338
|
webPort?: number;
|
|
339
|
+
publicApiUrl?: string;
|
|
339
340
|
telemetry: string[];
|
|
340
341
|
envFile?: string;
|
|
341
342
|
skipStartupConsole: boolean;
|
|
@@ -2733,6 +2734,7 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
2733
2734
|
webOnly: parseFlag(argv, "--web-only"),
|
|
2734
2735
|
open: parseFlag(argv, "--open"),
|
|
2735
2736
|
webPort,
|
|
2737
|
+
publicApiUrl: parseOptionValue(argv, "--public-api-url"),
|
|
2736
2738
|
telemetry: (parseOptionValue(argv, "--telemetry") ?? "local")
|
|
2737
2739
|
.split(",")
|
|
2738
2740
|
.map((value) => value.trim())
|
|
@@ -3250,6 +3252,7 @@ export function hasUnknownOption(argv: string[]): string | null {
|
|
|
3250
3252
|
"--postgres-version",
|
|
3251
3253
|
"--runtime-port",
|
|
3252
3254
|
"--web-port",
|
|
3255
|
+
"--public-api-url",
|
|
3253
3256
|
"--preview-port",
|
|
3254
3257
|
"--preview-url",
|
|
3255
3258
|
"--studio-url",
|
|
@@ -3377,6 +3380,7 @@ export function hasUnknownOption(argv: string[]): string | null {
|
|
|
3377
3380
|
arg === "--postgres-version" ||
|
|
3378
3381
|
arg === "--runtime-port" ||
|
|
3379
3382
|
arg === "--web-port" ||
|
|
3383
|
+
arg === "--public-api-url" ||
|
|
3380
3384
|
arg === "--preview-port" ||
|
|
3381
3385
|
arg === "--preview-url" ||
|
|
3382
3386
|
arg === "--studio-url" ||
|
package/src/forge/cli/workos.ts
CHANGED
|
@@ -573,6 +573,62 @@ function collectWorkOSRealEnvChecks(workspaceRoot: string, cliAuth?: WorkOSCliAu
|
|
|
573
573
|
];
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
function renderMergedDotEnv(current: string | null, values: Record<string, string>): string {
|
|
577
|
+
const lines = (current ?? "").split(/\r?\n/);
|
|
578
|
+
const seen = new Set<string>();
|
|
579
|
+
const next = lines.map((line) => {
|
|
580
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
|
|
581
|
+
if (!match || !(match[1]! in values)) {
|
|
582
|
+
return line;
|
|
583
|
+
}
|
|
584
|
+
seen.add(match[1]!);
|
|
585
|
+
return `${match[1]}=${values[match[1]!]}`;
|
|
586
|
+
});
|
|
587
|
+
for (const [key, value] of Object.entries(values)) {
|
|
588
|
+
if (!seen.has(key)) {
|
|
589
|
+
next.push(`${key}=${value}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return `${next.filter((line, index, all) => line.length > 0 || index < all.length - 1).join("\n").trimEnd()}\n`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function syncWorkOSPublicWebEnv(workspaceRoot: string, write: boolean): { ok: boolean; changed: string[]; path?: string; skipped?: string } {
|
|
596
|
+
const frontendWorkspace = findWorkOSFrontendWorkspace(workspaceRoot);
|
|
597
|
+
if (!frontendWorkspace) {
|
|
598
|
+
return { ok: true, changed: [], skipped: "no web workspace detected" };
|
|
599
|
+
}
|
|
600
|
+
const env = readRealEnv(workspaceRoot);
|
|
601
|
+
const clientId = env.VITE_WORKOS_CLIENT_ID || env.WORKOS_CLIENT_ID || "";
|
|
602
|
+
const redirectUri = env.VITE_WORKOS_REDIRECT_URI || env.WORKOS_REDIRECT_URI || "http://localhost:5173/callback";
|
|
603
|
+
const values: Record<string, string> = {
|
|
604
|
+
VITE_FORGE_URL: env.VITE_FORGE_URL || "http://localhost:3765",
|
|
605
|
+
VITE_WORKOS_CLIENT_ID: clientId,
|
|
606
|
+
VITE_WORKOS_REDIRECT_URI: redirectUri,
|
|
607
|
+
};
|
|
608
|
+
const relPath = join(frontendWorkspace, ".env.local");
|
|
609
|
+
const path = join(workspaceRoot, relPath);
|
|
610
|
+
const current = readRawText(workspaceRoot, relPath);
|
|
611
|
+
const next = renderMergedDotEnv(current, values);
|
|
612
|
+
if (current === next) {
|
|
613
|
+
return { ok: true, changed: [], path: relPath };
|
|
614
|
+
}
|
|
615
|
+
if (write) {
|
|
616
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
617
|
+
writeFileSync(path, next, "utf8");
|
|
618
|
+
}
|
|
619
|
+
return { ok: true, changed: [relPath], path: relPath };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function findWorkOSFrontendWorkspace(workspaceRoot: string): string | undefined {
|
|
623
|
+
for (const candidate of ["web", "frontend", "client", "apps/web", "packages/web"]) {
|
|
624
|
+
if (existsSync(join(workspaceRoot, candidate, "package.json"))) {
|
|
625
|
+
return candidate;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
|
|
576
632
|
export function collectPolicyPermissions(workspaceRoot: string): string[] {
|
|
577
633
|
const registry = readJson(workspaceRoot, `${GENERATED_DIR}/policyRegistry.json`) as {
|
|
578
634
|
policies?: Array<{ permissions?: string[] }>;
|
|
@@ -1757,12 +1813,10 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
|
|
|
1757
1813
|
readText(workspaceRoot, "web/src/main.tsx"),
|
|
1758
1814
|
readText(workspaceRoot, "web/src/App.tsx"),
|
|
1759
1815
|
].join("\n");
|
|
1760
|
-
const appShellUsesWorkOSProvider =
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
generatedFrontendAuthBridge.includes("getToken") ||
|
|
1765
|
-
generatedFrontendAuthBridge.includes("getAccessToken");
|
|
1816
|
+
const appShellUsesWorkOSProvider = frontendAppShell.includes("ForgeWorkOSAuthProvider");
|
|
1817
|
+
const authBridgeUsesBackendRoutes =
|
|
1818
|
+
generatedFrontendAuthBridge.includes("workOSApiUrl") &&
|
|
1819
|
+
["/login", "/logout", "/session"].every((route) => generatedFrontendAuthBridge.includes(route));
|
|
1766
1820
|
const authBridgeProvidesSessionClaims =
|
|
1767
1821
|
includesAll(generatedFrontendAuthBridge, ["useForgeWorkOSSession", "/session", "claims"]);
|
|
1768
1822
|
const authSessionProxyConfigured = webAuthSessionProxyConfigured(workspaceRoot);
|
|
@@ -1809,7 +1863,7 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
|
|
|
1809
1863
|
ok: !hasWeb || (hasValue(realEnv, "VITE_WORKOS_CLIENT_ID") || readRawText(workspaceRoot, ".env.example").includes("VITE_WORKOS_CLIENT_ID=")) &&
|
|
1810
1864
|
(hasValue(realEnv, "VITE_WORKOS_REDIRECT_URI") || readRawText(workspaceRoot, ".env.example").includes("VITE_WORKOS_REDIRECT_URI=")),
|
|
1811
1865
|
detail: hasWeb
|
|
1812
|
-
? "web workspace has
|
|
1866
|
+
? "web workspace has browser WorkOS client ID and redirect URI guidance for backend-owned AuthKit routes"
|
|
1813
1867
|
: "no web workspace detected",
|
|
1814
1868
|
},
|
|
1815
1869
|
{
|
|
@@ -1821,11 +1875,11 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
|
|
|
1821
1875
|
},
|
|
1822
1876
|
{
|
|
1823
1877
|
name: "browser-authkit-bridge",
|
|
1824
|
-
ok: !hasWeb ||
|
|
1825
|
-
|
|
1878
|
+
ok: !hasWeb || generatedFrontendAuthBridge.includes("ForgeProvider") &&
|
|
1879
|
+
authBridgeUsesBackendRoutes &&
|
|
1826
1880
|
authBridgeProvidesSessionClaims,
|
|
1827
1881
|
detail: hasWeb
|
|
1828
|
-
? "generated web/src/lib/workos-auth.tsx bridge
|
|
1882
|
+
? "generated web/src/lib/workos-auth.tsx bridge uses backend-owned AuthKit routes, ForgeProvider, and normalized /session claims"
|
|
1829
1883
|
: "no web workspace detected",
|
|
1830
1884
|
},
|
|
1831
1885
|
{
|
|
@@ -1841,7 +1895,7 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
|
|
|
1841
1895
|
name: "browser-authkit-provider",
|
|
1842
1896
|
ok: !hasWeb || appShellUsesWorkOSProvider,
|
|
1843
1897
|
detail: hasWeb
|
|
1844
|
-
? "web app shell mounts ForgeWorkOSAuthProvider
|
|
1898
|
+
? "web app shell mounts ForgeWorkOSAuthProvider"
|
|
1845
1899
|
: "no web workspace detected",
|
|
1846
1900
|
},
|
|
1847
1901
|
{
|
|
@@ -2486,6 +2540,7 @@ export function runWorkOSSetupCommand(options: WorkOSCommandOptions): WorkOSComm
|
|
|
2486
2540
|
const seed = parseSeedFile(options.workspaceRoot, file);
|
|
2487
2541
|
const seedState = readWorkOSSeedState(options.workspaceRoot, seed);
|
|
2488
2542
|
const setupDryRun = options.dryRun || !options.real;
|
|
2543
|
+
const publicEnvSync = syncWorkOSPublicWebEnv(options.workspaceRoot, localOk && !setupDryRun);
|
|
2489
2544
|
const configActions = runWorkOSConfigActions(seed, options, true);
|
|
2490
2545
|
const command = ["npx", "--yes", "workos@latest", "seed", "--file", file];
|
|
2491
2546
|
if (!localOk) {
|
|
@@ -2500,6 +2555,7 @@ export function runWorkOSSetupCommand(options: WorkOSCommandOptions): WorkOSComm
|
|
|
2500
2555
|
real: options.real ?? false,
|
|
2501
2556
|
seed,
|
|
2502
2557
|
seedState,
|
|
2558
|
+
publicEnvSync,
|
|
2503
2559
|
...(cliAuth ? { cliAuth } : {}),
|
|
2504
2560
|
configActions,
|
|
2505
2561
|
nextCommand: cliAuth && !cliAuth.ok
|
|
@@ -2521,6 +2577,7 @@ export function runWorkOSSetupCommand(options: WorkOSCommandOptions): WorkOSComm
|
|
|
2521
2577
|
real: false,
|
|
2522
2578
|
seed,
|
|
2523
2579
|
seedState,
|
|
2580
|
+
publicEnvSync,
|
|
2524
2581
|
...(cliAuth ? { cliAuth } : {}),
|
|
2525
2582
|
configActions,
|
|
2526
2583
|
nextCommand: `forge workos setup --real --file ${file} --json`,
|
|
@@ -2546,6 +2603,7 @@ export function runWorkOSSetupCommand(options: WorkOSCommandOptions): WorkOSComm
|
|
|
2546
2603
|
data: {
|
|
2547
2604
|
real: true,
|
|
2548
2605
|
seed,
|
|
2606
|
+
publicEnvSync,
|
|
2549
2607
|
...(cliAuth ? { cliAuth } : {}),
|
|
2550
2608
|
seedState: seedResultData.seedState ?? readWorkOSSeedState(options.workspaceRoot, seed),
|
|
2551
2609
|
seedResult: seedResult.data,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
resolveByPackageName,
|
|
32
32
|
resolveRecipe,
|
|
33
33
|
} from "../recipes/registry.ts";
|
|
34
|
+
import { applyWorkOSRecipeProfile } from "../recipes/profiles.ts";
|
|
34
35
|
import { discover } from "../orchestrator/discover.ts";
|
|
35
36
|
import {
|
|
36
37
|
loadManifest,
|
|
@@ -92,24 +93,6 @@ function recipeResultMetadata(recipe: IntegrationRecipe): Pick<
|
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
const WORKOS_FGA_INTEGRATION_FILES = new Set([
|
|
96
|
-
"workos/fga.ts",
|
|
97
|
-
"workos/resource-map.ts",
|
|
98
|
-
]);
|
|
99
|
-
|
|
100
|
-
function applyWorkOSRecipeProfile(
|
|
101
|
-
recipe: NonNullable<ReturnType<typeof resolveRecipe>>,
|
|
102
|
-
options: Pick<ForgeAddOptions, "withFga">,
|
|
103
|
-
): IntegrationRecipe {
|
|
104
|
-
if (recipe.alias !== "workos" || options.withFga) {
|
|
105
|
-
return recipe;
|
|
106
|
-
}
|
|
107
|
-
return {
|
|
108
|
-
...recipe,
|
|
109
|
-
integrations: recipe.integrations?.filter((file) => !WORKOS_FGA_INTEGRATION_FILES.has(file)),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
96
|
function addExplanation(result: ForgeAddResult): string {
|
|
114
97
|
if (result.mode === "package") {
|
|
115
98
|
const location = result.target && result.target !== "root" ? `${result.target}/package.json` : "package.json";
|
|
@@ -528,6 +511,60 @@ function workosFrontendWorkspace(workspaceRoot: string): string | undefined {
|
|
|
528
511
|
return findFrontendWorkspace(workspaceRoot);
|
|
529
512
|
}
|
|
530
513
|
|
|
514
|
+
function parseDotEnv(text: string | null): Record<string, string> {
|
|
515
|
+
const result: Record<string, string> = {};
|
|
516
|
+
for (const line of (text ?? "").split(/\r?\n/)) {
|
|
517
|
+
const trimmed = line.trim();
|
|
518
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
519
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
520
|
+
if (!match) continue;
|
|
521
|
+
result[match[1]!] = match[2]!.replace(/^["']|["']$/g, "");
|
|
522
|
+
}
|
|
523
|
+
return result;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function renderMergedDotEnv(current: string | null, values: Record<string, string>): string {
|
|
527
|
+
const lines = (current ?? "").split(/\r?\n/);
|
|
528
|
+
const seen = new Set<string>();
|
|
529
|
+
const next = lines.map((line) => {
|
|
530
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
|
|
531
|
+
if (!match || !(match[1]! in values)) {
|
|
532
|
+
return line;
|
|
533
|
+
}
|
|
534
|
+
seen.add(match[1]!);
|
|
535
|
+
return `${match[1]}=${values[match[1]!]}`;
|
|
536
|
+
});
|
|
537
|
+
for (const [key, value] of Object.entries(values)) {
|
|
538
|
+
if (!seen.has(key)) {
|
|
539
|
+
next.push(`${key}=${value}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return `${next.filter((line, index, all) => line.length > 0 || index < all.length - 1).join("\n").trimEnd()}\n`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function syncWorkOSPublicWebEnv(workspaceRoot: string, frontendWorkspace: string): { changed: string[] } {
|
|
546
|
+
const rootLocal = parseDotEnv(nodeFileSystem.readText(join(workspaceRoot, ".env.local")));
|
|
547
|
+
const rootExample = parseDotEnv(nodeFileSystem.readText(join(workspaceRoot, ".env.example")));
|
|
548
|
+
const source = { ...rootExample, ...rootLocal };
|
|
549
|
+
const clientId = source.VITE_WORKOS_CLIENT_ID || source.WORKOS_CLIENT_ID || "";
|
|
550
|
+
const redirectUri = source.VITE_WORKOS_REDIRECT_URI || source.WORKOS_REDIRECT_URI || "http://localhost:5173/callback";
|
|
551
|
+
const values: Record<string, string> = {
|
|
552
|
+
VITE_FORGE_URL: source.VITE_FORGE_URL || "http://localhost:3765",
|
|
553
|
+
VITE_WORKOS_CLIENT_ID: clientId,
|
|
554
|
+
VITE_WORKOS_REDIRECT_URI: redirectUri,
|
|
555
|
+
};
|
|
556
|
+
const envRel = `${frontendWorkspace}/.env.local`;
|
|
557
|
+
const envPath = join(workspaceRoot, envRel);
|
|
558
|
+
const current = nodeFileSystem.readText(envPath);
|
|
559
|
+
const next = renderMergedDotEnv(current, values);
|
|
560
|
+
if (current === next) {
|
|
561
|
+
return { changed: [] };
|
|
562
|
+
}
|
|
563
|
+
nodeFileSystem.mkdirp(join(workspaceRoot, frontendWorkspace));
|
|
564
|
+
nodeFileSystem.writeText(envPath, next);
|
|
565
|
+
return { changed: [envRel] };
|
|
566
|
+
}
|
|
567
|
+
|
|
531
568
|
function connectWorkOSReactRoot(workspaceRoot: string, frontendWorkspace: string): {
|
|
532
569
|
changed: string[];
|
|
533
570
|
warnings: Diagnostic[];
|
|
@@ -610,7 +647,7 @@ function connectWorkOSReactRoot(workspaceRoot: string, frontendWorkspace: string
|
|
|
610
647
|
" <p className=\"notice error\">{workosSession.error.message}</p>",
|
|
611
648
|
" <div className=\"login-form\">",
|
|
612
649
|
" <button type=\"button\" onClick={() => void workosSession.refresh()}>Retry session</button>",
|
|
613
|
-
" <button className=\"secondary\" type=\"button\" onClick={() => auth.signOut(
|
|
650
|
+
" <button className=\"secondary\" type=\"button\" onClick={() => auth.signOut()}>Sign out</button>",
|
|
614
651
|
" </div>",
|
|
615
652
|
" </section>",
|
|
616
653
|
" </main>",
|
|
@@ -631,8 +668,8 @@ function connectWorkOSReactRoot(workspaceRoot: string, frontendWorkspace: string
|
|
|
631
668
|
" <p className=\"eyebrow\">WorkOS AuthKit</p>",
|
|
632
669
|
" <h1>Sign in to review vendor access</h1>",
|
|
633
670
|
" <div className=\"login-form\">",
|
|
634
|
-
" <button type=\"button\" onClick={() =>
|
|
635
|
-
" <button className=\"secondary\" type=\"button\" onClick={() =>
|
|
671
|
+
" <button type=\"button\" onClick={() => auth.signIn()}>Sign in with WorkOS</button>",
|
|
672
|
+
" <button className=\"secondary\" type=\"button\" onClick={() => auth.signUp()}>Create account</button>",
|
|
636
673
|
" </div>",
|
|
637
674
|
" </section>",
|
|
638
675
|
" <aside className=\"login-context\">",
|
|
@@ -664,7 +701,7 @@ function connectWorkOSReactRoot(workspaceRoot: string, frontendWorkspace: string
|
|
|
664
701
|
" persona={persona}",
|
|
665
702
|
" personas={[persona]}",
|
|
666
703
|
" onPersonaChange={() => undefined}",
|
|
667
|
-
" onSignOut={() => auth.signOut(
|
|
704
|
+
" onSignOut={() => auth.signOut()}",
|
|
668
705
|
" />",
|
|
669
706
|
" );",
|
|
670
707
|
"}",
|
|
@@ -1103,7 +1140,11 @@ export async function forgeAdd(
|
|
|
1103
1140
|
});
|
|
1104
1141
|
|
|
1105
1142
|
const workosWeb = normalized === "workos" && frontendWorkspace
|
|
1106
|
-
?
|
|
1143
|
+
? (() => {
|
|
1144
|
+
const root = connectWorkOSReactRoot(options.workspaceRoot, frontendWorkspace);
|
|
1145
|
+
const env = syncWorkOSPublicWebEnv(options.workspaceRoot, frontendWorkspace);
|
|
1146
|
+
return { changed: [...root.changed, ...env.changed], warnings: root.warnings };
|
|
1147
|
+
})()
|
|
1107
1148
|
: { changed: [] as string[], warnings: [] as Diagnostic[] };
|
|
1108
1149
|
|
|
1109
1150
|
const warningsCombined = [...preinstalledWarnings, ...warnings, ...emitResult.warnings, ...workosWeb.warnings];
|
|
@@ -285,6 +285,7 @@ export function renderWorkosEnvExample(_input: IntegrationTemplateInput): string
|
|
|
285
285
|
"WORKOS_WEBHOOK_SECRET=",
|
|
286
286
|
"",
|
|
287
287
|
"# Browser AuthKit settings for Vite/React. These are public client identifiers, not secrets.",
|
|
288
|
+
"VITE_FORGE_URL=http://localhost:3765",
|
|
288
289
|
"VITE_WORKOS_CLIENT_ID=",
|
|
289
290
|
"VITE_WORKOS_REDIRECT_URI=http://localhost:5173/callback",
|
|
290
291
|
"",
|
|
@@ -299,12 +300,22 @@ export function renderWorkosReactBridge(_input: IntegrationTemplateInput): strin
|
|
|
299
300
|
"/** Forge generated WorkOS/AuthKit React bridge.",
|
|
300
301
|
" * Import ForgeWorkOSAuthProvider from web/src/main.tsx, or copy the pattern into your app shell.",
|
|
301
302
|
" */",
|
|
302
|
-
'import { AuthKitProvider, useAuth as useWorkOSAuth } from "@workos-inc/authkit-react";',
|
|
303
303
|
'import { useEffect, useState, type ReactNode } from "react";',
|
|
304
304
|
'import { ForgeProvider, forgeUrl } from "./forge";',
|
|
305
305
|
"",
|
|
306
306
|
"const clientId = import.meta.env.VITE_WORKOS_CLIENT_ID as string | undefined;",
|
|
307
307
|
"const redirectUri = import.meta.env.VITE_WORKOS_REDIRECT_URI as string | undefined;",
|
|
308
|
+
"const forgeApiUrl = import.meta.env.VITE_FORGE_URL as string | undefined;",
|
|
309
|
+
"",
|
|
310
|
+
"function workOSApiUrl(path: string): URL {",
|
|
311
|
+
" if (typeof window !== 'undefined' && forgeApiUrl) {",
|
|
312
|
+
" const configured = new URL(forgeApiUrl, window.location.origin);",
|
|
313
|
+
" if ((configured.hostname === 'localhost' || configured.hostname === '127.0.0.1') && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {",
|
|
314
|
+
" return new URL(path, window.location.origin);",
|
|
315
|
+
" }",
|
|
316
|
+
" }",
|
|
317
|
+
" return new URL(path, forgeApiUrl || forgeUrl || window.location.origin);",
|
|
318
|
+
"}",
|
|
308
319
|
"",
|
|
309
320
|
"export function hasWorkOSBrowserConfig(): boolean {",
|
|
310
321
|
" return Boolean(clientId && redirectUri);",
|
|
@@ -315,24 +326,7 @@ export function renderWorkosReactBridge(_input: IntegrationTemplateInput): strin
|
|
|
315
326
|
" throw new Error(\"WorkOS AuthKit requires VITE_WORKOS_CLIENT_ID and VITE_WORKOS_REDIRECT_URI\");",
|
|
316
327
|
" }",
|
|
317
328
|
" return (",
|
|
318
|
-
" <
|
|
319
|
-
" <ForgeWorkOSBearerProvider>{children}</ForgeWorkOSBearerProvider>",
|
|
320
|
-
" </AuthKitProvider>",
|
|
321
|
-
" );",
|
|
322
|
-
"}",
|
|
323
|
-
"",
|
|
324
|
-
"function ForgeWorkOSBearerProvider({ children }: { children: ReactNode }) {",
|
|
325
|
-
" const auth = useWorkOSAuth();",
|
|
326
|
-
" return (",
|
|
327
|
-
" <ForgeProvider",
|
|
328
|
-
" url={forgeUrl}",
|
|
329
|
-
" auth={{",
|
|
330
|
-
" getToken: async () => {",
|
|
331
|
-
" const token = await auth.getAccessToken();",
|
|
332
|
-
" return token ?? null;",
|
|
333
|
-
" },",
|
|
334
|
-
" }}",
|
|
335
|
-
" >",
|
|
329
|
+
" <ForgeProvider url={forgeUrl}>",
|
|
336
330
|
" {children}",
|
|
337
331
|
" </ForgeProvider>",
|
|
338
332
|
" );",
|
|
@@ -360,7 +354,7 @@ export function renderWorkosReactBridge(_input: IntegrationTemplateInput): strin
|
|
|
360
354
|
" setLoading(true);",
|
|
361
355
|
" setError(null);",
|
|
362
356
|
" try {",
|
|
363
|
-
" const response = await fetch('/session', { credentials: 'include' });",
|
|
357
|
+
" const response = await fetch(workOSApiUrl('/session'), { credentials: 'include' });",
|
|
364
358
|
" if (response.status === 401) {",
|
|
365
359
|
" setSession(null);",
|
|
366
360
|
" return;",
|
|
@@ -385,6 +379,30 @@ export function renderWorkosReactBridge(_input: IntegrationTemplateInput): strin
|
|
|
385
379
|
" return { loading, session, error, refresh };",
|
|
386
380
|
"}",
|
|
387
381
|
"",
|
|
382
|
+
"export function useWorkOSAuth() {",
|
|
383
|
+
" const session = useForgeWorkOSSession();",
|
|
384
|
+
" const user = session.session?.user ?? null;",
|
|
385
|
+
" return {",
|
|
386
|
+
" isLoading: session.loading,",
|
|
387
|
+
" user,",
|
|
388
|
+
" session: session.session,",
|
|
389
|
+
" sessionError: session.error,",
|
|
390
|
+
" refresh: session.refresh,",
|
|
391
|
+
" signIn: () => {",
|
|
392
|
+
" const url = workOSApiUrl('/login');",
|
|
393
|
+
" url.searchParams.set('returnTo', window.location.pathname + window.location.search);",
|
|
394
|
+
" window.location.assign(url);",
|
|
395
|
+
" },",
|
|
396
|
+
" signUp: () => {",
|
|
397
|
+
" const url = workOSApiUrl('/login');",
|
|
398
|
+
" url.searchParams.set('returnTo', window.location.pathname + window.location.search);",
|
|
399
|
+
" window.location.assign(url);",
|
|
400
|
+
" },",
|
|
401
|
+
" signOut: () => { void fetch(workOSApiUrl('/logout'), { method: 'POST', credentials: 'include' }).finally(() => { window.location.assign('/'); }); },",
|
|
402
|
+
" getAccessToken: async () => null,",
|
|
403
|
+
" };",
|
|
404
|
+
"}",
|
|
405
|
+
"",
|
|
388
406
|
"export function WorkOSLoginPanel() {",
|
|
389
407
|
" const { isLoading, user, signIn, signUp, signOut } = useWorkOSAuth();",
|
|
390
408
|
" useEffect(() => {",
|
|
@@ -405,12 +423,11 @@ export function renderWorkosReactBridge(_input: IntegrationTemplateInput): strin
|
|
|
405
423
|
" return (",
|
|
406
424
|
" <div>",
|
|
407
425
|
" <span>{user.email}</span>",
|
|
408
|
-
" <button type=\"button\" onClick={() => signOut(
|
|
426
|
+
" <button type=\"button\" onClick={() => signOut()}>Sign out</button>",
|
|
409
427
|
" </div>",
|
|
410
428
|
" );",
|
|
411
429
|
"}",
|
|
412
430
|
"",
|
|
413
|
-
"export { useWorkOSAuth };",
|
|
414
431
|
"",
|
|
415
432
|
].join("\n");
|
|
416
433
|
}
|
|
@@ -829,7 +846,7 @@ export function renderWorkosAuthRoutes(_input: IntegrationTemplateInput): string
|
|
|
829
846
|
"export interface WorkOSAuthRoutesOptions {",
|
|
830
847
|
" workos: WorkOS;",
|
|
831
848
|
" config: ForgeWorkOSAuthKitConfig;",
|
|
832
|
-
" resolveOrganization?: (input: { user: Record<string, unknown>; accessToken: string }) => Promise<{ organizationId?: string; organizationMembershipId?: string; role?: string; roles?: string[]; permissions?: string[] }> | { organizationId?: string; organizationMembershipId?: string; role?: string; roles?: string[]; permissions?: string[] };",
|
|
849
|
+
" resolveOrganization?: (input: { user: Record<string, unknown>; accessToken: string; organizationId?: string; organizationMembershipId?: string; result: Record<string, unknown> }) => Promise<{ organizationId?: string; organizationMembershipId?: string; role?: string; roles?: string[]; permissions?: string[] }> | { organizationId?: string; organizationMembershipId?: string; role?: string; roles?: string[]; permissions?: string[] };",
|
|
833
850
|
"}",
|
|
834
851
|
"",
|
|
835
852
|
"function jsonResponse(body: unknown, status = 200, headers?: HeadersInit): Response {",
|
|
@@ -863,12 +880,15 @@ export function renderWorkosAuthRoutes(_input: IntegrationTemplateInput): string
|
|
|
863
880
|
"",
|
|
864
881
|
"export function getWorkOSAuthorizationUrl(input: { workos: WorkOS; config: ForgeWorkOSAuthKitConfig; state?: string }): string {",
|
|
865
882
|
" if (!input.config.redirectUri) throw new Error(\"WORKOS_REDIRECT_URI is required\");",
|
|
866
|
-
"
|
|
883
|
+
" const url = new URL(input.workos.userManagement.getAuthorizationUrl({",
|
|
867
884
|
" provider: \"authkit\",",
|
|
868
885
|
" redirectUri: input.config.redirectUri,",
|
|
869
886
|
" clientId: input.config.clientId,",
|
|
870
887
|
" ...(input.state ? { state: input.state } : {}),",
|
|
871
|
-
" });",
|
|
888
|
+
" }));",
|
|
889
|
+
" url.searchParams.set(\"response_type\", \"code\");",
|
|
890
|
+
" url.searchParams.delete(\"organization_id\");",
|
|
891
|
+
" return url.toString();",
|
|
872
892
|
"}",
|
|
873
893
|
"",
|
|
874
894
|
"export async function authenticateWorkOSCallback(input: { workos: WorkOS; config: ForgeWorkOSAuthKitConfig; code: string; resolveOrganization?: WorkOSAuthRoutesOptions[\"resolveOrganization\"] }): Promise<ForgeWorkOSSession> {",
|
|
@@ -877,19 +897,34 @@ export function renderWorkosAuthRoutes(_input: IntegrationTemplateInput): string
|
|
|
877
897
|
" clientId: input.config.clientId,",
|
|
878
898
|
" });",
|
|
879
899
|
" const user = workOSUserRecord(result.user);",
|
|
900
|
+
" const rawResult = result as Record<string, unknown>;",
|
|
901
|
+
" const resultOrganization = (rawResult.organization ?? {}) as Record<string, unknown>;",
|
|
902
|
+
" const resultMembership = (rawResult.organizationMembership ?? {}) as Record<string, unknown>;",
|
|
903
|
+
" const resultOrganizationId = typeof result.organizationId === \"string\" ? result.organizationId : typeof resultOrganization.id === \"string\" ? resultOrganization.id : undefined;",
|
|
904
|
+
" const resultOrganizationMembershipId = typeof result.organizationMembershipId === \"string\" ? result.organizationMembershipId : typeof resultMembership.id === \"string\" ? resultMembership.id : undefined;",
|
|
880
905
|
" const resolved = await input.resolveOrganization?.({",
|
|
881
906
|
" user,",
|
|
882
907
|
" accessToken: result.accessToken,",
|
|
908
|
+
" organizationId: resultOrganizationId,",
|
|
909
|
+
" organizationMembershipId: resultOrganizationMembershipId,",
|
|
910
|
+
" result: rawResult,",
|
|
883
911
|
" });",
|
|
912
|
+
" const resultRoles = Array.isArray(result.roles) ? result.roles.filter((role): role is string => typeof role === \"string\") : [];",
|
|
913
|
+
" const resultPermissions = Array.isArray(result.permissions) ? result.permissions.filter((permission): permission is string => typeof permission === \"string\") : [];",
|
|
914
|
+
" const organizationId = resolved?.organizationId ?? resultOrganizationId;",
|
|
915
|
+
" const organizationMembershipId = resolved?.organizationMembershipId ?? resultOrganizationMembershipId;",
|
|
916
|
+
" const role = resolved?.role ?? (typeof result.role === \"string\" ? result.role : resultRoles[0]);",
|
|
917
|
+
" const roles = resolved?.roles ?? resultRoles;",
|
|
918
|
+
" const permissions = resolved?.permissions ?? resultPermissions;",
|
|
884
919
|
" return {",
|
|
885
920
|
" user: workOSSessionUser(user),",
|
|
886
921
|
" accessToken: result.accessToken,",
|
|
887
922
|
" ...(result.refreshToken ? { refreshToken: result.refreshToken } : {}),",
|
|
888
|
-
" ...(
|
|
889
|
-
" ...(
|
|
890
|
-
" ...(
|
|
891
|
-
" ...(
|
|
892
|
-
" ...(
|
|
923
|
+
" ...(organizationId ? { organizationId } : {}),",
|
|
924
|
+
" ...(organizationMembershipId ? { organizationMembershipId } : {}),",
|
|
925
|
+
" ...(role ? { role } : {}),",
|
|
926
|
+
" ...(roles.length > 0 ? { roles } : {}),",
|
|
927
|
+
" ...(permissions.length > 0 ? { permissions } : {}),",
|
|
893
928
|
" issuedAt: Math.floor(Date.now() / 1000),",
|
|
894
929
|
" };",
|
|
895
930
|
"}",
|
|
@@ -16,6 +16,7 @@ import { buildRuntimeMatrix } from "../classifier/runtime-matrix.ts";
|
|
|
16
16
|
import { emit } from "../emitter/emit.ts";
|
|
17
17
|
import { PackageGraphCompiler } from "../package-graph/compiler.ts";
|
|
18
18
|
import { resolveByPackageName } from "../recipes/registry.ts";
|
|
19
|
+
import { applyWorkspaceRecipeProfile } from "../recipes/profiles.ts";
|
|
19
20
|
import { checkImportGuards } from "./guards.ts";
|
|
20
21
|
import {
|
|
21
22
|
saveManifest,
|
|
@@ -45,9 +46,10 @@ import { verifyLockIntegrity } from "./verify.ts";
|
|
|
45
46
|
|
|
46
47
|
function classifyPackages(
|
|
47
48
|
packageGraph: Awaited<ReturnType<PackageGraphCompiler["build"]>>["graph"],
|
|
49
|
+
workspaceRoot: string,
|
|
48
50
|
): ClassifiedPackage[] {
|
|
49
51
|
return packageGraph.packages.map((api) => {
|
|
50
|
-
const recipe = resolveByPackageName(api.name)
|
|
52
|
+
const recipe = applyWorkspaceRecipeProfile(resolveByPackageName(api.name), workspaceRoot);
|
|
51
53
|
return {
|
|
52
54
|
api,
|
|
53
55
|
classification: classify(api, recipe),
|
|
@@ -171,7 +173,7 @@ async function runUnlocked(options: GenerateOptions): Promise<GenerateResult> {
|
|
|
171
173
|
if (profileEnabled) {
|
|
172
174
|
resetSignalProfile();
|
|
173
175
|
}
|
|
174
|
-
const classified = classifyPackages(pkgResult.graph);
|
|
176
|
+
const classified = classifyPackages(pkgResult.graph, options.workspaceRoot);
|
|
175
177
|
const classifierSignals = profileEnabled ? getSignalProfile() : undefined;
|
|
176
178
|
if (profileEnabled) {
|
|
177
179
|
clearSignalProfile();
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { nodeFileSystem } from "../fs/index.ts";
|
|
3
|
+
import { FORGE_LOCK_PATH, GENERATED_DIR } from "../emitter/constants.ts";
|
|
4
|
+
import type { IntegrationRecipe } from "../types/integration.ts";
|
|
5
|
+
import type { ForgeLock } from "../types/lock.ts";
|
|
6
|
+
|
|
7
|
+
export const WORKOS_FGA_INTEGRATION_FILES = new Set([
|
|
8
|
+
"workos/fga.ts",
|
|
9
|
+
"workos/resource-map.ts",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function filterWorkOSFga(recipe: IntegrationRecipe): IntegrationRecipe {
|
|
13
|
+
return {
|
|
14
|
+
...recipe,
|
|
15
|
+
integrations: recipe.integrations?.filter((file) => !WORKOS_FGA_INTEGRATION_FILES.has(file)),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function applyWorkOSRecipeProfile(
|
|
20
|
+
recipe: IntegrationRecipe,
|
|
21
|
+
options: { withFga?: boolean },
|
|
22
|
+
): IntegrationRecipe {
|
|
23
|
+
if (recipe.alias !== "workos" || options.withFga) {
|
|
24
|
+
return recipe;
|
|
25
|
+
}
|
|
26
|
+
return filterWorkOSFga(recipe);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readForgeLock(workspaceRoot: string): ForgeLock | null {
|
|
30
|
+
const absolute = join(workspaceRoot, FORGE_LOCK_PATH);
|
|
31
|
+
if (!nodeFileSystem.exists(absolute)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(nodeFileSystem.readText(absolute) ?? "") as ForgeLock;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function lockEnablesWorkOSFga(lock: ForgeLock | null): boolean {
|
|
42
|
+
const entry = lock?.packages.find((pkg) => pkg.name === "@workos-inc/node" || pkg.name === "workos");
|
|
43
|
+
return Boolean(entry?.generatedFiles.some((file) =>
|
|
44
|
+
file === `${GENERATED_DIR}/integrations/workos/fga.ts` ||
|
|
45
|
+
file === `${GENERATED_DIR}/integrations/workos/resource-map.ts`
|
|
46
|
+
));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function seedEnablesWorkOSFga(workspaceRoot: string): boolean {
|
|
50
|
+
for (const path of [
|
|
51
|
+
"workos-seed.yml",
|
|
52
|
+
`${GENERATED_DIR}/integrations/workos/workos-seed.yml`,
|
|
53
|
+
]) {
|
|
54
|
+
const text = nodeFileSystem.readText(join(workspaceRoot, path));
|
|
55
|
+
if (text !== null && /^\s*resource_types\s*:/m.test(text)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function workspaceEnablesWorkOSFga(workspaceRoot: string): boolean {
|
|
63
|
+
return nodeFileSystem.exists(join(workspaceRoot, ".workos-fga-state.json")) ||
|
|
64
|
+
seedEnablesWorkOSFga(workspaceRoot) ||
|
|
65
|
+
lockEnablesWorkOSFga(readForgeLock(workspaceRoot));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function applyWorkspaceRecipeProfile(
|
|
69
|
+
recipe: IntegrationRecipe | null | undefined,
|
|
70
|
+
workspaceRoot: string,
|
|
71
|
+
): IntegrationRecipe | undefined {
|
|
72
|
+
if (!recipe) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (recipe.alias !== "workos") {
|
|
76
|
+
return recipe;
|
|
77
|
+
}
|
|
78
|
+
return applyWorkOSRecipeProfile(recipe, {
|
|
79
|
+
withFga: workspaceEnablesWorkOSFga(workspaceRoot),
|
|
80
|
+
});
|
|
81
|
+
}
|
package/src/forge/dev/server.ts
CHANGED
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
import { authenticateHeaders } from "../runtime/auth/authenticate.ts";
|
|
25
25
|
import { loadAuthConfigFromEnv } from "../runtime/auth/config.ts";
|
|
26
26
|
import { ForgeAuthError } from "../runtime/auth/errors.ts";
|
|
27
|
+
import { resolveAuthFromHeaders } from "../runtime/auth/resolve.ts";
|
|
28
|
+
import type { AuthContext } from "../runtime/auth/types.ts";
|
|
27
29
|
import type { TableMapEntry } from "../compiler/data-graph/sql/serialize.ts";
|
|
28
30
|
import type { SqlPlan } from "../compiler/data-graph/sql/types.ts";
|
|
29
31
|
import { GENERATED_DIR } from "../compiler/emitter/constants.ts";
|
|
@@ -506,6 +508,61 @@ function workOSDevSessionClaims(session: WorkOSDevSession): Record<string, unkno
|
|
|
506
508
|
};
|
|
507
509
|
}
|
|
508
510
|
|
|
511
|
+
function authFromWorkOSDevSessionCookie(
|
|
512
|
+
request: Request,
|
|
513
|
+
envStore: ReturnType<typeof getRuntimeEnvStore>,
|
|
514
|
+
): AuthContext | null {
|
|
515
|
+
const cookiePassword = envStore.resolve("WORKOS_COOKIE_PASSWORD");
|
|
516
|
+
if (!cookiePassword) {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const session = decodeWorkOSDevSession(
|
|
520
|
+
readRequestCookie(request, workOSDevCookieName(envStore)),
|
|
521
|
+
cookiePassword,
|
|
522
|
+
);
|
|
523
|
+
if (!session) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
const claims = workOSDevSessionClaims(session);
|
|
527
|
+
return resolveAuthFromHeaders({
|
|
528
|
+
userId: typeof claims.sub === "string" ? claims.sub : undefined,
|
|
529
|
+
tenantId: typeof claims.organization_id === "string" ? claims.organization_id : undefined,
|
|
530
|
+
organizationId: typeof claims.organization_id === "string" ? claims.organization_id : undefined,
|
|
531
|
+
organizationMembershipId:
|
|
532
|
+
typeof claims.organization_membership_id === "string" ? claims.organization_membership_id : undefined,
|
|
533
|
+
role: typeof claims.role === "string" ? claims.role : undefined,
|
|
534
|
+
roles: Array.isArray(claims.roles) ? claims.roles.filter((role): role is string => typeof role === "string") : [],
|
|
535
|
+
permissions: Array.isArray(claims.permissions)
|
|
536
|
+
? claims.permissions.filter((permission): permission is string => typeof permission === "string")
|
|
537
|
+
: [],
|
|
538
|
+
claims,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function authenticateDevRequest(
|
|
543
|
+
request: Request,
|
|
544
|
+
authConfig: ReturnType<typeof loadAuthConfigFromEnv>,
|
|
545
|
+
envStore: ReturnType<typeof getRuntimeEnvStore>,
|
|
546
|
+
): Promise<AuthContext> {
|
|
547
|
+
if (authConfig.mode === "dev-headers") {
|
|
548
|
+
const headerAuth = await authenticateHeaders(request.headers, authConfig);
|
|
549
|
+
if (headerAuth.kind !== "anonymous") {
|
|
550
|
+
return headerAuth;
|
|
551
|
+
}
|
|
552
|
+
return authFromWorkOSDevSessionCookie(request, envStore) ?? headerAuth;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
return await authenticateHeaders(request.headers, authConfig);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const cookieAuth = authFromWorkOSDevSessionCookie(request, envStore);
|
|
559
|
+
if (cookieAuth) {
|
|
560
|
+
return cookieAuth;
|
|
561
|
+
}
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
509
566
|
function createWorkOSDevSessionCookie(
|
|
510
567
|
envStore: ReturnType<typeof getRuntimeEnvStore>,
|
|
511
568
|
session: WorkOSDevSession,
|
|
@@ -539,17 +596,64 @@ function workOSConfigDiagnostics(envStore: ReturnType<typeof getRuntimeEnvStore>
|
|
|
539
596
|
);
|
|
540
597
|
}
|
|
541
598
|
|
|
542
|
-
function workOSAuthorizationUrl(
|
|
599
|
+
function workOSAuthorizationUrl(
|
|
600
|
+
envStore: ReturnType<typeof getRuntimeEnvStore>,
|
|
601
|
+
state: string,
|
|
602
|
+
organizationId?: string,
|
|
603
|
+
): string {
|
|
543
604
|
const url = new URL("https://api.workos.com/user_management/authorize");
|
|
605
|
+
url.searchParams.set("response_type", "code");
|
|
544
606
|
url.searchParams.set("provider", "authkit");
|
|
545
607
|
url.searchParams.set("client_id", envStore.resolve("WORKOS_CLIENT_ID") ?? "");
|
|
546
608
|
url.searchParams.set("redirect_uri", envStore.resolve("WORKOS_REDIRECT_URI") ?? "");
|
|
609
|
+
const effectiveOrganizationId = normalizeOptionalId(organizationId);
|
|
610
|
+
if (effectiveOrganizationId) {
|
|
611
|
+
url.searchParams.set("organization_id", effectiveOrganizationId);
|
|
612
|
+
}
|
|
547
613
|
if (state) {
|
|
548
614
|
url.searchParams.set("state", state);
|
|
549
615
|
}
|
|
550
616
|
return url.toString();
|
|
551
617
|
}
|
|
552
618
|
|
|
619
|
+
function normalizeOptionalId(value: string | undefined): string | undefined {
|
|
620
|
+
const trimmed = value?.trim();
|
|
621
|
+
if (!trimmed || trimmed === "undefined" || trimmed === "null") return undefined;
|
|
622
|
+
return trimmed;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function uniqueStrings(values: Iterable<unknown>): string[] {
|
|
626
|
+
return [...new Set([...values].filter((value): value is string => typeof value === "string" && value.length > 0))];
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function permissionsFromPolicyRegistry(workspaceRoot: string): string[] {
|
|
630
|
+
const registryPath = join(workspaceRoot, `${GENERATED_DIR}/policyRegistry.json`);
|
|
631
|
+
if (!existsSync(registryPath)) return [];
|
|
632
|
+
try {
|
|
633
|
+
const registry = readGeneratedJson<{
|
|
634
|
+
policies?: Array<{ permissions?: unknown[] }>;
|
|
635
|
+
}>(workspaceRoot, `${GENERATED_DIR}/policyRegistry.json`);
|
|
636
|
+
return uniqueStrings(((registry?.policies ?? [])).flatMap((policy) => policy.permissions ?? []));
|
|
637
|
+
} catch {
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function deriveDevPermissionsForRole(workspaceRoot: string, role: string | undefined): string[] {
|
|
643
|
+
const permissions = permissionsFromPolicyRegistry(workspaceRoot);
|
|
644
|
+
if (permissions.length === 0) return [];
|
|
645
|
+
const normalizedRole = role?.toLowerCase();
|
|
646
|
+
if (normalizedRole && ["owner", "admin", "manager", "security"].includes(normalizedRole)) {
|
|
647
|
+
return permissions;
|
|
648
|
+
}
|
|
649
|
+
if (normalizedRole === "requester") {
|
|
650
|
+
return permissions.filter((permission) =>
|
|
651
|
+
/(^|:)read$|read|list|view|audit|request|demo:seed/i.test(permission),
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
return permissions.filter((permission) => /(^|:)read$|read|list|view|audit|demo:seed/i.test(permission));
|
|
655
|
+
}
|
|
656
|
+
|
|
553
657
|
async function authenticateWorkOSCode(workspaceRoot: string, envStore: ReturnType<typeof getRuntimeEnvStore>, code: string) {
|
|
554
658
|
const req = createRequire(join(workspaceRoot, "package.json"));
|
|
555
659
|
const localEntry = join(workspaceRoot, "node_modules/@workos-inc/node/index.js");
|
|
@@ -569,14 +673,60 @@ async function authenticateWorkOSCode(workspaceRoot: string, envStore: ReturnTyp
|
|
|
569
673
|
clientId: envStore.resolve("WORKOS_CLIENT_ID") ?? "",
|
|
570
674
|
})
|
|
571
675
|
: null;
|
|
572
|
-
const userManagement = (client as {
|
|
676
|
+
const userManagement = (client as {
|
|
677
|
+
userManagement?: {
|
|
678
|
+
authenticateWithCode?: (input: { code: string; clientId: string }) => Promise<Record<string, unknown>>;
|
|
679
|
+
listOrganizationMemberships?: (input: { userId?: string; organizationId?: string }) => Promise<unknown>;
|
|
680
|
+
};
|
|
681
|
+
} | null)?.userManagement;
|
|
573
682
|
if (!userManagement?.authenticateWithCode) {
|
|
574
683
|
throw new Error("@workos-inc/node does not expose userManagement.authenticateWithCode");
|
|
575
684
|
}
|
|
576
|
-
|
|
685
|
+
const result = await userManagement.authenticateWithCode({
|
|
577
686
|
code,
|
|
578
687
|
clientId: envStore.resolve("WORKOS_CLIENT_ID") ?? "",
|
|
579
688
|
});
|
|
689
|
+
const user = (result.user ?? {}) as Record<string, unknown>;
|
|
690
|
+
const userId = typeof user.id === "string" ? user.id : undefined;
|
|
691
|
+
const defaultOrganizationId =
|
|
692
|
+
normalizeOptionalId(envStore.resolve("WORKOS_DEFAULT_ORGANIZATION_ID")) ||
|
|
693
|
+
normalizeOptionalId(envStore.resolve("WORKOS_ORGANIZATION_ID"));
|
|
694
|
+
const hasOrganization =
|
|
695
|
+
typeof result.organizationId === "string" ||
|
|
696
|
+
(typeof ((result.organization ?? {}) as Record<string, unknown>).id === "string");
|
|
697
|
+
if (!hasOrganization && userId && defaultOrganizationId && userManagement.listOrganizationMemberships) {
|
|
698
|
+
const membershipList = await userManagement.listOrganizationMemberships({
|
|
699
|
+
userId,
|
|
700
|
+
organizationId: defaultOrganizationId,
|
|
701
|
+
});
|
|
702
|
+
const data = (membershipList as { data?: unknown[] }).data;
|
|
703
|
+
const membership = Array.isArray(data)
|
|
704
|
+
? data.find((item) => {
|
|
705
|
+
const record = item as Record<string, unknown>;
|
|
706
|
+
return record.status === undefined || record.status === "active";
|
|
707
|
+
}) as Record<string, unknown> | undefined
|
|
708
|
+
: undefined;
|
|
709
|
+
if (membership) {
|
|
710
|
+
const roleRecord = membership.role as Record<string, unknown> | undefined;
|
|
711
|
+
const roles = Array.isArray(membership.roles)
|
|
712
|
+
? membership.roles
|
|
713
|
+
.map((role) => (typeof role === "string" ? role : (role as Record<string, unknown>).slug))
|
|
714
|
+
.filter((role): role is string => typeof role === "string")
|
|
715
|
+
: [];
|
|
716
|
+
const role = typeof roleRecord?.slug === "string" ? roleRecord.slug : roles[0];
|
|
717
|
+
return {
|
|
718
|
+
...result,
|
|
719
|
+
organizationId: defaultOrganizationId,
|
|
720
|
+
organization: { id: defaultOrganizationId },
|
|
721
|
+
organizationMembershipId: typeof membership.id === "string" ? membership.id : undefined,
|
|
722
|
+
organizationMembership: membership,
|
|
723
|
+
...(role ? { role } : {}),
|
|
724
|
+
...(roles.length > 0 ? { roles } : role ? { roles: [role] } : {}),
|
|
725
|
+
permissions: deriveDevPermissionsForRole(workspaceRoot, role),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return result;
|
|
580
730
|
}
|
|
581
731
|
|
|
582
732
|
function acceptsHtml(request: Request): boolean {
|
|
@@ -1409,8 +1559,10 @@ export async function startDevServer(
|
|
|
1409
1559
|
if (diagnostics.length > 0) {
|
|
1410
1560
|
return jsonResponse({ ok: false, diagnostics }, 503);
|
|
1411
1561
|
}
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1562
|
+
const url = new URL(request.url);
|
|
1563
|
+
const returnTo = normalizeLocalRedirect(url.searchParams.get("returnTo"));
|
|
1564
|
+
const organizationId = url.searchParams.get("organizationId") ?? url.searchParams.get("organization_id") ?? undefined;
|
|
1565
|
+
return redirectResponse(workOSAuthorizationUrl(envStore, returnTo, organizationId));
|
|
1414
1566
|
}
|
|
1415
1567
|
|
|
1416
1568
|
if (pathname === "/callback") {
|
|
@@ -1733,7 +1885,7 @@ export async function startDevServer(
|
|
|
1733
1885
|
}
|
|
1734
1886
|
}
|
|
1735
1887
|
|
|
1736
|
-
const auth = await
|
|
1888
|
+
const auth = await authenticateDevRequest(request, authConfig, getRuntimeEnvStore(workspaceRoot));
|
|
1737
1889
|
const lastRevision = Number(
|
|
1738
1890
|
request.headers.get("last-event-id") ??
|
|
1739
1891
|
url.searchParams.get("lastRevision") ??
|
|
@@ -1857,7 +2009,7 @@ export async function startDevServer(
|
|
|
1857
2009
|
400,
|
|
1858
2010
|
);
|
|
1859
2011
|
}
|
|
1860
|
-
const auth = await
|
|
2012
|
+
const auth = await authenticateDevRequest(request, authConfig, getRuntimeEnvStore(workspaceRoot));
|
|
1861
2013
|
const envStore = getRuntimeEnvStore(workspaceRoot);
|
|
1862
2014
|
const secretRegistry = loadSecretRegistry(workspaceRoot);
|
|
1863
2015
|
const bundle = createRuntimeSecretsBundle({
|
|
@@ -1977,7 +2129,7 @@ export async function startDevServer(
|
|
|
1977
2129
|
}
|
|
1978
2130
|
|
|
1979
2131
|
const purpose = body.purpose ?? "dev_agent_chat";
|
|
1980
|
-
const auth = await
|
|
2132
|
+
const auth = await authenticateDevRequest(request, authConfig, getRuntimeEnvStore(workspaceRoot));
|
|
1981
2133
|
const envStore = getRuntimeEnvStore(workspaceRoot);
|
|
1982
2134
|
const secretRegistry = loadSecretRegistry(workspaceRoot);
|
|
1983
2135
|
const bundle = createRuntimeSecretsBundle({
|
|
@@ -2392,7 +2544,7 @@ export async function startDevServer(
|
|
|
2392
2544
|
const externalInvoke = parseExternalInvoke(pathname);
|
|
2393
2545
|
if (externalInvoke) {
|
|
2394
2546
|
const args = await parseRequestArgs(request);
|
|
2395
|
-
const auth = await
|
|
2547
|
+
const auth = await authenticateDevRequest(request, authConfig, getRuntimeEnvStore(workspaceRoot));
|
|
2396
2548
|
const result = await runExternalEntry(
|
|
2397
2549
|
workspaceRoot,
|
|
2398
2550
|
{
|
|
@@ -2460,7 +2612,7 @@ export async function startDevServer(
|
|
|
2460
2612
|
}
|
|
2461
2613
|
|
|
2462
2614
|
const args = await parseRequestArgs(request);
|
|
2463
|
-
const auth = await
|
|
2615
|
+
const auth = await authenticateDevRequest(request, authConfig, getRuntimeEnvStore(workspaceRoot));
|
|
2464
2616
|
|
|
2465
2617
|
const result = await runQuery(
|
|
2466
2618
|
workspaceRoot,
|
|
@@ -2533,7 +2685,7 @@ export async function startDevServer(
|
|
|
2533
2685
|
}
|
|
2534
2686
|
|
|
2535
2687
|
const args = await parseRequestArgs(request);
|
|
2536
|
-
const auth = await
|
|
2688
|
+
const auth = await authenticateDevRequest(request, authConfig, getRuntimeEnvStore(workspaceRoot));
|
|
2537
2689
|
|
|
2538
2690
|
await prepareRuntimeEnvironment(workspaceRoot, {
|
|
2539
2691
|
mock: options.mock,
|
package/src/forge/ui/index.ts
CHANGED
|
@@ -233,20 +233,6 @@ function hasWorkOSIntegration(workspaceRoot: string): boolean {
|
|
|
233
233
|
nodeFileSystem.exists(join(workspaceRoot, `${GENERATED}/integrations/workos/auth-routes.ts`));
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
function webPackageHasAuthKit(workspaceRoot: string): boolean {
|
|
237
|
-
const path = join(workspaceRoot, "web/package.json");
|
|
238
|
-
if (!nodeFileSystem.exists(path)) return false;
|
|
239
|
-
try {
|
|
240
|
-
const packageJson = JSON.parse(nodeFileSystem.readText(path) ?? "{}") as {
|
|
241
|
-
dependencies?: Record<string, string>;
|
|
242
|
-
devDependencies?: Record<string, string>;
|
|
243
|
-
};
|
|
244
|
-
return Boolean(packageJson.dependencies?.["@workos-inc/authkit-react"] ?? packageJson.devDependencies?.["@workos-inc/authkit-react"]);
|
|
245
|
-
} catch {
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
236
|
type UiPackageManager = "bun" | "npm" | "pnpm" | "yarn";
|
|
251
237
|
|
|
252
238
|
interface UiPackageContext {
|
|
@@ -1440,23 +1426,19 @@ function runUiAudit(options: UiCommandOptions): UiCommandResult {
|
|
|
1440
1426
|
hasWorkOSIntegration(options.workspaceRoot) &&
|
|
1441
1427
|
manifest.webRoot &&
|
|
1442
1428
|
webSources.length > 0 &&
|
|
1443
|
-
(
|
|
1444
|
-
(!/AuthKitProvider/.test(appShellText) && !/ForgeWorkOSAuthProvider/.test(appShellText)) ||
|
|
1445
|
-
(!/getToken/.test(webImplementationText) && !/getAccessToken/.test(webImplementationText)))
|
|
1429
|
+
!/ForgeWorkOSAuthProvider/.test(appShellText)
|
|
1446
1430
|
) {
|
|
1447
1431
|
diagnostics.push(diagnostic(
|
|
1448
1432
|
"warning",
|
|
1449
1433
|
"FORGE_UI_WORKOS_AUTHKIT_MISSING",
|
|
1450
|
-
"WorkOS integration is present, but the web app does not appear to mount
|
|
1434
|
+
"WorkOS integration is present, but the web app does not appear to mount ForgeWorkOSAuthProvider.",
|
|
1451
1435
|
));
|
|
1452
1436
|
}
|
|
1453
1437
|
if (
|
|
1454
1438
|
hasWorkOSIntegration(options.workspaceRoot) &&
|
|
1455
1439
|
manifest.webRoot &&
|
|
1456
1440
|
webSources.length > 0 &&
|
|
1457
|
-
|
|
1458
|
-
(/AuthKitProvider/.test(appShellText) || /ForgeWorkOSAuthProvider/.test(appShellText)) &&
|
|
1459
|
-
(/getToken/.test(webImplementationText) || /getAccessToken/.test(webImplementationText)) &&
|
|
1441
|
+
/ForgeWorkOSAuthProvider/.test(appShellText) &&
|
|
1460
1442
|
(!webUsesWorkOSSessionClaims(webImplementationText) || !webConfigProxiesWorkOSSession(options.workspaceRoot, manifest.webRoot))
|
|
1461
1443
|
) {
|
|
1462
1444
|
diagnostics.push(diagnostic(
|
package/src/forge/version.ts
CHANGED