forgeos 0.1.0-alpha.55 → 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 +38 -2
- package/docs/changelog.md +32 -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 +2 -0
- package/src/forge/cli/deploy.ts +169 -22
- package/src/forge/cli/dev.ts +5 -2
- package/src/forge/cli/main.ts +6 -1
- package/src/forge/cli/parse.ts +15 -5
- package/src/forge/cli/secrets.ts +131 -1
- 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/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;
|
|
@@ -418,6 +419,7 @@ export type ForgeCommand =
|
|
|
418
419
|
subcommand: EnvSubcommand;
|
|
419
420
|
json: boolean;
|
|
420
421
|
redacted: boolean;
|
|
422
|
+
target?: "local" | "staging" | "production";
|
|
421
423
|
workspaceRoot: string;
|
|
422
424
|
}
|
|
423
425
|
| {
|
|
@@ -602,7 +604,7 @@ const BASELINE_SUBCOMMANDS: BaselineSubcommand[] = ["create", "status"];
|
|
|
602
604
|
const AUTHMD_SUBCOMMANDS: AuthMdSubcommand[] = ["generate", "check"];
|
|
603
605
|
const WORKOS_SUBCOMMANDS: WorkOSSubcommand[] = ["install", "doctor", "seed", "setup", "prove", "fga"];
|
|
604
606
|
const WORKOS_FGA_ACTIONS: WorkOSFgaAction[] = ["plan", "sync", "prove", "doctor"];
|
|
605
|
-
const DEPLOY_SUBCOMMANDS: DeploySubcommand[] = ["plan", "check", "render", "package", "verify"];
|
|
607
|
+
const DEPLOY_SUBCOMMANDS: DeploySubcommand[] = ["plan", "init", "check", "readiness", "render", "package", "verify"];
|
|
606
608
|
const FIELD_TEST_SUBCOMMANDS: FieldTestSubcommand[] = ["create", "run", "report"];
|
|
607
609
|
const SECURITY_SUBCOMMANDS: SecuritySubcommand[] = ["prove"];
|
|
608
610
|
const RLS_SUBCOMMANDS: RlsSubcommand[] = ["generate", "check", "apply", "test", "mutate-test"];
|
|
@@ -1484,10 +1486,10 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
1484
1486
|
case "deploy": {
|
|
1485
1487
|
const subcommand = rest[0] as DeploySubcommand | undefined;
|
|
1486
1488
|
if (!subcommand || !DEPLOY_SUBCOMMANDS.includes(subcommand)) {
|
|
1487
|
-
errors.push("forge deploy requires subcommand: plan, check, render, package, or verify");
|
|
1489
|
+
errors.push("forge deploy requires subcommand: plan, init, check, readiness, render, package, or verify");
|
|
1488
1490
|
return { command: null, workspaceRoot, errors };
|
|
1489
1491
|
}
|
|
1490
|
-
const targetRaw = parseOptionValue(argv, "--target") ?? (subcommand === "render" || subcommand === "package" ? rest[1] : undefined) ?? "docker";
|
|
1492
|
+
const targetRaw = parseOptionValue(argv, "--target") ?? (subcommand === "init" || subcommand === "render" || subcommand === "package" ? rest[1] : undefined) ?? "docker";
|
|
1491
1493
|
if (targetRaw !== "docker" && targetRaw !== "forge-cloud") {
|
|
1492
1494
|
errors.push("forge deploy --target must be docker or forge-cloud");
|
|
1493
1495
|
}
|
|
@@ -2732,6 +2734,7 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
2732
2734
|
webOnly: parseFlag(argv, "--web-only"),
|
|
2733
2735
|
open: parseFlag(argv, "--open"),
|
|
2734
2736
|
webPort,
|
|
2737
|
+
publicApiUrl: parseOptionValue(argv, "--public-api-url"),
|
|
2735
2738
|
telemetry: (parseOptionValue(argv, "--telemetry") ?? "local")
|
|
2736
2739
|
.split(",")
|
|
2737
2740
|
.map((value) => value.trim())
|
|
@@ -2974,10 +2977,14 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
2974
2977
|
}
|
|
2975
2978
|
case "env": {
|
|
2976
2979
|
const subcommand = rest[0] as EnvSubcommand | undefined;
|
|
2977
|
-
if (!subcommand || !["list", "check", "print"].includes(subcommand)) {
|
|
2978
|
-
errors.push("forge env requires subcommand: list, check, or
|
|
2980
|
+
if (!subcommand || !["list", "check", "print", "doctor"].includes(subcommand)) {
|
|
2981
|
+
errors.push("forge env requires subcommand: list, check, print, or doctor");
|
|
2979
2982
|
return { command: null, workspaceRoot, errors };
|
|
2980
2983
|
}
|
|
2984
|
+
const targetRaw = parseOptionValue(argv, "--target") ?? "local";
|
|
2985
|
+
if (!["local", "staging", "production"].includes(targetRaw)) {
|
|
2986
|
+
errors.push("forge env --target must be local, staging, or production");
|
|
2987
|
+
}
|
|
2981
2988
|
|
|
2982
2989
|
return {
|
|
2983
2990
|
command: {
|
|
@@ -2985,6 +2992,7 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
2985
2992
|
subcommand,
|
|
2986
2993
|
json: parseFlag(argv, "--json"),
|
|
2987
2994
|
redacted: parseFlag(argv, "--redacted"),
|
|
2995
|
+
target: targetRaw as "local" | "staging" | "production",
|
|
2988
2996
|
workspaceRoot,
|
|
2989
2997
|
},
|
|
2990
2998
|
workspaceRoot,
|
|
@@ -3244,6 +3252,7 @@ export function hasUnknownOption(argv: string[]): string | null {
|
|
|
3244
3252
|
"--postgres-version",
|
|
3245
3253
|
"--runtime-port",
|
|
3246
3254
|
"--web-port",
|
|
3255
|
+
"--public-api-url",
|
|
3247
3256
|
"--preview-port",
|
|
3248
3257
|
"--preview-url",
|
|
3249
3258
|
"--studio-url",
|
|
@@ -3371,6 +3380,7 @@ export function hasUnknownOption(argv: string[]): string | null {
|
|
|
3371
3380
|
arg === "--postgres-version" ||
|
|
3372
3381
|
arg === "--runtime-port" ||
|
|
3373
3382
|
arg === "--web-port" ||
|
|
3383
|
+
arg === "--public-api-url" ||
|
|
3374
3384
|
arg === "--preview-port" ||
|
|
3375
3385
|
arg === "--preview-url" ||
|
|
3376
3386
|
arg === "--studio-url" ||
|
package/src/forge/cli/secrets.ts
CHANGED
|
@@ -219,13 +219,141 @@ export function formatSecretsJson(result: SecretsCommandResult): string {
|
|
|
219
219
|
return `${JSON.stringify(result, null, 2)}\n`;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
export type EnvSubcommand = "list" | "check" | "print";
|
|
222
|
+
export type EnvSubcommand = "list" | "check" | "print" | "doctor";
|
|
223
|
+
export type EnvDoctorTarget = "local" | "staging" | "production";
|
|
223
224
|
|
|
224
225
|
export interface EnvCommandOptions {
|
|
225
226
|
subcommand: EnvSubcommand;
|
|
226
227
|
workspaceRoot: string;
|
|
227
228
|
json: boolean;
|
|
228
229
|
redacted?: boolean;
|
|
230
|
+
target?: EnvDoctorTarget;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const ENV_DOCTOR_KEYS = [
|
|
234
|
+
"DATABASE_URL",
|
|
235
|
+
"FORGE_AUTH_MODE",
|
|
236
|
+
"FORGE_AUTH_ISSUER",
|
|
237
|
+
"FORGE_AUTH_AUDIENCE",
|
|
238
|
+
"FORGE_AUTH_JWKS_URI",
|
|
239
|
+
"FORGE_AUTH_DISCOVERY_URL",
|
|
240
|
+
"WORKOS_API_KEY",
|
|
241
|
+
"WORKOS_CLIENT_ID",
|
|
242
|
+
"WORKOS_COOKIE_PASSWORD",
|
|
243
|
+
"WORKOS_REDIRECT_URI",
|
|
244
|
+
"WORKOS_POST_LOGIN_REDIRECT_URI",
|
|
245
|
+
"WORKOS_POST_LOGOUT_REDIRECT_URI",
|
|
246
|
+
"WORKOS_WEBHOOK_SECRET",
|
|
247
|
+
] as const;
|
|
248
|
+
|
|
249
|
+
function parseEnvFile(workspaceRoot: string, relative: string): Record<string, string> {
|
|
250
|
+
const path = join(workspaceRoot, relative);
|
|
251
|
+
if (!nodeFileSystem.exists(path)) return {};
|
|
252
|
+
const values: Record<string, string> = {};
|
|
253
|
+
for (const rawLine of (nodeFileSystem.readText(path) ?? "").split(/\r?\n/)) {
|
|
254
|
+
const line = rawLine.trim();
|
|
255
|
+
if (!line || line.startsWith("#")) continue;
|
|
256
|
+
const eq = line.indexOf("=");
|
|
257
|
+
if (eq <= 0) continue;
|
|
258
|
+
const rawValue = line.slice(eq + 1).trim();
|
|
259
|
+
values[line.slice(0, eq).trim()] = rawValue.replace(/^["']|["']$/g, "");
|
|
260
|
+
}
|
|
261
|
+
return values;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function envDoctorFiles(target: EnvDoctorTarget): string[] {
|
|
265
|
+
if (target === "production") return ["deploy/.env.production"];
|
|
266
|
+
if (target === "staging") return ["deploy/.env.staging", ".env.staging"];
|
|
267
|
+
return [".env", ".env.local"];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function hasWorkOSAppArtifacts(workspaceRoot: string): boolean {
|
|
271
|
+
return nodeFileSystem.exists(join(workspaceRoot, "workos-seed.yml")) ||
|
|
272
|
+
nodeFileSystem.exists(join(workspaceRoot, "src/policies.workos.ts")) ||
|
|
273
|
+
nodeFileSystem.exists(join(workspaceRoot, "src/forge/_generated/integrations/workos/auth-routes.ts"));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function envDoctor(options: EnvCommandOptions): SecretsCommandResult {
|
|
277
|
+
const target = options.target ?? "local";
|
|
278
|
+
const files = envDoctorFiles(target).map((path) => {
|
|
279
|
+
const values = parseEnvFile(options.workspaceRoot, path);
|
|
280
|
+
return {
|
|
281
|
+
path,
|
|
282
|
+
present: nodeFileSystem.exists(join(options.workspaceRoot, path)),
|
|
283
|
+
keys: Object.keys(values).filter((key) => ENV_DOCTOR_KEYS.includes(key as (typeof ENV_DOCTOR_KEYS)[number])).sort(),
|
|
284
|
+
values,
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
const processValues = Object.fromEntries(
|
|
288
|
+
ENV_DOCTOR_KEYS.filter((key) => Boolean(process.env[key])).map((key) => [key, process.env[key]!]),
|
|
289
|
+
);
|
|
290
|
+
const effective = {
|
|
291
|
+
...Object.assign({}, ...files.map((file) => file.values)),
|
|
292
|
+
...processValues,
|
|
293
|
+
} as Record<string, string>;
|
|
294
|
+
const required = target === "production"
|
|
295
|
+
? ["DATABASE_URL", "FORGE_AUTH_MODE", "FORGE_AUTH_ISSUER", "FORGE_AUTH_AUDIENCE"]
|
|
296
|
+
: ["FORGE_AUTH_MODE"];
|
|
297
|
+
const missing = required.filter((key) => !effective[key]);
|
|
298
|
+
const authMode = effective.FORGE_AUTH_MODE ?? "dev-headers";
|
|
299
|
+
const productionAuth = authMode === "jwt" || authMode === "oidc";
|
|
300
|
+
const database = effective.DATABASE_URL
|
|
301
|
+
? "postgres"
|
|
302
|
+
: target === "production"
|
|
303
|
+
? "missing"
|
|
304
|
+
: "local-dev";
|
|
305
|
+
const workosDetected = hasWorkOSAppArtifacts(options.workspaceRoot) ||
|
|
306
|
+
Boolean(effective.WORKOS_CLIENT_ID || effective.WORKOS_API_KEY);
|
|
307
|
+
const provider = workosDetected ? "workos" : "none";
|
|
308
|
+
const hasJwtSource = Boolean(effective.FORGE_AUTH_JWKS_URI || effective.FORGE_AUTH_DISCOVERY_URL);
|
|
309
|
+
const blockers = [
|
|
310
|
+
...missing.filter((key) => key !== "DATABASE_URL").map((key) => `${key} missing`),
|
|
311
|
+
...(target === "production" && !productionAuth ? [`FORGE_AUTH_MODE=${authMode} is not production auth`] : []),
|
|
312
|
+
...(target === "production" && database === "missing" ? ["DATABASE_URL missing"] : []),
|
|
313
|
+
...(target === "production" && productionAuth && !hasJwtSource
|
|
314
|
+
? ["FORGE_AUTH_JWKS_URI or FORGE_AUTH_DISCOVERY_URL missing"]
|
|
315
|
+
: []),
|
|
316
|
+
...(target === "production" && workosDetected && !effective.WORKOS_CLIENT_ID ? ["WORKOS_CLIENT_ID missing"] : []),
|
|
317
|
+
...(target === "production" && workosDetected && !effective.WORKOS_API_KEY ? ["WORKOS_API_KEY missing"] : []),
|
|
318
|
+
];
|
|
319
|
+
const warnings = [
|
|
320
|
+
...(target === "production" && files.every((file) => !file.present)
|
|
321
|
+
? ["deploy/.env.production not found; process.env alone may be fine in CI but is easy for agents to miss"]
|
|
322
|
+
: []),
|
|
323
|
+
...(workosDetected && !effective.WORKOS_COOKIE_PASSWORD ? ["WORKOS_COOKIE_PASSWORD missing"] : []),
|
|
324
|
+
...(workosDetected && !effective.WORKOS_REDIRECT_URI ? ["WORKOS_REDIRECT_URI missing"] : []),
|
|
325
|
+
];
|
|
326
|
+
return {
|
|
327
|
+
exitCode: blockers.length === 0 ? 0 : 1,
|
|
328
|
+
data: {
|
|
329
|
+
schemaVersion: "0.1.0",
|
|
330
|
+
kind: "env-doctor",
|
|
331
|
+
ok: blockers.length === 0,
|
|
332
|
+
target,
|
|
333
|
+
authMode,
|
|
334
|
+
productionAuth,
|
|
335
|
+
database,
|
|
336
|
+
provider,
|
|
337
|
+
sources: [
|
|
338
|
+
{
|
|
339
|
+
path: "process.env",
|
|
340
|
+
present: Object.keys(processValues).length > 0,
|
|
341
|
+
keys: Object.keys(processValues).sort(),
|
|
342
|
+
values: undefined,
|
|
343
|
+
},
|
|
344
|
+
...files.map(({ path, present, keys }) => ({ path, present, keys })),
|
|
345
|
+
],
|
|
346
|
+
present: ENV_DOCTOR_KEYS.filter((key) => Boolean(effective[key])).sort(),
|
|
347
|
+
missing,
|
|
348
|
+
blockers,
|
|
349
|
+
warnings,
|
|
350
|
+
nextActions: blockers.length === 0
|
|
351
|
+
? ["forge deploy readiness --production --json"]
|
|
352
|
+
: target === "production"
|
|
353
|
+
? ["cp deploy/.env.production.example deploy/.env.production", "forge env doctor --target production --json"]
|
|
354
|
+
: ["forge env doctor --target local --json"],
|
|
355
|
+
},
|
|
356
|
+
};
|
|
229
357
|
}
|
|
230
358
|
|
|
231
359
|
export async function runEnvCommand(options: EnvCommandOptions): Promise<SecretsCommandResult> {
|
|
@@ -233,6 +361,8 @@ export async function runEnvCommand(options: EnvCommandOptions): Promise<Secrets
|
|
|
233
361
|
const schema = loadEnvSchema(options.workspaceRoot);
|
|
234
362
|
|
|
235
363
|
switch (options.subcommand) {
|
|
364
|
+
case "doctor":
|
|
365
|
+
return envDoctor(options);
|
|
236
366
|
case "list":
|
|
237
367
|
if (!schema) {
|
|
238
368
|
return {
|
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];
|