forgeos 0.1.0-alpha.53 → 0.1.0-alpha.55
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 +34 -0
- package/docs/changelog.md +21 -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/deploy.ts +39 -22
- package/src/forge/cli/main.ts +3 -2
- package/src/forge/cli/parse.ts +2 -0
- package/src/forge/cli/workos.ts +101 -37
- package/src/forge/compiler/integration/add.ts +36 -15
- package/src/forge/compiler/integration/templates/workos.ts +52 -16
- package/src/forge/compiler/types/cli.ts +1 -0
- 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.55 input=bbd63535f88ad4d382f6da1f26a2898e49fa00849042fcd683e316054e3b3959 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
|
|
2
2
|
# AGENTS.md
|
|
3
3
|
|
|
4
4
|
<!-- forge-generated:start -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# forgeos
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.55
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Make WorkOS FGA opt-in instead of part of the default auth adapter path.
|
|
8
|
+
|
|
9
|
+
- `forge add auth workos` now generates the AuthKit/RBAC/permission-claims
|
|
10
|
+
adapter, seed, policies, tenant claim mapping, webhook helpers, and React
|
|
11
|
+
bridge without WorkOS FGA resource helpers by default.
|
|
12
|
+
- `forge add auth workos --with-fga` keeps generating the optional WorkOS
|
|
13
|
+
Authorization/FGA resource graph helpers, `resource_types` seed metadata,
|
|
14
|
+
cache/telemetry helpers, and cross-tenant resource guard.
|
|
15
|
+
- `forge workos doctor` and `forge workos seed` now treat `resource_types`
|
|
16
|
+
and FGA state as optional unless the app has opted into FGA artifacts.
|
|
17
|
+
- `forge deploy check --production` no longer blocks ordinary WorkOS apps on
|
|
18
|
+
`workos-fga-proof`; it still requires FGA sync/proof when FGA helpers or
|
|
19
|
+
FGA state are present.
|
|
20
|
+
|
|
21
|
+
## 0.1.0-alpha.54
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Add a machine-readable WorkOS FGA hosted setup artifact.
|
|
26
|
+
|
|
27
|
+
- `forge workos fga plan --write --json` and real sync failures that write
|
|
28
|
+
`.forge/workos-fga-setup.md` now also write
|
|
29
|
+
`.forge/workos-fga-setup.json`.
|
|
30
|
+
- The JSON artifact includes the derived resource types, hosted setup
|
|
31
|
+
boundary, rerun commands, docs, unsupported automation notes, and paired
|
|
32
|
+
Markdown/JSON paths so agents and CI can consume the FGA handoff without
|
|
33
|
+
parsing Markdown.
|
|
34
|
+
- Human output now prints both the Markdown guide and JSON setup artifact
|
|
35
|
+
paths when they are written.
|
|
36
|
+
|
|
3
37
|
## 0.1.0-alpha.53
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/docs/changelog.md
CHANGED
|
@@ -6,6 +6,27 @@ The canonical source file in the repository is `CHANGELOG.md`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.1.0-alpha.55
|
|
10
|
+
|
|
11
|
+
- `forge add auth workos` now defaults to AuthKit/RBAC, permission claims,
|
|
12
|
+
Forge policies, tenant claim mapping, seed, webhooks, and frontend bridge
|
|
13
|
+
without generating WorkOS FGA resource helpers.
|
|
14
|
+
- `forge add auth workos --with-fga` enables the optional WorkOS FGA resource
|
|
15
|
+
graph helpers and `resource_types` seed metadata for apps that need
|
|
16
|
+
resource-level authorization.
|
|
17
|
+
- WorkOS doctor/seed/deploy checks now treat FGA as optional unless the app has
|
|
18
|
+
FGA artifacts or FGA state, so normal WorkOS apps are gated by AuthKit, RBAC,
|
|
19
|
+
hosted seed evidence, policies, and tenant proof instead of mandatory FGA
|
|
20
|
+
proof.
|
|
21
|
+
|
|
22
|
+
## 0.1.0-alpha.54
|
|
23
|
+
|
|
24
|
+
- WorkOS FGA setup writes now produce a paired machine-readable
|
|
25
|
+
`.forge/workos-fga-setup.json` artifact alongside the Markdown guide.
|
|
26
|
+
- The JSON artifact includes derived resource types, hosted setup boundary,
|
|
27
|
+
rerun commands, docs, unsupported automation notes, and paired artifact paths
|
|
28
|
+
for agents and CI.
|
|
29
|
+
|
|
9
30
|
## 0.1.0-alpha.53
|
|
10
31
|
|
|
11
32
|
- `forge workos fga doctor --json` now distinguishes local FGA proof timestamps
|
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.55","releaseId":"forgeos@0.1.0-alpha.55+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.55 input=bbd63535f88ad4d382f6da1f26a2898e49fa00849042fcd683e316054e3b3959 content=c6fc38ddd436db8a660ed2e37713bc2d76c3d305cd1ebcb207de25ac1830b307
|
|
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.55",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.55+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
package/src/forge/cli/deploy.ts
CHANGED
|
@@ -101,6 +101,12 @@ function hasWorkOSIntegration(workspaceRoot: string): boolean {
|
|
|
101
101
|
nodeFileSystem.exists(join(workspaceRoot, "src/policies.workos.ts"));
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function hasWorkOSFgaIntegration(workspaceRoot: string): boolean {
|
|
105
|
+
return nodeFileSystem.exists(join(workspaceRoot, `${GENERATED_DIR}/integrations/workos/fga.ts`)) ||
|
|
106
|
+
nodeFileSystem.exists(join(workspaceRoot, `${GENERATED_DIR}/integrations/workos/resource-map.ts`)) ||
|
|
107
|
+
nodeFileSystem.exists(join(workspaceRoot, ".workos-fga-state.json"));
|
|
108
|
+
}
|
|
109
|
+
|
|
104
110
|
function readJsonFile<T>(workspaceRoot: string, relative: string): T | null {
|
|
105
111
|
const absolute = join(workspaceRoot, relative);
|
|
106
112
|
if (!nodeFileSystem.exists(absolute)) return null;
|
|
@@ -821,6 +827,7 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
|
|
|
821
827
|
},
|
|
822
828
|
});
|
|
823
829
|
if (hasWorkOSIntegration(options.workspaceRoot)) {
|
|
830
|
+
const workosFgaEnabled = hasWorkOSFgaIntegration(options.workspaceRoot);
|
|
824
831
|
const workosDoctor = await withDeployEnv(options.workspaceRoot, async () =>
|
|
825
832
|
runWorkOSCommand({
|
|
826
833
|
subcommand: "doctor",
|
|
@@ -835,7 +842,7 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
|
|
|
835
842
|
ok: workosDoctor.exitCode === 0,
|
|
836
843
|
severity: options.production ? "error" : "warning",
|
|
837
844
|
message: workosDoctor.exitCode === 0
|
|
838
|
-
?
|
|
845
|
+
? `WorkOS adapter files, seed, claims, policies, and tenant guards are app-aware${workosFgaEnabled ? " with FGA enabled" : ""}`
|
|
839
846
|
: "WorkOS adapter is incomplete for this app; run forge workos doctor --json",
|
|
840
847
|
command: "forge workos doctor --json",
|
|
841
848
|
details: workosDoctor.checks,
|
|
@@ -855,27 +862,37 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
|
|
|
855
862
|
command: "forge workos prove --real --file workos-seed.yml --json",
|
|
856
863
|
details: seedState,
|
|
857
864
|
});
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
865
|
+
if (workosFgaEnabled) {
|
|
866
|
+
const workosFgaDoctor = await withDeployEnv(options.workspaceRoot, async () =>
|
|
867
|
+
runWorkOSCommand({
|
|
868
|
+
subcommand: "fga",
|
|
869
|
+
fgaAction: "doctor",
|
|
870
|
+
workspaceRoot: options.workspaceRoot,
|
|
871
|
+
json: true,
|
|
872
|
+
yes: false,
|
|
873
|
+
dryRun: true,
|
|
874
|
+
real: options.production,
|
|
875
|
+
})
|
|
876
|
+
);
|
|
877
|
+
checks.push({
|
|
878
|
+
name: "workos-fga-proof",
|
|
879
|
+
ok: !options.production || workosFgaDoctor.exitCode === 0,
|
|
880
|
+
severity: options.production ? "error" : "warning",
|
|
881
|
+
message: workosFgaDoctor.exitCode === 0
|
|
882
|
+
? "WorkOS FGA resource graph state matches the current app contract and seed"
|
|
883
|
+
: "WorkOS production deploy requires FGA graph sync/proof evidence matching the current app because FGA helpers are enabled",
|
|
884
|
+
command: "forge workos fga prove --real --file workos-seed.yml --json",
|
|
885
|
+
details: workosFgaDoctor.data,
|
|
886
|
+
});
|
|
887
|
+
} else {
|
|
888
|
+
checks.push({
|
|
889
|
+
name: "workos-fga-optional",
|
|
890
|
+
ok: true,
|
|
891
|
+
severity: "warning",
|
|
892
|
+
message: "WorkOS FGA is not enabled; production deploy is gated by AuthKit, RBAC permission claims, Forge policies, tenant proof, and hosted seed evidence",
|
|
893
|
+
command: "forge add auth workos --with-fga",
|
|
894
|
+
});
|
|
895
|
+
}
|
|
879
896
|
}
|
|
880
897
|
const tenantProofRequired = auth.requiresTenant || hasWorkOSIntegration(options.workspaceRoot);
|
|
881
898
|
const tenantProof = tenantProofRequired
|
package/src/forge/cli/main.ts
CHANGED
|
@@ -38,11 +38,12 @@ function formatHelp(): string {
|
|
|
38
38
|
" forge auth check --production --json Fail unless auth is jwt/oidc production-ready",
|
|
39
39
|
" forge authmd check --json Check public/auth.md drift for CI and agent-ready apps",
|
|
40
40
|
" forge workos install --yes --json Delegate AuthKit setup to npx --yes workos@latest install",
|
|
41
|
-
" forge workos doctor --json Check WorkOS AuthKit/
|
|
41
|
+
" forge workos doctor --json Check WorkOS AuthKit/RBAC files, claims, seed, webhook, and tenant guards",
|
|
42
42
|
" forge workos doctor --yes --json Run local checks, then delegate to npx --yes workos@latest doctor",
|
|
43
43
|
" forge workos seed --file workos-seed.yml --dry-run --json Validate WorkOS seed without hosted changes",
|
|
44
|
-
" forge workos prove --file workos-seed.yml --json Prove local WorkOS/AuthKit/
|
|
44
|
+
" forge workos prove --file workos-seed.yml --json Prove local WorkOS/AuthKit/RBAC/seed readiness without dashboard changes",
|
|
45
45
|
" forge workos setup --real --file workos-seed.yml --json Apply redirect/CORS/homepage config and seed WorkOS without dashboard",
|
|
46
|
+
" forge add auth workos --with-fga Add optional WorkOS FGA resource-level authorization helpers",
|
|
46
47
|
" forge workos fga plan --json Derive the WorkOS FGA resource graph and proof scenarios from the app",
|
|
47
48
|
" forge workos fga plan --write --json Write .forge/workos-fga-setup.md for hosted FGA resource type setup",
|
|
48
49
|
" forge workos fga sync --json Record an idempotent local FGA graph state for deploy gates",
|
package/src/forge/cli/parse.ts
CHANGED
|
@@ -822,6 +822,7 @@ function parseAddOptions(
|
|
|
822
822
|
parseOptionValue(args, "--sandbox-backend"),
|
|
823
823
|
),
|
|
824
824
|
allowScripts: parseFlag(args, "--allow-scripts"),
|
|
825
|
+
withFga: parseFlag(args, "--with-fga"),
|
|
825
826
|
mode,
|
|
826
827
|
installWorkspace: parseOptionValue(args, "--workspace"),
|
|
827
828
|
packageTarget: frontend && !backend ? "frontend" : backend ? "backend" : undefined,
|
|
@@ -3117,6 +3118,7 @@ export function hasUnknownOption(argv: string[]): string | null {
|
|
|
3117
3118
|
"--with-livequery",
|
|
3118
3119
|
"--with-react",
|
|
3119
3120
|
"--with-ui",
|
|
3121
|
+
"--with-fga",
|
|
3120
3122
|
"--with-tests",
|
|
3121
3123
|
"--with-create-form",
|
|
3122
3124
|
"--write",
|
package/src/forge/cli/workos.ts
CHANGED
|
@@ -57,6 +57,7 @@ const GENERATED_SEED_FILE = `${GENERATED_DIR}/integrations/workos/workos-seed.ym
|
|
|
57
57
|
const WORKOS_SEED_STATE_FILE = ".workos-seed-state.json";
|
|
58
58
|
const WORKOS_FGA_STATE_FILE = ".workos-fga-state.json";
|
|
59
59
|
const WORKOS_FGA_SETUP_GUIDE_FILE = ".forge/workos-fga-setup.md";
|
|
60
|
+
const WORKOS_FGA_SETUP_PLAN_FILE = ".forge/workos-fga-setup.json";
|
|
60
61
|
|
|
61
62
|
function runExternalCommand(
|
|
62
63
|
command: string[],
|
|
@@ -84,6 +85,12 @@ function exists(root: string, path: string): boolean {
|
|
|
84
85
|
return existsSync(join(root, path));
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
function hasWorkOSFgaArtifacts(root: string): boolean {
|
|
89
|
+
return exists(root, `${GENERATED_DIR}/integrations/workos/fga.ts`) ||
|
|
90
|
+
exists(root, `${GENERATED_DIR}/integrations/workos/resource-map.ts`) ||
|
|
91
|
+
exists(root, WORKOS_FGA_STATE_FILE);
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
function readJson(root: string, path: string): unknown | null {
|
|
88
95
|
const absolute = join(root, path);
|
|
89
96
|
if (!existsSync(absolute)) {
|
|
@@ -287,7 +294,6 @@ export function parseSeedFile(workspaceRoot: string, preferredPath = DEFAULT_SEE
|
|
|
287
294
|
|
|
288
295
|
if (permissions.size === 0) diagnostics.push("no permission slugs found");
|
|
289
296
|
if (roles.size === 0) diagnostics.push("no role slugs found");
|
|
290
|
-
if (resourceTypes.size === 0) diagnostics.push("no resource_types slugs found");
|
|
291
297
|
if (organizations.size === 0) diagnostics.push("no organizations found");
|
|
292
298
|
|
|
293
299
|
return {
|
|
@@ -714,7 +720,16 @@ export interface WorkOSFgaResourceTypeSetup {
|
|
|
714
720
|
}
|
|
715
721
|
|
|
716
722
|
export interface WorkOSFgaSetupGuide {
|
|
723
|
+
schemaVersion: "0.1.0";
|
|
724
|
+
kind: "workos-fga-setup";
|
|
717
725
|
resourceTypes: WorkOSFgaResourceTypeSetup[];
|
|
726
|
+
hostedSetup: WorkOSFgaHostedSetup;
|
|
727
|
+
commands: {
|
|
728
|
+
plan: string;
|
|
729
|
+
syncReal: string;
|
|
730
|
+
proveReal: string;
|
|
731
|
+
deployCheck: string;
|
|
732
|
+
};
|
|
718
733
|
markdown: string;
|
|
719
734
|
docs: string[];
|
|
720
735
|
unsupportedAutomation: string[];
|
|
@@ -1196,7 +1211,16 @@ function workOSFgaSetupGuide(manifest: WorkOSFgaManifest, hostedSetup: WorkOSFga
|
|
|
1196
1211
|
"",
|
|
1197
1212
|
].join("\n");
|
|
1198
1213
|
return {
|
|
1214
|
+
schemaVersion: "0.1.0",
|
|
1215
|
+
kind: "workos-fga-setup",
|
|
1199
1216
|
resourceTypes,
|
|
1217
|
+
hostedSetup,
|
|
1218
|
+
commands: {
|
|
1219
|
+
plan: "forge workos fga plan --file workos-seed.yml --write --json",
|
|
1220
|
+
syncReal: "forge workos fga sync --real --file workos-seed.yml --json",
|
|
1221
|
+
proveReal: "forge workos fga prove --real --file workos-seed.yml --json",
|
|
1222
|
+
deployCheck: "forge deploy check --production --json",
|
|
1223
|
+
},
|
|
1200
1224
|
markdown,
|
|
1201
1225
|
docs: hostedSetup.docs,
|
|
1202
1226
|
unsupportedAutomation: [
|
|
@@ -1214,16 +1238,34 @@ function resolveWorkOSFgaSetupGuidePath(options: WorkOSCommandOptions): string |
|
|
|
1214
1238
|
return candidate && candidate !== "true" && !candidate.startsWith("--") ? candidate : WORKOS_FGA_SETUP_GUIDE_FILE;
|
|
1215
1239
|
}
|
|
1216
1240
|
|
|
1241
|
+
function workOSFgaSetupPlanPath(markdownPath: string): string {
|
|
1242
|
+
if (markdownPath === WORKOS_FGA_SETUP_GUIDE_FILE) {
|
|
1243
|
+
return WORKOS_FGA_SETUP_PLAN_FILE;
|
|
1244
|
+
}
|
|
1245
|
+
return markdownPath.endsWith(".md")
|
|
1246
|
+
? `${markdownPath.slice(0, -3)}.json`
|
|
1247
|
+
: `${markdownPath}.json`;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1217
1250
|
function writeWorkOSFgaSetupGuide(
|
|
1218
1251
|
workspaceRoot: string,
|
|
1219
1252
|
guide: WorkOSFgaSetupGuide,
|
|
1220
1253
|
preferredPath = WORKOS_FGA_SETUP_GUIDE_FILE,
|
|
1221
|
-
): string {
|
|
1254
|
+
): { markdownPath: string; jsonPath: string } {
|
|
1222
1255
|
const relativePath = preferredPath.startsWith("/") ? preferredPath.slice(1) : preferredPath;
|
|
1223
1256
|
const absolutePath = join(workspaceRoot, relativePath);
|
|
1224
1257
|
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
1225
1258
|
writeFileSync(absolutePath, guide.markdown, "utf8");
|
|
1226
|
-
|
|
1259
|
+
const jsonPath = workOSFgaSetupPlanPath(relativePath);
|
|
1260
|
+
const absoluteJsonPath = join(workspaceRoot, jsonPath);
|
|
1261
|
+
mkdirSync(dirname(absoluteJsonPath), { recursive: true });
|
|
1262
|
+
const { markdown: _markdown, ...jsonGuide } = guide;
|
|
1263
|
+
writeFileSync(absoluteJsonPath, `${JSON.stringify({
|
|
1264
|
+
...jsonGuide,
|
|
1265
|
+
markdownPath: relativePath,
|
|
1266
|
+
jsonPath,
|
|
1267
|
+
}, null, 2)}\n`, "utf8");
|
|
1268
|
+
return { markdownPath: relativePath, jsonPath };
|
|
1227
1269
|
}
|
|
1228
1270
|
|
|
1229
1271
|
function fgaData(input: {
|
|
@@ -1240,6 +1282,7 @@ function fgaData(input: {
|
|
|
1240
1282
|
nextCommand?: string;
|
|
1241
1283
|
nextActions?: string[];
|
|
1242
1284
|
setupGuidePath?: string;
|
|
1285
|
+
setupGuideJsonPath?: string;
|
|
1243
1286
|
}): Record<string, unknown> {
|
|
1244
1287
|
const hostedSetup = workOSFgaHostedSetup(input.manifest, input.workosSdk);
|
|
1245
1288
|
const setupGuide = workOSFgaSetupGuide(input.manifest, hostedSetup);
|
|
@@ -1259,6 +1302,7 @@ function fgaData(input: {
|
|
|
1259
1302
|
...(input.workosSdk ? { workosSdk: input.workosSdk } : {}),
|
|
1260
1303
|
...(input.stateFile ? { stateFile: input.stateFile } : {}),
|
|
1261
1304
|
...(input.setupGuidePath ? { setupGuidePath: input.setupGuidePath } : {}),
|
|
1305
|
+
...(input.setupGuideJsonPath ? { setupGuideJsonPath: input.setupGuideJsonPath } : {}),
|
|
1262
1306
|
nextCommand: input.nextCommand,
|
|
1263
1307
|
nextActions: input.nextActions ?? hostedSetup.nextActions,
|
|
1264
1308
|
notes: [
|
|
@@ -1703,6 +1747,7 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
|
|
|
1703
1747
|
const authRoutes = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/auth-routes.ts`);
|
|
1704
1748
|
const fga = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/fga.ts`);
|
|
1705
1749
|
const resourceMap = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/resource-map.ts`);
|
|
1750
|
+
const fgaEnabled = hasWorkOSFgaArtifacts(workspaceRoot);
|
|
1706
1751
|
const httpHandler = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/http-handler.ts`);
|
|
1707
1752
|
const policies = readText(workspaceRoot, "src/policies.workos.ts");
|
|
1708
1753
|
const session = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/session.ts`);
|
|
@@ -1803,7 +1848,7 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
|
|
|
1803
1848
|
name: "seed-file",
|
|
1804
1849
|
ok: seed.exists,
|
|
1805
1850
|
detail: seed.exists
|
|
1806
|
-
? `${seed.path} exists with ${seed.permissions.length} permission(s), ${seed.roles.length} role(s), ${seed.resourceTypes.length} resource type(s)`
|
|
1851
|
+
? `${seed.path} exists with ${seed.permissions.length} permission(s), ${seed.roles.length} role(s), ${seed.resourceTypes.length} optional FGA resource type(s)`
|
|
1807
1852
|
: `${DEFAULT_SEED_FILE} or ${GENERATED_SEED_FILE} is required`,
|
|
1808
1853
|
},
|
|
1809
1854
|
{
|
|
@@ -1832,11 +1877,13 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
|
|
|
1832
1877
|
},
|
|
1833
1878
|
{
|
|
1834
1879
|
name: "seed-resource-types",
|
|
1835
|
-
ok: seed.resourceTypes.length > 0 &&
|
|
1836
|
-
(expectedResourceTypes.length === 0 || missingSeedResources.length === 0),
|
|
1837
|
-
detail:
|
|
1838
|
-
?
|
|
1839
|
-
:
|
|
1880
|
+
ok: !fgaEnabled || (seed.resourceTypes.length > 0 &&
|
|
1881
|
+
(expectedResourceTypes.length === 0 || missingSeedResources.length === 0)),
|
|
1882
|
+
detail: !fgaEnabled
|
|
1883
|
+
? "FGA is not enabled; resource_types are optional for AuthKit/RBAC apps"
|
|
1884
|
+
: missingSeedResources.length === 0
|
|
1885
|
+
? `seed resource_types cover app graph: ${expectedResourceTypes.length > 0 ? expectedResourceTypes.join(", ") : seed.resourceTypes.join(", ")}`
|
|
1886
|
+
: `seed missing resource_type(s) for app graph: ${missingSeedResources.join(", ")}`,
|
|
1840
1887
|
},
|
|
1841
1888
|
{
|
|
1842
1889
|
name: "seed-auth-config",
|
|
@@ -1858,21 +1905,25 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
|
|
|
1858
1905
|
},
|
|
1859
1906
|
{
|
|
1860
1907
|
name: "fga-plan",
|
|
1861
|
-
ok: fgaManifest.diagnostics.length === 0,
|
|
1862
|
-
detail:
|
|
1863
|
-
?
|
|
1864
|
-
:
|
|
1908
|
+
ok: !fgaEnabled || fgaManifest.diagnostics.length === 0,
|
|
1909
|
+
detail: !fgaEnabled
|
|
1910
|
+
? "FGA is not enabled; run forge add auth workos --with-fga for resource-level WorkOS Authorization"
|
|
1911
|
+
: fgaManifest.diagnostics.length === 0
|
|
1912
|
+
? `FGA plan covers ${fgaManifest.resourceTypes.length} resource type(s), ${fgaManifest.resources.length} resource(s), and ${fgaManifest.proofScenarios.length} proof scenario(s)`
|
|
1913
|
+
: `FGA plan has gap(s): ${fgaManifest.diagnostics.join("; ")}`,
|
|
1865
1914
|
},
|
|
1866
1915
|
{
|
|
1867
1916
|
name: "fga-state",
|
|
1868
1917
|
ok: true,
|
|
1869
|
-
detail: !
|
|
1870
|
-
?
|
|
1871
|
-
: fgaState.
|
|
1872
|
-
? `${WORKOS_FGA_STATE_FILE}
|
|
1873
|
-
: fgaState.
|
|
1874
|
-
? `${WORKOS_FGA_STATE_FILE}
|
|
1875
|
-
:
|
|
1918
|
+
detail: !fgaEnabled
|
|
1919
|
+
? "FGA is not enabled; no FGA state file is required"
|
|
1920
|
+
: !fgaState.exists
|
|
1921
|
+
? `${WORKOS_FGA_STATE_FILE} not found; run forge workos fga sync --json before production deploy`
|
|
1922
|
+
: fgaState.matchesManifestHash
|
|
1923
|
+
? `${WORKOS_FGA_STATE_FILE} matches current FGA manifest${fgaState.mode === "real" ? " in real mode" : ""}`
|
|
1924
|
+
: fgaState.valid
|
|
1925
|
+
? `${WORKOS_FGA_STATE_FILE} exists but does not match current FGA manifest; rerun forge workos fga sync --json`
|
|
1926
|
+
: `${WORKOS_FGA_STATE_FILE} exists but is invalid: ${fgaState.diagnostics.join("; ")}`,
|
|
1876
1927
|
},
|
|
1877
1928
|
{
|
|
1878
1929
|
name: "authkit-routes",
|
|
@@ -1896,7 +1947,7 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
|
|
|
1896
1947
|
},
|
|
1897
1948
|
{
|
|
1898
1949
|
name: "fga-bridge",
|
|
1899
|
-
ok: includesAll(resourceMap, [
|
|
1950
|
+
ok: !fgaEnabled || (includesAll(resourceMap, [
|
|
1900
1951
|
"canWorkOS",
|
|
1901
1952
|
"assertWorkOSResourceTenant",
|
|
1902
1953
|
"FORGE_WORKOS_CROSS_TENANT_RESOURCE",
|
|
@@ -1906,8 +1957,10 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
|
|
|
1906
1957
|
"permissionSlug",
|
|
1907
1958
|
"resourceExternalId",
|
|
1908
1959
|
]) &&
|
|
1909
|
-
includesAll(fga, ["forgeWorkOSResourceTypes"]),
|
|
1910
|
-
detail:
|
|
1960
|
+
includesAll(fga, ["forgeWorkOSResourceTypes"])),
|
|
1961
|
+
detail: fgaEnabled
|
|
1962
|
+
? "WorkOS FGA resource-map bridge exists with sync, cache, telemetry, official check shape, and cross-tenant guard"
|
|
1963
|
+
: "FGA bridge is optional and not enabled for this WorkOS AuthKit/RBAC app",
|
|
1911
1964
|
},
|
|
1912
1965
|
{
|
|
1913
1966
|
name: "policies",
|
|
@@ -2237,6 +2290,7 @@ export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSComma
|
|
|
2237
2290
|
const missingSeedPermissions = missingValues(activePermissions, seed.permissions);
|
|
2238
2291
|
const missingSeedResources = missingValues(expectedResourceTypes, seed.resourceTypes);
|
|
2239
2292
|
const unusedSeedPermissions = seed.permissions.filter((permission) => !activePermissions.includes(permission));
|
|
2293
|
+
const fgaEnabled = hasWorkOSFgaArtifacts(options.workspaceRoot);
|
|
2240
2294
|
const checks = [
|
|
2241
2295
|
{
|
|
2242
2296
|
name: "seed-file",
|
|
@@ -2246,7 +2300,9 @@ export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSComma
|
|
|
2246
2300
|
{
|
|
2247
2301
|
name: "seed-yaml-shape",
|
|
2248
2302
|
ok: seed.valid,
|
|
2249
|
-
detail: seed.valid
|
|
2303
|
+
detail: seed.valid
|
|
2304
|
+
? `seed contains permissions, roles, organizations${seed.resourceTypes.length > 0 ? ", and optional FGA resource_types" : ""}`
|
|
2305
|
+
: seed.diagnostics.join("; "),
|
|
2250
2306
|
},
|
|
2251
2307
|
{
|
|
2252
2308
|
name: "seed-policy-coverage",
|
|
@@ -2266,10 +2322,12 @@ export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSComma
|
|
|
2266
2322
|
},
|
|
2267
2323
|
{
|
|
2268
2324
|
name: "seed-resource-coverage",
|
|
2269
|
-
ok: missingSeedResources.length === 0,
|
|
2270
|
-
detail:
|
|
2271
|
-
?
|
|
2272
|
-
:
|
|
2325
|
+
ok: !fgaEnabled || missingSeedResources.length === 0,
|
|
2326
|
+
detail: !fgaEnabled
|
|
2327
|
+
? "FGA is not enabled; seed resource type coverage is not required"
|
|
2328
|
+
: missingSeedResources.length === 0
|
|
2329
|
+
? `seed covers app resource type(s): ${expectedResourceTypes.join(", ") || "none required"}`
|
|
2330
|
+
: `seed missing resource type(s): ${missingSeedResources.join(", ")}`,
|
|
2273
2331
|
},
|
|
2274
2332
|
{
|
|
2275
2333
|
name: "seed-hosted-config",
|
|
@@ -2764,9 +2822,12 @@ export function runWorkOSFgaCommand(options: WorkOSCommandOptions): WorkOSComman
|
|
|
2764
2822
|
const manifest = collectWorkOSFgaManifest(options.workspaceRoot, file);
|
|
2765
2823
|
const initialHostedSetup = workOSFgaHostedSetup(manifest);
|
|
2766
2824
|
const setupGuidePath = resolveWorkOSFgaSetupGuidePath(options);
|
|
2767
|
-
const
|
|
2825
|
+
const writtenSetupGuide = setupGuidePath
|
|
2768
2826
|
? writeWorkOSFgaSetupGuide(options.workspaceRoot, workOSFgaSetupGuide(manifest, initialHostedSetup), setupGuidePath)
|
|
2769
2827
|
: undefined;
|
|
2828
|
+
const writtenSetupGuideData = writtenSetupGuide
|
|
2829
|
+
? { setupGuidePath: writtenSetupGuide.markdownPath, setupGuideJsonPath: writtenSetupGuide.jsonPath }
|
|
2830
|
+
: {};
|
|
2770
2831
|
let state = readWorkOSFgaState(options.workspaceRoot, manifest);
|
|
2771
2832
|
const command = ["forge", "workos", "fga", action, "--file", file];
|
|
2772
2833
|
const real = options.real ?? false;
|
|
@@ -2798,7 +2859,7 @@ export function runWorkOSFgaCommand(options: WorkOSCommandOptions): WorkOSComman
|
|
|
2798
2859
|
state,
|
|
2799
2860
|
seedState,
|
|
2800
2861
|
real,
|
|
2801
|
-
...
|
|
2862
|
+
...writtenSetupGuideData,
|
|
2802
2863
|
nextCommand: `forge workos fga sync --file ${file} --json`,
|
|
2803
2864
|
}),
|
|
2804
2865
|
exitCode: ok ? 0 : 1,
|
|
@@ -2835,7 +2896,7 @@ export function runWorkOSFgaCommand(options: WorkOSCommandOptions): WorkOSComman
|
|
|
2835
2896
|
seedState,
|
|
2836
2897
|
real,
|
|
2837
2898
|
...(cliAuth ? { cliAuth } : {}),
|
|
2838
|
-
...
|
|
2899
|
+
...writtenSetupGuideData,
|
|
2839
2900
|
nextCommand: real ? `forge workos fga sync --real --file ${file} --json` : `forge workos fga plan --file ${file} --json`,
|
|
2840
2901
|
}),
|
|
2841
2902
|
exitCode: 1,
|
|
@@ -2856,7 +2917,7 @@ export function runWorkOSFgaCommand(options: WorkOSCommandOptions): WorkOSComman
|
|
|
2856
2917
|
seedState,
|
|
2857
2918
|
real,
|
|
2858
2919
|
...(cliAuth ? { cliAuth } : {}),
|
|
2859
|
-
...
|
|
2920
|
+
...writtenSetupGuideData,
|
|
2860
2921
|
nextCommand: real ? `forge workos fga sync --real --file ${file} --json` : `forge workos fga sync --file ${file} --json`,
|
|
2861
2922
|
}),
|
|
2862
2923
|
exitCode: 0,
|
|
@@ -2886,7 +2947,7 @@ export function runWorkOSFgaCommand(options: WorkOSCommandOptions): WorkOSComman
|
|
|
2886
2947
|
real,
|
|
2887
2948
|
...(cliAuth ? { cliAuth } : {}),
|
|
2888
2949
|
workosSdk: sdk.data,
|
|
2889
|
-
...
|
|
2950
|
+
...writtenSetupGuideData,
|
|
2890
2951
|
nextCommand: `forge workos fga sync --real --file ${file} --json`,
|
|
2891
2952
|
nextActions: workOSFgaHostedSetup(manifest, sdk.data).nextActions,
|
|
2892
2953
|
}),
|
|
@@ -2919,7 +2980,7 @@ export function runWorkOSFgaCommand(options: WorkOSCommandOptions): WorkOSComman
|
|
|
2919
2980
|
...(cliAuth ? { cliAuth } : {}),
|
|
2920
2981
|
...(sdk ? { workosSdk: sdk.data } : {}),
|
|
2921
2982
|
stateFile,
|
|
2922
|
-
...
|
|
2983
|
+
...writtenSetupGuideData,
|
|
2923
2984
|
nextCommand: real ? `forge workos fga prove --real --file ${file} --json` : `forge workos fga prove --file ${file} --json`,
|
|
2924
2985
|
}),
|
|
2925
2986
|
exitCode: 0,
|
|
@@ -2992,7 +3053,7 @@ export function runWorkOSFgaCommand(options: WorkOSCommandOptions): WorkOSComman
|
|
|
2992
3053
|
...(cliAuth ? { cliAuth } : {}),
|
|
2993
3054
|
...(sdk ? { workosSdk: sdk.data } : {}),
|
|
2994
3055
|
...(stateFile ? { stateFile } : {}),
|
|
2995
|
-
...
|
|
3056
|
+
...writtenSetupGuideData,
|
|
2996
3057
|
nextCommand: ok ? "forge deploy check --production --json" : `forge workos fga sync${real ? " --real" : ""} --file ${file} --json`,
|
|
2997
3058
|
}),
|
|
2998
3059
|
stdout: sdk?.stdout,
|
|
@@ -3032,7 +3093,7 @@ export function runWorkOSFgaCommand(options: WorkOSCommandOptions): WorkOSComman
|
|
|
3032
3093
|
seedState,
|
|
3033
3094
|
readiness,
|
|
3034
3095
|
real,
|
|
3035
|
-
...
|
|
3096
|
+
...writtenSetupGuideData,
|
|
3036
3097
|
nextCommand: readiness.nextCommand,
|
|
3037
3098
|
nextActions: readiness.nextActions,
|
|
3038
3099
|
}),
|
|
@@ -3159,11 +3220,14 @@ export function formatWorkOSHuman(result: WorkOSCommandResult): string {
|
|
|
3159
3220
|
}
|
|
3160
3221
|
if (result.kind === "workos-fga") {
|
|
3161
3222
|
const data = result.data && typeof result.data === "object"
|
|
3162
|
-
? result.data as { action?: string; real?: boolean; nextCommand?: string; stateFile?: string; setupGuidePath?: string; nextActions?: string[] }
|
|
3223
|
+
? result.data as { action?: string; real?: boolean; nextCommand?: string; stateFile?: string; setupGuidePath?: string; setupGuideJsonPath?: string; nextActions?: string[] }
|
|
3163
3224
|
: {};
|
|
3164
3225
|
if (data.setupGuidePath) {
|
|
3165
3226
|
lines.push(`FGA setup guide: ${data.setupGuidePath}`);
|
|
3166
3227
|
}
|
|
3228
|
+
if (data.setupGuideJsonPath) {
|
|
3229
|
+
lines.push(`FGA setup JSON: ${data.setupGuideJsonPath}`);
|
|
3230
|
+
}
|
|
3167
3231
|
if (result.ok && data.action === "plan") {
|
|
3168
3232
|
lines.push(`WorkOS FGA plan passed; run ${data.nextCommand ?? "forge workos fga sync --json"}.`);
|
|
3169
3233
|
} else if (result.ok && data.action === "sync") {
|
|
@@ -3,6 +3,7 @@ import { nodeFileSystem } from "../fs/index.ts";
|
|
|
3
3
|
import type { AddOptions } from "../types/cli.ts";
|
|
4
4
|
import type { Diagnostic } from "../types/diagnostic.ts";
|
|
5
5
|
import type { Dependency } from "../types/package-graph.ts";
|
|
6
|
+
import type { IntegrationRecipe } from "../types/integration.ts";
|
|
6
7
|
import type { ClassifiedPackage } from "../classifier/runtime-matrix.ts";
|
|
7
8
|
import { buildAppGraph } from "../app-graph/build.ts";
|
|
8
9
|
import { classify } from "../classifier/classify.ts";
|
|
@@ -79,7 +80,7 @@ export interface ForgeAddResult {
|
|
|
79
80
|
failureKind?: string;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
function recipeResultMetadata(recipe:
|
|
83
|
+
function recipeResultMetadata(recipe: IntegrationRecipe): Pick<
|
|
83
84
|
ForgeAddResult,
|
|
84
85
|
"recipeVersion" | "recipePackages" | "requiredSecrets" | "optionalSecrets"
|
|
85
86
|
> {
|
|
@@ -91,6 +92,24 @@ function recipeResultMetadata(recipe: NonNullable<ReturnType<typeof resolveRecip
|
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
|
|
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
|
+
|
|
94
113
|
function addExplanation(result: ForgeAddResult): string {
|
|
95
114
|
if (result.mode === "package") {
|
|
96
115
|
const location = result.target && result.target !== "root" ? `${result.target}/package.json` : "package.json";
|
|
@@ -726,7 +745,7 @@ function connectWorkOSReactRoot(workspaceRoot: string, frontendWorkspace: string
|
|
|
726
745
|
}
|
|
727
746
|
|
|
728
747
|
async function analyzeRecipePackages(
|
|
729
|
-
recipe:
|
|
748
|
+
recipe: IntegrationRecipe,
|
|
730
749
|
ctx: ReturnType<typeof discover>,
|
|
731
750
|
installRoot: string,
|
|
732
751
|
options: ForgeAddOptions,
|
|
@@ -783,7 +802,7 @@ async function analyzeRecipePackages(
|
|
|
783
802
|
|
|
784
803
|
async function buildAddPlan(
|
|
785
804
|
alias: string,
|
|
786
|
-
recipe:
|
|
805
|
+
recipe: IntegrationRecipe,
|
|
787
806
|
ctx: ReturnType<typeof discover>,
|
|
788
807
|
installRoot: string,
|
|
789
808
|
options: ForgeAddOptions,
|
|
@@ -945,6 +964,8 @@ export async function forgeAdd(
|
|
|
945
964
|
});
|
|
946
965
|
}
|
|
947
966
|
|
|
967
|
+
const effectiveRecipe = applyWorkOSRecipeProfile(recipe, effectiveOptions);
|
|
968
|
+
|
|
948
969
|
const pm =
|
|
949
970
|
options.pmAdapter ??
|
|
950
971
|
detectAndCreatePackageManagerAdapter(options.workspaceRoot);
|
|
@@ -955,7 +976,7 @@ export async function forgeAdd(
|
|
|
955
976
|
|
|
956
977
|
try {
|
|
957
978
|
const dryRun = await pm.dryRunAddWithPath(
|
|
958
|
-
|
|
979
|
+
effectiveRecipe.packages.map((pkg) => pkg.packageName).join(" "),
|
|
959
980
|
{
|
|
960
981
|
cwd: options.workspaceRoot,
|
|
961
982
|
ignoreScripts: !options.allowScripts,
|
|
@@ -966,7 +987,7 @@ export async function forgeAdd(
|
|
|
966
987
|
const fallback = dryRunRecipeFallbackMessage(normalized);
|
|
967
988
|
const { emitPlan, warnings, errors } = await buildAddPlan(
|
|
968
989
|
normalized,
|
|
969
|
-
|
|
990
|
+
effectiveRecipe,
|
|
970
991
|
ctx,
|
|
971
992
|
installRoot,
|
|
972
993
|
effectiveOptions,
|
|
@@ -982,7 +1003,7 @@ export async function forgeAdd(
|
|
|
982
1003
|
return finalizeAddResult({
|
|
983
1004
|
alias: normalized,
|
|
984
1005
|
mode: "integration",
|
|
985
|
-
...recipeResultMetadata(
|
|
1006
|
+
...recipeResultMetadata(effectiveRecipe),
|
|
986
1007
|
changed: [...emitPlan.files.map((file) => file.path), "forge.lock"],
|
|
987
1008
|
unchanged: [],
|
|
988
1009
|
warnings,
|
|
@@ -994,7 +1015,7 @@ export async function forgeAdd(
|
|
|
994
1015
|
|
|
995
1016
|
const { emitPlan, warnings, errors } = await buildAddPlan(
|
|
996
1017
|
normalized,
|
|
997
|
-
|
|
1018
|
+
effectiveRecipe,
|
|
998
1019
|
ctx,
|
|
999
1020
|
installRoot,
|
|
1000
1021
|
effectiveOptions,
|
|
@@ -1003,7 +1024,7 @@ export async function forgeAdd(
|
|
|
1003
1024
|
return finalizeAddResult({
|
|
1004
1025
|
alias: normalized,
|
|
1005
1026
|
mode: "integration",
|
|
1006
|
-
...recipeResultMetadata(
|
|
1027
|
+
...recipeResultMetadata(effectiveRecipe),
|
|
1007
1028
|
changed: [...emitPlan.files.map((file) => file.path), "forge.lock"],
|
|
1008
1029
|
unchanged: [],
|
|
1009
1030
|
warnings,
|
|
@@ -1023,7 +1044,7 @@ export async function forgeAdd(
|
|
|
1023
1044
|
frontendWorkspace ? [packageJsonRelativeFor(frontendWorkspace)] : [],
|
|
1024
1045
|
);
|
|
1025
1046
|
const preinstalledWarnings: Diagnostic[] = [];
|
|
1026
|
-
for (const pkg of
|
|
1047
|
+
for (const pkg of effectiveRecipe.packages) {
|
|
1027
1048
|
const rootPackageJson = join(options.workspaceRoot, "package.json");
|
|
1028
1049
|
await addPackageOrContinueIfInstalled({
|
|
1029
1050
|
pm,
|
|
@@ -1055,7 +1076,7 @@ export async function forgeAdd(
|
|
|
1055
1076
|
const ctx = discover({ workspaceRoot: options.workspaceRoot });
|
|
1056
1077
|
const { emitPlan, warnings, errors: analyzeErrors } = await buildAddPlan(
|
|
1057
1078
|
normalized,
|
|
1058
|
-
|
|
1079
|
+
effectiveRecipe,
|
|
1059
1080
|
ctx,
|
|
1060
1081
|
options.workspaceRoot,
|
|
1061
1082
|
options,
|
|
@@ -1066,7 +1087,7 @@ export async function forgeAdd(
|
|
|
1066
1087
|
return finalizeAddResult({
|
|
1067
1088
|
alias: normalized,
|
|
1068
1089
|
mode: "integration",
|
|
1069
|
-
...recipeResultMetadata(
|
|
1090
|
+
...recipeResultMetadata(effectiveRecipe),
|
|
1070
1091
|
changed: [],
|
|
1071
1092
|
unchanged: [],
|
|
1072
1093
|
warnings,
|
|
@@ -1093,7 +1114,7 @@ export async function forgeAdd(
|
|
|
1093
1114
|
return finalizeAddResult({
|
|
1094
1115
|
alias: normalized,
|
|
1095
1116
|
mode: "integration",
|
|
1096
|
-
...recipeResultMetadata(
|
|
1117
|
+
...recipeResultMetadata(effectiveRecipe),
|
|
1097
1118
|
changed: [],
|
|
1098
1119
|
unchanged: [],
|
|
1099
1120
|
warnings: warningsCombined,
|
|
@@ -1112,7 +1133,7 @@ export async function forgeAdd(
|
|
|
1112
1133
|
return finalizeAddResult({
|
|
1113
1134
|
alias: normalized,
|
|
1114
1135
|
mode: "integration",
|
|
1115
|
-
...recipeResultMetadata(
|
|
1136
|
+
...recipeResultMetadata(effectiveRecipe),
|
|
1116
1137
|
changed: [],
|
|
1117
1138
|
unchanged: [],
|
|
1118
1139
|
warnings: warningsCombined,
|
|
@@ -1148,7 +1169,7 @@ export async function forgeAdd(
|
|
|
1148
1169
|
return finalizeAddResult({
|
|
1149
1170
|
alias: normalized,
|
|
1150
1171
|
mode: "integration",
|
|
1151
|
-
...recipeResultMetadata(
|
|
1172
|
+
...recipeResultMetadata(effectiveRecipe),
|
|
1152
1173
|
changed: [
|
|
1153
1174
|
...changedPackageManagerFiles(options.workspaceRoot, packageManagerBefore),
|
|
1154
1175
|
...emitResult.changed,
|
|
@@ -1178,7 +1199,7 @@ export async function forgeAdd(
|
|
|
1178
1199
|
return finalizeAddResult({
|
|
1179
1200
|
alias: normalized,
|
|
1180
1201
|
mode: "integration",
|
|
1181
|
-
...recipeResultMetadata(
|
|
1202
|
+
...recipeResultMetadata(effectiveRecipe),
|
|
1182
1203
|
changed: [],
|
|
1183
1204
|
unchanged: [],
|
|
1184
1205
|
warnings: [],
|
|
@@ -125,6 +125,13 @@ function rolePermissions(permissions: string[]): Record<string, string[]> {
|
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function workosFgaEnabled(input: IntegrationTemplateInput): boolean {
|
|
129
|
+
return Boolean(
|
|
130
|
+
input.recipe.integrations?.includes("workos/fga.ts") ||
|
|
131
|
+
input.recipe.integrations?.includes("workos/resource-map.ts"),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
export function renderWorkosServerAdapter(_input: IntegrationTemplateInput): string {
|
|
129
136
|
return [
|
|
130
137
|
"/** Forge generated WorkOS server adapter — action/workflow/endpoint/server contexts only. */",
|
|
@@ -1059,7 +1066,18 @@ export function renderWorkosSeed(_input: IntegrationTemplateInput): string {
|
|
|
1059
1066
|
export function renderWorkosSeedYaml(input: IntegrationTemplateInput): string {
|
|
1060
1067
|
const permissions = permissionsFromApp(input);
|
|
1061
1068
|
const roles = rolePermissions(permissions);
|
|
1062
|
-
const resourceTypes = resourceTypesFromApp(input);
|
|
1069
|
+
const resourceTypes = workosFgaEnabled(input) ? resourceTypesFromApp(input) : [];
|
|
1070
|
+
const resourceTypeSection = resourceTypes.length > 0
|
|
1071
|
+
? [
|
|
1072
|
+
"",
|
|
1073
|
+
"resource_types:",
|
|
1074
|
+
...resourceTypes.flatMap((resource) => [
|
|
1075
|
+
` - slug: '${resource.slug}'`,
|
|
1076
|
+
` name: '${resource.name}'`,
|
|
1077
|
+
...(resource.parent ? [` parent: '${resource.parent}'`] : []),
|
|
1078
|
+
]),
|
|
1079
|
+
]
|
|
1080
|
+
: [];
|
|
1063
1081
|
return [
|
|
1064
1082
|
"# Forge generated WorkOS seed file.",
|
|
1065
1083
|
"# Review names, domains, redirect URIs, and origins before applying to a real WorkOS environment.",
|
|
@@ -1068,13 +1086,7 @@ export function renderWorkosSeedYaml(input: IntegrationTemplateInput): string {
|
|
|
1068
1086
|
` - name: '${toTitle(permission)}'`,
|
|
1069
1087
|
` slug: '${permission}'`,
|
|
1070
1088
|
]),
|
|
1071
|
-
|
|
1072
|
-
"resource_types:",
|
|
1073
|
-
...resourceTypes.flatMap((resource) => [
|
|
1074
|
-
` - slug: '${resource.slug}'`,
|
|
1075
|
-
` name: '${resource.name}'`,
|
|
1076
|
-
...(resource.parent ? [` parent: '${resource.parent}'`] : []),
|
|
1077
|
-
]),
|
|
1089
|
+
...resourceTypeSection,
|
|
1078
1090
|
"",
|
|
1079
1091
|
"roles:",
|
|
1080
1092
|
...Object.entries(roles).flatMap(([role, rolePermissions]) => [
|
|
@@ -1210,6 +1222,34 @@ export function renderWorkosTestkit(input: IntegrationTemplateInput): string {
|
|
|
1210
1222
|
}
|
|
1211
1223
|
|
|
1212
1224
|
export function renderWorkosDoc(input: IntegrationTemplateInput): string {
|
|
1225
|
+
const fgaEnabled = workosFgaEnabled(input);
|
|
1226
|
+
const fgaCommandLines = fgaEnabled
|
|
1227
|
+
? [
|
|
1228
|
+
"forge workos fga plan --file workos-seed.yml --write --json",
|
|
1229
|
+
"forge workos fga sync --file workos-seed.yml --json",
|
|
1230
|
+
"forge workos fga prove --file workos-seed.yml --json",
|
|
1231
|
+
]
|
|
1232
|
+
: [
|
|
1233
|
+
"# Optional when resource-level WorkOS Authorization is required:",
|
|
1234
|
+
"forge add auth workos --with-fga",
|
|
1235
|
+
];
|
|
1236
|
+
const fgaSection = fgaEnabled
|
|
1237
|
+
? [
|
|
1238
|
+
"## FGA strategy",
|
|
1239
|
+
"",
|
|
1240
|
+
"Use JWT permissions for organization-wide checks. Use the WorkOS Authorization API for resource-level checks such as project, workspace, or task access. Keep those API calls in actions, workflows, endpoints, or server code; commands and queries should consume validated auth/policy context rather than importing the WorkOS SDK.",
|
|
1241
|
+
"",
|
|
1242
|
+
"Use `syncWorkOSResourceGraph(...)` after app resources are created or renamed to mirror the Forge resource graph into WorkOS FGA resources. Use `canWorkOS(...)` with `ForgeWorkOSFgaDecisionCache` for resource-level checks; it calls WorkOS `authorization.check` with `organizationMembershipId`, `permissionSlug`, `resourceTypeSlug`, and `resourceExternalId`, emits optional telemetry, and denies by default on provider errors unless `fallback: \"throw\"` is set.",
|
|
1243
|
+
"",
|
|
1244
|
+
"Use `assertWorkOSResourceTenant(...)` before resource-level FGA checks whenever the resource came from user input or an external id. This keeps Acme resources from being checked under Globex membership context.",
|
|
1245
|
+
]
|
|
1246
|
+
: [
|
|
1247
|
+
"## Authorization strategy",
|
|
1248
|
+
"",
|
|
1249
|
+
"The default WorkOS recipe uses AuthKit, organization claims, roles, permissions, Forge policies, and tenant isolation. That is enough for most SaaS apps.",
|
|
1250
|
+
"",
|
|
1251
|
+
"FGA is optional. Add it only when your app needs resource-level authorization beyond organization roles and permission claims, for example per-project, per-document, or inherited resource access.",
|
|
1252
|
+
];
|
|
1213
1253
|
return [
|
|
1214
1254
|
"# WorkOS integration",
|
|
1215
1255
|
"",
|
|
@@ -1222,7 +1262,8 @@ export function renderWorkosDoc(input: IntegrationTemplateInput): string {
|
|
|
1222
1262
|
"- Installs `@workos-inc/node` for server/action/workflow/endpoint code.",
|
|
1223
1263
|
"- Installs `@workos-inc/authkit-react` and generates a Vite React bridge when a `web/package.json` workspace exists.",
|
|
1224
1264
|
"- Registers WorkOS secret names without storing values.",
|
|
1225
|
-
"- Generates an AuthKit/
|
|
1265
|
+
"- Generates an AuthKit/RBAC checklist and a `workos-seed.yml` for roles, permissions, organizations, redirect URIs, and CORS origins.",
|
|
1266
|
+
...(fgaEnabled ? ["- Generates optional WorkOS FGA resource graph helpers and resource type seed metadata."] : []),
|
|
1226
1267
|
"- Generates framework-agnostic AuthKit route/session helpers for `/login`, `/callback`, `/logout`, and `/session`.",
|
|
1227
1268
|
"- Generates a framework-agnostic `Request -> Response` webhook handler for `POST /webhooks/workos`.",
|
|
1228
1269
|
"- Marks WorkOS SDK imports as forbidden from commands, queries, liveQueries, client, shared, and edge code.",
|
|
@@ -1238,6 +1279,7 @@ export function renderWorkosDoc(input: IntegrationTemplateInput): string {
|
|
|
1238
1279
|
"forge workos seed --file workos-seed.yml --json",
|
|
1239
1280
|
"forge workos prove --file workos-seed.yml --json",
|
|
1240
1281
|
"forge workos setup --real --file workos-seed.yml --json",
|
|
1282
|
+
...fgaCommandLines,
|
|
1241
1283
|
"forge auth check --json",
|
|
1242
1284
|
"forge auth prove --json",
|
|
1243
1285
|
"forge check --json",
|
|
@@ -1270,13 +1312,7 @@ export function renderWorkosDoc(input: IntegrationTemplateInput): string {
|
|
|
1270
1312
|
"",
|
|
1271
1313
|
"Mount `handleWorkOSAuthRequest(request, { workos, config, resolveOrganization })` for `/login`, `/callback`, `/logout`, and `/session`. The callback exchanges the AuthKit authorization code, signs an HttpOnly session cookie, and exposes Forge-normalized claims through `/session`.",
|
|
1272
1314
|
"",
|
|
1273
|
-
|
|
1274
|
-
"",
|
|
1275
|
-
"Use JWT permissions for organization-wide checks. Use the WorkOS Authorization API for resource-level checks such as project, workspace, or task access. Keep those API calls in actions, workflows, endpoints, or server code; commands and queries should consume validated auth/policy context rather than importing the WorkOS SDK.",
|
|
1276
|
-
"",
|
|
1277
|
-
"Use `syncWorkOSResourceGraph(...)` after app resources are created or renamed to mirror the Forge resource graph into WorkOS FGA resources. Use `canWorkOS(...)` with `ForgeWorkOSFgaDecisionCache` for resource-level checks; it calls WorkOS `authorization.check` with `organizationMembershipId`, `permissionSlug`, `resourceTypeSlug`, and `resourceExternalId`, emits optional telemetry, and denies by default on provider errors unless `fallback: \"throw\"` is set.",
|
|
1278
|
-
"",
|
|
1279
|
-
"Use `assertWorkOSResourceTenant(...)` before resource-level FGA checks whenever the resource came from user input or an external id. This keeps Acme resources from being checked under Globex membership context.",
|
|
1315
|
+
...fgaSection,
|
|
1280
1316
|
"",
|
|
1281
1317
|
"## Webhook route",
|
|
1282
1318
|
"",
|
|
@@ -63,6 +63,7 @@ export interface AddOptions extends CliCommonOptions {
|
|
|
63
63
|
runtimeInspect: boolean;
|
|
64
64
|
sandboxBackend: SandboxBackend;
|
|
65
65
|
allowScripts: boolean;
|
|
66
|
+
withFga?: boolean;
|
|
66
67
|
mode?: "auto" | "integration" | "package";
|
|
67
68
|
installWorkspace?: string;
|
|
68
69
|
packageTarget?: "frontend" | "backend";
|
package/src/forge/version.ts
CHANGED