forgeos 0.1.0-alpha.54 → 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 CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.54 input=7577a1a1a717ae3776c6725b005a98a36c149041f62ef4e28e098baad0bd1fcc content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
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,23 @@
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
+
3
21
  ## 0.1.0-alpha.54
4
22
 
5
23
  ### Patch Changes
package/docs/changelog.md CHANGED
@@ -6,6 +6,19 @@ 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
+
9
22
  ## 0.1.0-alpha.54
10
23
 
11
24
  - WorkOS FGA setup writes now produce a paired machine-readable
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.54",
3
+ "version": "0.1.0-alpha.55",
4
4
  "description": "Agent-native application framework and compiler for building Forge apps without a mandatory dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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.54","releaseId":"forgeos@0.1.0-alpha.54+unknown","schemaVersion":"0.1.0"}
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.54 input=7577a1a1a717ae3776c6725b005a98a36c149041f62ef4e28e098baad0bd1fcc content=7078de03fb1c3ae829b803fdfd1e9320fa10b8c128e51d374d8afab09cdc3df5
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.54",
23
- "releaseId": "forgeos@0.1.0-alpha.54+unknown",
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;
@@ -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
- ? "WorkOS adapter files, seed, claims, policies, and FGA bridge are app-aware"
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
- const workosFgaDoctor = await withDeployEnv(options.workspaceRoot, async () =>
859
- runWorkOSCommand({
860
- subcommand: "fga",
861
- fgaAction: "doctor",
862
- workspaceRoot: options.workspaceRoot,
863
- json: true,
864
- yes: false,
865
- dryRun: true,
866
- real: options.production,
867
- })
868
- );
869
- checks.push({
870
- name: "workos-fga-proof",
871
- ok: !options.production || workosFgaDoctor.exitCode === 0,
872
- severity: options.production ? "error" : "warning",
873
- message: workosFgaDoctor.exitCode === 0
874
- ? "WorkOS FGA resource graph state matches the current app contract and seed"
875
- : "WorkOS production deploy requires FGA graph sync/proof evidence matching the current app",
876
- command: "forge workos fga prove --real --file workos-seed.yml --json",
877
- details: workosFgaDoctor.data,
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
@@ -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/FGA files, claims, seed, webhook, and tenant guards",
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/FGA/seed readiness without dashboard changes",
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",
@@ -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",
@@ -85,6 +85,12 @@ function exists(root: string, path: string): boolean {
85
85
  return existsSync(join(root, path));
86
86
  }
87
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
+
88
94
  function readJson(root: string, path: string): unknown | null {
89
95
  const absolute = join(root, path);
90
96
  if (!existsSync(absolute)) {
@@ -288,7 +294,6 @@ export function parseSeedFile(workspaceRoot: string, preferredPath = DEFAULT_SEE
288
294
 
289
295
  if (permissions.size === 0) diagnostics.push("no permission slugs found");
290
296
  if (roles.size === 0) diagnostics.push("no role slugs found");
291
- if (resourceTypes.size === 0) diagnostics.push("no resource_types slugs found");
292
297
  if (organizations.size === 0) diagnostics.push("no organizations found");
293
298
 
294
299
  return {
@@ -1742,6 +1747,7 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
1742
1747
  const authRoutes = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/auth-routes.ts`);
1743
1748
  const fga = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/fga.ts`);
1744
1749
  const resourceMap = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/resource-map.ts`);
1750
+ const fgaEnabled = hasWorkOSFgaArtifacts(workspaceRoot);
1745
1751
  const httpHandler = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/http-handler.ts`);
1746
1752
  const policies = readText(workspaceRoot, "src/policies.workos.ts");
1747
1753
  const session = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/session.ts`);
@@ -1842,7 +1848,7 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
1842
1848
  name: "seed-file",
1843
1849
  ok: seed.exists,
1844
1850
  detail: seed.exists
1845
- ? `${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)`
1846
1852
  : `${DEFAULT_SEED_FILE} or ${GENERATED_SEED_FILE} is required`,
1847
1853
  },
1848
1854
  {
@@ -1871,11 +1877,13 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
1871
1877
  },
1872
1878
  {
1873
1879
  name: "seed-resource-types",
1874
- ok: seed.resourceTypes.length > 0 &&
1875
- (expectedResourceTypes.length === 0 || missingSeedResources.length === 0),
1876
- detail: missingSeedResources.length === 0
1877
- ? `seed resource_types cover app graph: ${expectedResourceTypes.length > 0 ? expectedResourceTypes.join(", ") : seed.resourceTypes.join(", ")}`
1878
- : `seed missing resource_type(s) for app graph: ${missingSeedResources.join(", ")}`,
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(", ")}`,
1879
1887
  },
1880
1888
  {
1881
1889
  name: "seed-auth-config",
@@ -1897,21 +1905,25 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
1897
1905
  },
1898
1906
  {
1899
1907
  name: "fga-plan",
1900
- ok: fgaManifest.diagnostics.length === 0,
1901
- detail: fgaManifest.diagnostics.length === 0
1902
- ? `FGA plan covers ${fgaManifest.resourceTypes.length} resource type(s), ${fgaManifest.resources.length} resource(s), and ${fgaManifest.proofScenarios.length} proof scenario(s)`
1903
- : `FGA plan has gap(s): ${fgaManifest.diagnostics.join("; ")}`,
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("; ")}`,
1904
1914
  },
1905
1915
  {
1906
1916
  name: "fga-state",
1907
1917
  ok: true,
1908
- detail: !fgaState.exists
1909
- ? `${WORKOS_FGA_STATE_FILE} not found; run forge workos fga sync --json before production deploy`
1910
- : fgaState.matchesManifestHash
1911
- ? `${WORKOS_FGA_STATE_FILE} matches current FGA manifest${fgaState.mode === "real" ? " in real mode" : ""}`
1912
- : fgaState.valid
1913
- ? `${WORKOS_FGA_STATE_FILE} exists but does not match current FGA manifest; rerun forge workos fga sync --json`
1914
- : `${WORKOS_FGA_STATE_FILE} exists but is invalid: ${fgaState.diagnostics.join("; ")}`,
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("; ")}`,
1915
1927
  },
1916
1928
  {
1917
1929
  name: "authkit-routes",
@@ -1935,7 +1947,7 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
1935
1947
  },
1936
1948
  {
1937
1949
  name: "fga-bridge",
1938
- ok: includesAll(resourceMap, [
1950
+ ok: !fgaEnabled || (includesAll(resourceMap, [
1939
1951
  "canWorkOS",
1940
1952
  "assertWorkOSResourceTenant",
1941
1953
  "FORGE_WORKOS_CROSS_TENANT_RESOURCE",
@@ -1945,8 +1957,10 @@ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_
1945
1957
  "permissionSlug",
1946
1958
  "resourceExternalId",
1947
1959
  ]) &&
1948
- includesAll(fga, ["forgeWorkOSResourceTypes"]),
1949
- detail: "WorkOS FGA resource-map bridge exists with sync, cache, telemetry, official check shape, and cross-tenant guard",
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",
1950
1964
  },
1951
1965
  {
1952
1966
  name: "policies",
@@ -2276,6 +2290,7 @@ export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSComma
2276
2290
  const missingSeedPermissions = missingValues(activePermissions, seed.permissions);
2277
2291
  const missingSeedResources = missingValues(expectedResourceTypes, seed.resourceTypes);
2278
2292
  const unusedSeedPermissions = seed.permissions.filter((permission) => !activePermissions.includes(permission));
2293
+ const fgaEnabled = hasWorkOSFgaArtifacts(options.workspaceRoot);
2279
2294
  const checks = [
2280
2295
  {
2281
2296
  name: "seed-file",
@@ -2285,7 +2300,9 @@ export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSComma
2285
2300
  {
2286
2301
  name: "seed-yaml-shape",
2287
2302
  ok: seed.valid,
2288
- detail: seed.valid ? "seed contains permissions, roles, resource_types, and organizations" : seed.diagnostics.join("; "),
2303
+ detail: seed.valid
2304
+ ? `seed contains permissions, roles, organizations${seed.resourceTypes.length > 0 ? ", and optional FGA resource_types" : ""}`
2305
+ : seed.diagnostics.join("; "),
2289
2306
  },
2290
2307
  {
2291
2308
  name: "seed-policy-coverage",
@@ -2305,10 +2322,12 @@ export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSComma
2305
2322
  },
2306
2323
  {
2307
2324
  name: "seed-resource-coverage",
2308
- ok: missingSeedResources.length === 0,
2309
- detail: missingSeedResources.length === 0
2310
- ? `seed covers app resource type(s): ${expectedResourceTypes.join(", ") || "none required"}`
2311
- : `seed missing resource type(s): ${missingSeedResources.join(", ")}`,
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(", ")}`,
2312
2331
  },
2313
2332
  {
2314
2333
  name: "seed-hosted-config",
@@ -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: NonNullable<ReturnType<typeof resolveRecipe>>): Pick<
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: NonNullable<ReturnType<typeof resolveRecipe>>,
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: NonNullable<ReturnType<typeof resolveRecipe>>,
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
- recipe.packages.map((pkg) => pkg.packageName).join(" "),
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
- recipe,
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(recipe),
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
- recipe,
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(recipe),
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 recipe.packages) {
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
- recipe,
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(recipe),
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(recipe),
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(recipe),
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(recipe),
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(recipe),
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/FGA checklist and a `workos-seed.yml` for roles, permissions, organizations, redirect URIs, and CORS origins.",
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
- "## FGA strategy",
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";
@@ -1,3 +1,3 @@
1
- export const FORGEOS_VERSION = "0.1.0-alpha.54";
1
+ export const FORGEOS_VERSION = "0.1.0-alpha.55";
2
2
  export const GENERATOR_VERSION = FORGEOS_VERSION;
3
3
  export const CLI_VERSION = FORGEOS_VERSION;