forgeos 0.1.0-alpha.7 → 0.1.0-alpha.71

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.
Files changed (598) hide show
  1. package/.agents/skills/forge-agent-fabric/SKILL.md +178 -0
  2. package/.agents/skills/forge-agent-fabric/references/managed-workflows.md +95 -0
  3. package/.agents/skills/forge-agent-fabric/references/repository-analysis.md +102 -0
  4. package/.agents/skills/forge-agent-fabric/scripts/fabric.mjs +46 -0
  5. package/.agents/skills/forge-agent-fabric/scripts/repository.mjs +44 -0
  6. package/.npmignore +10 -0
  7. package/AGENTS.md +142 -90
  8. package/CHANGELOG.md +870 -9
  9. package/LICENSE +21 -0
  10. package/README.md +121 -6
  11. package/adapters/go/README.md +23 -0
  12. package/adapters/go/go.mod +3 -0
  13. package/adapters/go/http.go +149 -0
  14. package/adapters/go/registry.go +234 -0
  15. package/adapters/go/types.go +136 -0
  16. package/adapters/java/README.md +68 -0
  17. package/adapters/java/pom.xml +47 -0
  18. package/adapters/java/src/main/java/dev/forgeos/adapter/Auth.java +20 -0
  19. package/adapters/java/src/main/java/dev/forgeos/adapter/Diagnostic.java +16 -0
  20. package/adapters/java/src/main/java/dev/forgeos/adapter/Entry.java +38 -0
  21. package/adapters/java/src/main/java/dev/forgeos/adapter/EntryKind.java +16 -0
  22. package/adapters/java/src/main/java/dev/forgeos/adapter/ErrorInfo.java +4 -0
  23. package/adapters/java/src/main/java/dev/forgeos/adapter/Forge.java +94 -0
  24. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeCall.java +12 -0
  25. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeContext.java +11 -0
  26. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHandler.java +8 -0
  27. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHttpHandler.java +179 -0
  28. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeRegistry.java +121 -0
  29. package/adapters/java/src/main/java/dev/forgeos/adapter/Json.java +14 -0
  30. package/adapters/java/src/main/java/dev/forgeos/adapter/Manifest.java +14 -0
  31. package/adapters/java/src/main/java/dev/forgeos/adapter/RequestEnvelope.java +6 -0
  32. package/adapters/java/src/main/java/dev/forgeos/adapter/ResponseEnvelope.java +25 -0
  33. package/adapters/java/src/main/java/dev/forgeos/adapter/Risk.java +18 -0
  34. package/adapters/java/src/main/java/dev/forgeos/adapter/Schemas.java +36 -0
  35. package/adapters/java/src/main/java/dev/forgeos/adapter/Service.java +65 -0
  36. package/adapters/java/src/main/java/dev/forgeos/adapter/TransactionMode.java +18 -0
  37. package/adapters/java/src/main/java/dev/forgeos/adapter/TypedForgeHandler.java +6 -0
  38. package/adapters/java-spring-boot-starter/README.md +32 -0
  39. package/adapters/java-spring-boot-starter/pom.xml +49 -0
  40. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeCommand.java +22 -0
  41. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeExternalService.java +15 -0
  42. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeQuery.java +16 -0
  43. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.java +18 -0
  44. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.java +16 -0
  45. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringRuntime.java +104 -0
  46. package/adapters/java-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
  47. package/bin/forge.mjs +18 -0
  48. package/docs/agent-fabric-programs.md +151 -0
  49. package/docs/agent-fabric.md +582 -0
  50. package/docs/architecture/agent-fabric/PORTABLE_PROJECTS_PLAN.md +136 -0
  51. package/docs/architecture/agent-fabric/README.md +180 -0
  52. package/docs/architecture/agent-fabric/REPOSITORY_MANIFEST_PLAN.md +284 -0
  53. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_ADR.md +81 -0
  54. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS.md +39 -0
  55. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS_EVIDENCE.md +53 -0
  56. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_EVIDENCE.md +49 -0
  57. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_PLAN.md +590 -0
  58. package/docs/cair-protocol.md +103 -0
  59. package/docs/changelog.md +1070 -0
  60. package/docs/forge-protocol.md +189 -0
  61. package/docs/repository-agent-benchmark.md +179 -0
  62. package/docs/repository-analysis.md +139 -0
  63. package/docs/repository-context-quality.md +179 -0
  64. package/docs/repository-runtime-observation.md +82 -0
  65. package/examples/agent-fabric-v2/README.md +70 -0
  66. package/examples/agent-fabric-v2/item-child.workflow.ts +11 -0
  67. package/examples/agent-fabric-v2/make-request.mjs +4 -0
  68. package/examples/agent-fabric-v2/map-child.workflow.ts +10 -0
  69. package/examples/agent-fabric-v2/migrate.workflow.ts +35 -0
  70. package/examples/agent-fabric-v2/registry.example.json +256 -0
  71. package/examples/agent-fabric-v2/run-ui-fixture.ts +48 -0
  72. package/examples/agent-fabric-v2/ui-audit.workflow.ts +28 -0
  73. package/examples/agent-fabric-v2/ui-fixture-worker.cjs +36 -0
  74. package/examples/agent-fabric-v2/visual-population.json +13 -0
  75. package/examples/agent-fabric-v2/workers/check.mjs +2 -0
  76. package/examples/agent-fabric-v2/workers/common.mjs +7 -0
  77. package/examples/agent-fabric-v2/workers/discover.mjs +2 -0
  78. package/examples/agent-fabric-v2/workers/migrate.mjs +4 -0
  79. package/examples/agent-fabric-v2/workers/review.mjs +3 -0
  80. package/examples/go-billing/go.mod +7 -0
  81. package/examples/go-billing/main.go +120 -0
  82. package/examples/java-billing/pom.xml +60 -0
  83. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/CreateInvoiceInput.java +4 -0
  84. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Invoice.java +11 -0
  85. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Main.java +127 -0
  86. package/package.json +60 -5
  87. package/schemas/agent-fabric/v0.1/control-event.schema.json +281 -0
  88. package/schemas/forge-manifest.schema.json +389 -0
  89. package/scripts/field-test-forgeos.mjs +1021 -0
  90. package/scripts/install-agent-fabric-skill.mjs +105 -0
  91. package/src/forge/_generated/releaseManifest.json +1 -2
  92. package/src/forge/_generated/releaseManifest.ts +3 -3
  93. package/src/forge/agent-adapters/index.ts +1592 -125
  94. package/src/forge/agent-adapters/types.ts +225 -1
  95. package/src/forge/agent-fabric/adapter.ts +131 -0
  96. package/src/forge/agent-fabric/attached-snapshot.ts +55 -0
  97. package/src/forge/agent-fabric/attached-task-contract.ts +105 -0
  98. package/src/forge/agent-fabric/attached-task-service.ts +127 -0
  99. package/src/forge/agent-fabric/attached-task-store.ts +109 -0
  100. package/src/forge/agent-fabric/authority.ts +298 -0
  101. package/src/forge/agent-fabric/canonical.ts +135 -0
  102. package/src/forge/agent-fabric/codex-adversarial-review.ts +229 -0
  103. package/src/forge/agent-fabric/codex-sdk-worker.ts +203 -0
  104. package/src/forge/agent-fabric/conductor.ts +754 -0
  105. package/src/forge/agent-fabric/dictionary.ts +12 -0
  106. package/src/forge/agent-fabric/errors.ts +32 -0
  107. package/src/forge/agent-fabric/hardened-conductor.ts +485 -0
  108. package/src/forge/agent-fabric/hardened-reducer.ts +211 -0
  109. package/src/forge/agent-fabric/index.ts +48 -0
  110. package/src/forge/agent-fabric/journal.ts +116 -0
  111. package/src/forge/agent-fabric/local-adaptive-digest-worker.mjs +26 -0
  112. package/src/forge/agent-fabric/local-adaptive-harness.ts +343 -0
  113. package/src/forge/agent-fabric/local-adaptive-service.ts +398 -0
  114. package/src/forge/agent-fabric/local-adaptive-worker.ts +146 -0
  115. package/src/forge/agent-fabric/local-approval-window.ts +211 -0
  116. package/src/forge/agent-fabric/local-change-review-service.ts +374 -0
  117. package/src/forge/agent-fabric/local-coding-worker.ts +285 -0
  118. package/src/forge/agent-fabric/local-control-store.ts +239 -0
  119. package/src/forge/agent-fabric/local-effect-broker.ts +348 -0
  120. package/src/forge/agent-fabric/local-evolution-approval.ts +81 -0
  121. package/src/forge/agent-fabric/local-evolution-profile.ts +87 -0
  122. package/src/forge/agent-fabric/local-evolution-registry.ts +427 -0
  123. package/src/forge/agent-fabric/local-evolution-service.ts +200 -0
  124. package/src/forge/agent-fabric/local-intelligence.ts +281 -0
  125. package/src/forge/agent-fabric/local-paths.ts +18 -0
  126. package/src/forge/agent-fabric/local-task-contract.ts +203 -0
  127. package/src/forge/agent-fabric/local-task-inbox.ts +543 -0
  128. package/src/forge/agent-fabric/local-task-server.ts +374 -0
  129. package/src/forge/agent-fabric/local-task-service.ts +982 -0
  130. package/src/forge/agent-fabric/local-verification.ts +551 -0
  131. package/src/forge/agent-fabric/managed-environment.ts +205 -0
  132. package/src/forge/agent-fabric/managed-run-contract.ts +103 -0
  133. package/src/forge/agent-fabric/managed-run-service.ts +401 -0
  134. package/src/forge/agent-fabric/managed-run-store.ts +165 -0
  135. package/src/forge/agent-fabric/managed-workspace.ts +253 -0
  136. package/src/forge/agent-fabric/p0a.ts +104 -0
  137. package/src/forge/agent-fabric/p0b-model-adapter.ts +434 -0
  138. package/src/forge/agent-fabric/planning.ts +155 -0
  139. package/src/forge/agent-fabric/program-api.ts +37 -0
  140. package/src/forge/agent-fabric/program-contract.ts +283 -0
  141. package/src/forge/agent-fabric/program-dsl.ts +126 -0
  142. package/src/forge/agent-fabric/program-evidence.ts +22 -0
  143. package/src/forge/agent-fabric/program-image.ts +15 -0
  144. package/src/forge/agent-fabric/program-lock.ts +50 -0
  145. package/src/forge/agent-fabric/program-owner.ts +16 -0
  146. package/src/forge/agent-fabric/program-scheduler.ts +38 -0
  147. package/src/forge/agent-fabric/program-service.ts +793 -0
  148. package/src/forge/agent-fabric/program-store.ts +142 -0
  149. package/src/forge/agent-fabric/program-structure.ts +65 -0
  150. package/src/forge/agent-fabric/program-types.ts +110 -0
  151. package/src/forge/agent-fabric/program-view.ts +12 -0
  152. package/src/forge/agent-fabric/program-worker.ts +108 -0
  153. package/src/forge/agent-fabric/project-profile.ts +50 -0
  154. package/src/forge/agent-fabric/project-registry.ts +120 -0
  155. package/src/forge/agent-fabric/project-runtime.ts +85 -0
  156. package/src/forge/agent-fabric/readiness.ts +26 -0
  157. package/src/forge/agent-fabric/reducer.ts +863 -0
  158. package/src/forge/agent-fabric/repository-context.ts +83 -0
  159. package/src/forge/agent-fabric/resource-ledger.ts +266 -0
  160. package/src/forge/agent-fabric/serialized-local-adapter.ts +60 -0
  161. package/src/forge/agent-fabric/types.ts +424 -0
  162. package/src/forge/agent-fabric/validation.ts +500 -0
  163. package/src/forge/agent-fabric/workflow-engine.ts +218 -0
  164. package/src/forge/agent-fabric/workflow-task-actions.ts +104 -0
  165. package/src/forge/agent-fabric/workflow-templates.ts +11 -0
  166. package/src/forge/agent-memory/bridge.ts +1728 -0
  167. package/src/forge/agent-memory/context-pack.ts +284 -0
  168. package/src/forge/agent-memory/hook-runner.ts +331 -0
  169. package/src/forge/agent-memory/mcp.ts +597 -0
  170. package/src/forge/agent-memory/normalize.ts +498 -0
  171. package/src/forge/agent-memory/redaction.ts +103 -0
  172. package/src/forge/agent-memory/sources/claude-code.ts +51 -0
  173. package/src/forge/agent-memory/sources/codex-hook-runner.mjs +343 -0
  174. package/src/forge/agent-memory/sources/codex.ts +161 -0
  175. package/src/forge/agent-memory/sources/cursor.ts +35 -0
  176. package/src/forge/agent-memory/types.ts +215 -0
  177. package/src/forge/bench.ts +248 -0
  178. package/src/forge/brownfield-import/index.ts +896 -0
  179. package/src/forge/brownfield-import/types.ts +127 -0
  180. package/src/forge/cair/action-journal.ts +61 -0
  181. package/src/forge/cair/action-parser.ts +314 -0
  182. package/src/forge/cair/action-validator.ts +40 -0
  183. package/src/forge/cair/actions.ts +1818 -0
  184. package/src/forge/cair/format.ts +77 -0
  185. package/src/forge/cair/index.ts +118 -0
  186. package/src/forge/cair/query.ts +478 -0
  187. package/src/forge/cair/snapshot.ts +315 -0
  188. package/src/forge/cair/types.ts +255 -0
  189. package/src/forge/cli/adaptive.ts +51 -0
  190. package/src/forge/cli/ai.ts +16 -1
  191. package/src/forge/cli/auth.ts +459 -15
  192. package/src/forge/cli/authmd.ts +441 -0
  193. package/src/forge/cli/baseline.ts +112 -0
  194. package/src/forge/cli/build.ts +20 -4
  195. package/src/forge/cli/changed.ts +418 -0
  196. package/src/forge/cli/codex-app-server.ts +877 -0
  197. package/src/forge/cli/commands.ts +1859 -20
  198. package/src/forge/cli/db.ts +209 -2
  199. package/src/forge/cli/deploy.ts +1339 -0
  200. package/src/forge/cli/deps.ts +150 -23
  201. package/src/forge/cli/dev.ts +1137 -53
  202. package/src/forge/cli/docs.ts +265 -0
  203. package/src/forge/cli/doctor.ts +297 -0
  204. package/src/forge/cli/env-placeholders.ts +62 -0
  205. package/src/forge/cli/evolution.ts +40 -0
  206. package/src/forge/cli/fabric.ts +253 -0
  207. package/src/forge/cli/field-test.ts +568 -0
  208. package/src/forge/cli/golden-path.ts +487 -0
  209. package/src/forge/cli/handoff.ts +336 -0
  210. package/src/forge/cli/index.ts +1 -0
  211. package/src/forge/cli/last-run.ts +84 -0
  212. package/src/forge/cli/main.ts +196 -3
  213. package/src/forge/cli/new.ts +169 -17
  214. package/src/forge/cli/next-actions.ts +23 -0
  215. package/src/forge/cli/output.ts +398 -6
  216. package/src/forge/cli/parse.ts +1377 -38
  217. package/src/forge/cli/progress.ts +51 -0
  218. package/src/forge/cli/query.ts +32 -0
  219. package/src/forge/cli/release.ts +35 -11
  220. package/src/forge/cli/repository.ts +151 -0
  221. package/src/forge/cli/run.ts +42 -0
  222. package/src/forge/cli/secrets.ts +137 -1
  223. package/src/forge/cli/seed.ts +581 -0
  224. package/src/forge/cli/self-host.ts +56 -14
  225. package/src/forge/cli/studio.ts +2282 -0
  226. package/src/forge/cli/verify.ts +1458 -32
  227. package/src/forge/cli/windows.ts +23 -0
  228. package/src/forge/cli/workos.ts +3509 -0
  229. package/src/forge/compiler/agent-contract/build.ts +728 -64
  230. package/src/forge/compiler/agent-contract/types.ts +45 -2
  231. package/src/forge/compiler/ai-registry/parse.ts +71 -2
  232. package/src/forge/compiler/api-surface/build.ts +47 -0
  233. package/src/forge/compiler/app-graph/build.ts +112 -9
  234. package/src/forge/compiler/app-graph/extract.ts +107 -0
  235. package/src/forge/compiler/app-graph/module-graph.ts +73 -78
  236. package/src/forge/compiler/app-graph/parser.ts +24 -24
  237. package/src/forge/compiler/app-graph/profile.ts +26 -0
  238. package/src/forge/compiler/app-graph/tsconfig-hash.ts +41 -4
  239. package/src/forge/compiler/app-graph/versions.ts +1 -1
  240. package/src/forge/compiler/classifier/capabilities.ts +3 -2
  241. package/src/forge/compiler/classifier/classify.ts +32 -8
  242. package/src/forge/compiler/classifier/secrets.ts +3 -2
  243. package/src/forge/compiler/classifier/signals.ts +91 -1
  244. package/src/forge/compiler/client-sdk/build-manifest.ts +59 -0
  245. package/src/forge/compiler/client-sdk/render-client.ts +295 -13
  246. package/src/forge/compiler/data-graph/build.ts +29 -1
  247. package/src/forge/compiler/data-graph/parse.ts +20 -4
  248. package/src/forge/compiler/data-graph/sql/ddl.ts +144 -31
  249. package/src/forge/compiler/data-graph/sql/serialize.ts +4 -0
  250. package/src/forge/compiler/data-graph/sql/types.ts +1 -0
  251. package/src/forge/compiler/diagnostics/codes.ts +38 -0
  252. package/src/forge/compiler/diagnostics/create.ts +28 -2
  253. package/src/forge/compiler/diagnostics/index.ts +2 -0
  254. package/src/forge/compiler/emitter/barrel.ts +3 -0
  255. package/src/forge/compiler/emitter/render.ts +9 -0
  256. package/src/forge/compiler/external-manifest/registry.ts +217 -0
  257. package/src/forge/compiler/external-manifest/types.ts +91 -0
  258. package/src/forge/compiler/external-manifest/validate.ts +373 -0
  259. package/src/forge/compiler/frontend-graph/build.ts +142 -14
  260. package/src/forge/compiler/integration/add.ts +991 -33
  261. package/src/forge/compiler/integration/plan.ts +43 -12
  262. package/src/forge/compiler/integration/render.ts +29 -0
  263. package/src/forge/compiler/integration/snapshot.ts +2 -0
  264. package/src/forge/compiler/integration/templates/convex.ts +70 -0
  265. package/src/forge/compiler/integration/templates/index.ts +1 -0
  266. package/src/forge/compiler/integration/templates/render.ts +38 -0
  267. package/src/forge/compiler/integration/templates/types.ts +3 -0
  268. package/src/forge/compiler/integration/templates/workos.ts +1439 -0
  269. package/src/forge/compiler/make-registry/build.ts +8 -9
  270. package/src/forge/compiler/orchestrator/fast-check.ts +4 -0
  271. package/src/forge/compiler/orchestrator/generate-lock.ts +14 -3
  272. package/src/forge/compiler/orchestrator/manifest.ts +1 -1
  273. package/src/forge/compiler/orchestrator/plan-profile.ts +23 -0
  274. package/src/forge/compiler/orchestrator/plan.ts +249 -10
  275. package/src/forge/compiler/orchestrator/profile.ts +65 -0
  276. package/src/forge/compiler/orchestrator/run.ts +101 -33
  277. package/src/forge/compiler/orchestrator/serialize.ts +33 -8
  278. package/src/forge/compiler/orchestrator/types.ts +1 -1
  279. package/src/forge/compiler/package-graph/compiler.ts +3 -1
  280. package/src/forge/compiler/package-graph/constants.ts +1 -1
  281. package/src/forge/compiler/package-graph/dts-extractor.ts +14 -1
  282. package/src/forge/compiler/package-manager/adapter.ts +4 -1
  283. package/src/forge/compiler/package-manager/commands.ts +4 -0
  284. package/src/forge/compiler/package-manager/executor.ts +30 -1
  285. package/src/forge/compiler/package-manager/version.ts +36 -12
  286. package/src/forge/compiler/package-upgrades/planner.ts +155 -18
  287. package/src/forge/compiler/package-upgrades/types.ts +2 -0
  288. package/src/forge/compiler/policy-registry/build.ts +47 -2
  289. package/src/forge/compiler/policy-registry/parse.ts +32 -2
  290. package/src/forge/compiler/recipes/definitions.ts +63 -0
  291. package/src/forge/compiler/recipes/index.ts +2 -0
  292. package/src/forge/compiler/recipes/profiles.ts +81 -0
  293. package/src/forge/compiler/recipes/registry.ts +6 -0
  294. package/src/forge/compiler/test-graph/build.ts +11 -3
  295. package/src/forge/compiler/types/app-graph.ts +8 -2
  296. package/src/forge/compiler/types/cli.ts +76 -1
  297. package/src/forge/compiler/types/dev-manifest.ts +4 -0
  298. package/src/forge/compiler/types/emit.ts +2 -0
  299. package/src/forge/compiler/types/frontend-graph.ts +2 -2
  300. package/src/forge/compiler/types/integration.ts +1 -0
  301. package/src/forge/compiler/types/package-graph.ts +6 -0
  302. package/src/forge/compiler/types/policy-registry.ts +3 -1
  303. package/src/forge/delta/broker-runner.mjs +5 -0
  304. package/src/forge/delta/broker.ts +429 -0
  305. package/src/forge/delta/classifier.ts +52 -0
  306. package/src/forge/delta/explain.ts +238 -0
  307. package/src/forge/delta/git-observer.ts +67 -0
  308. package/src/forge/delta/ids.ts +44 -0
  309. package/src/forge/delta/index.ts +25 -0
  310. package/src/forge/delta/process-identity.ts +37 -0
  311. package/src/forge/delta/queue-status.ts +190 -0
  312. package/src/forge/delta/recorder.ts +540 -0
  313. package/src/forge/delta/redaction.ts +50 -0
  314. package/src/forge/delta/schema.ts +240 -0
  315. package/src/forge/delta/session.ts +142 -0
  316. package/src/forge/delta/status.ts +1139 -0
  317. package/src/forge/delta/store.ts +3329 -0
  318. package/src/forge/delta/timeline.ts +178 -0
  319. package/src/forge/dev/server.ts +1061 -22
  320. package/src/forge/dev/types.ts +15 -1
  321. package/src/forge/dev/watch.ts +17 -7
  322. package/src/forge/dev-console/cycle.ts +285 -33
  323. package/src/forge/dev-console/types.ts +47 -1
  324. package/src/forge/impact/index.ts +222 -30
  325. package/src/forge/impact/types.ts +33 -1
  326. package/src/forge/intent/index.ts +35 -16
  327. package/src/forge/make/fields.ts +26 -0
  328. package/src/forge/make/index.ts +42 -6
  329. package/src/forge/make/templates.ts +211 -4
  330. package/src/forge/make/types.ts +2 -1
  331. package/src/forge/policy.ts +1 -1
  332. package/src/forge/react/index.ts +160 -2
  333. package/src/forge/refactor/index.ts +1 -0
  334. package/src/forge/repair/rules/index.ts +2 -2
  335. package/src/forge/repository-analysis/adapters/common.ts +72 -0
  336. package/src/forge/repository-analysis/adapters/docker.ts +185 -0
  337. package/src/forge/repository-analysis/adapters/java.ts +133 -0
  338. package/src/forge/repository-analysis/adapters/typescript.ts +350 -0
  339. package/src/forge/repository-analysis/adapters/vue.ts +71 -0
  340. package/src/forge/repository-analysis/analyze.ts +434 -0
  341. package/src/forge/repository-analysis/benchmark.ts +336 -0
  342. package/src/forge/repository-analysis/cache-gc.ts +154 -0
  343. package/src/forge/repository-analysis/check-selection.ts +123 -0
  344. package/src/forge/repository-analysis/context-node.ts +17 -0
  345. package/src/forge/repository-analysis/context.ts +456 -0
  346. package/src/forge/repository-analysis/http-resolution.ts +135 -0
  347. package/src/forge/repository-analysis/identity.ts +45 -0
  348. package/src/forge/repository-analysis/index.ts +18 -0
  349. package/src/forge/repository-analysis/nuxt-resolution.ts +188 -0
  350. package/src/forge/repository-analysis/quality.ts +60 -0
  351. package/src/forge/repository-analysis/retrieval.ts +309 -0
  352. package/src/forge/repository-analysis/runtime-artifacts.ts +195 -0
  353. package/src/forge/repository-analysis/runtime-observation.ts +325 -0
  354. package/src/forge/repository-analysis/scanner.ts +174 -0
  355. package/src/forge/repository-analysis/storage.ts +193 -0
  356. package/src/forge/repository-analysis/test-associations.ts +59 -0
  357. package/src/forge/repository-analysis/types.ts +99 -0
  358. package/src/forge/repository-manifest/index.ts +178 -0
  359. package/src/forge/repository-manifest/types.ts +63 -0
  360. package/src/forge/review/index.ts +162 -12
  361. package/src/forge/review/types.ts +15 -0
  362. package/src/forge/runtime/ai/providers.ts +19 -4
  363. package/src/forge/runtime/auth/config.ts +17 -0
  364. package/src/forge/runtime/auth/evaluate.ts +15 -2
  365. package/src/forge/runtime/auth/resolve.ts +61 -6
  366. package/src/forge/runtime/auth/types.ts +1 -0
  367. package/src/forge/runtime/db/factory.ts +1 -3
  368. package/src/forge/runtime/db/generated-client.ts +13 -2
  369. package/src/forge/runtime/db/memory-adapter.ts +139 -32
  370. package/src/forge/runtime/db/pglite-adapter.ts +285 -2
  371. package/src/forge/runtime/executor.ts +129 -16
  372. package/src/forge/runtime/external/bridge.ts +663 -0
  373. package/src/forge/runtime/runner/run-entry.ts +16 -7
  374. package/src/forge/runtime/telemetry/buffer.ts +4 -1
  375. package/src/forge/runtime/telemetry/scrubber.ts +35 -5
  376. package/src/forge/runtime/webhooks/security.ts +12 -7
  377. package/src/forge/server.ts +71 -7
  378. package/src/forge/ui/index.ts +831 -19
  379. package/src/forge/ui/types.ts +3 -1
  380. package/src/forge/version.ts +1 -1
  381. package/src/forge/vue/index.ts +422 -0
  382. package/src/forge/workspace/baseline.ts +112 -0
  383. package/src/forge/workspace/change-summary.ts +292 -0
  384. package/src/forge/workspace/forge-cli.ts +42 -0
  385. package/src/forge/workspace/git-summary.ts +393 -0
  386. package/templates/agent-workroom/AGENTS.md +29 -0
  387. package/templates/agent-workroom/README.md +34 -0
  388. package/templates/agent-workroom/forge.config.ts +3 -0
  389. package/templates/agent-workroom/package.json +34 -0
  390. package/templates/agent-workroom/pnpm-workspace.yaml +2 -0
  391. package/templates/agent-workroom/src/actions/indexAgentSignal.ts +10 -0
  392. package/templates/agent-workroom/src/commands/openWorkroom.ts +61 -0
  393. package/templates/agent-workroom/src/commands/recordAgentSignal.ts +119 -0
  394. package/templates/agent-workroom/src/commands/recordCheckRun.ts +52 -0
  395. package/templates/agent-workroom/src/forge/schema.ts +54 -0
  396. package/templates/agent-workroom/src/policies.ts +6 -0
  397. package/templates/agent-workroom/src/queries/listWorkrooms.ts +11 -0
  398. package/templates/agent-workroom/src/queries/liveWorkroom.ts +63 -0
  399. package/templates/agent-workroom/tsconfig.json +16 -0
  400. package/templates/agent-workroom/web/index.html +13 -0
  401. package/templates/agent-workroom/web/package.json +21 -0
  402. package/templates/agent-workroom/web/src/App.tsx +345 -0
  403. package/templates/agent-workroom/web/src/lib/forge.ts +20 -0
  404. package/templates/agent-workroom/web/src/main.tsx +13 -0
  405. package/templates/agent-workroom/web/src/styles.css +545 -0
  406. package/templates/agent-workroom/web/tsconfig.json +27 -0
  407. package/templates/agent-workroom/web/vite.config.ts +36 -0
  408. package/templates/b2b-support-web/package.json +2 -0
  409. package/templates/b2b-support-web/pnpm-workspace.yaml +2 -0
  410. package/templates/b2b-support-web/web/app/page.tsx +2 -2
  411. package/templates/b2b-support-web/web/lib/forge.ts +9 -2
  412. package/templates/b2b-support-web/web/next.config.ts +24 -0
  413. package/templates/b2b-support-web/web/package.json +1 -1
  414. package/templates/minimal-web/package.json +2 -1
  415. package/templates/minimal-web/pnpm-workspace.yaml +2 -0
  416. package/templates/minimal-web/web/index.html +1 -0
  417. package/templates/minimal-web/web/src/App.tsx +12 -6
  418. package/templates/minimal-web/web/src/lib/forge.ts +9 -2
  419. package/templates/minimal-web/web/src/styles.css +34 -0
  420. package/templates/minimal-web/web/vite.config.ts +36 -0
  421. package/templates/nuxt-web/.vscode/settings.json +14 -0
  422. package/templates/nuxt-web/.yarnrc.yml +1 -0
  423. package/templates/nuxt-web/README.md +30 -0
  424. package/templates/nuxt-web/forge.config.ts +3 -0
  425. package/templates/nuxt-web/package.json +37 -0
  426. package/templates/nuxt-web/pnpm-workspace.yaml +2 -0
  427. package/templates/nuxt-web/src/actions/logNoteCreated.ts +11 -0
  428. package/templates/nuxt-web/src/commands/createNote.ts +26 -0
  429. package/templates/nuxt-web/src/forge/schema.ts +12 -0
  430. package/templates/nuxt-web/src/policies.ts +6 -0
  431. package/templates/nuxt-web/src/queries/listNotes.ts +8 -0
  432. package/templates/nuxt-web/src/queries/liveNotes.ts +8 -0
  433. package/templates/nuxt-web/tsconfig.json +17 -0
  434. package/templates/nuxt-web/web/app.vue +67 -0
  435. package/templates/nuxt-web/web/components/LiveNotes.vue +89 -0
  436. package/templates/nuxt-web/web/components/NoteComposer.vue +100 -0
  437. package/templates/nuxt-web/web/composables/forge.ts +13 -0
  438. package/templates/nuxt-web/web/composables/useNotes.ts +24 -0
  439. package/templates/nuxt-web/web/nuxt.config.ts +11 -0
  440. package/templates/nuxt-web/web/package.json +22 -0
  441. package/templates/nuxt-web/web/plugins/forge.client.ts +10 -0
  442. package/templates/nuxt-web/web/plugins/forge.server.ts +10 -0
  443. package/templates/nuxt-web/web/server/api/forge-health.get.ts +7 -0
  444. package/templates/nuxt-web/web/tsconfig.json +6 -0
  445. package/templates/vendor-access/.vscode/settings.json +14 -0
  446. package/templates/vendor-access/README.md +30 -0
  447. package/templates/vendor-access/forge.config.ts +3 -0
  448. package/templates/vendor-access/package.json +35 -0
  449. package/templates/vendor-access/pnpm-workspace.yaml +2 -0
  450. package/templates/vendor-access/src/commands/addEvidence.ts +42 -0
  451. package/templates/vendor-access/src/commands/approveAccessRequest.ts +43 -0
  452. package/templates/vendor-access/src/commands/createAccessRequest.ts +49 -0
  453. package/templates/vendor-access/src/commands/seedVendorAccessDemo.ts +200 -0
  454. package/templates/vendor-access/src/forge/schema.ts +74 -0
  455. package/templates/vendor-access/src/policies.ts +11 -0
  456. package/templates/vendor-access/src/queries/listVendorAccessDashboard.ts +24 -0
  457. package/templates/vendor-access/src/queries/liveVendorAccessDashboard.ts +24 -0
  458. package/templates/vendor-access/tsconfig.json +17 -0
  459. package/templates/vendor-access/web/index.html +13 -0
  460. package/templates/vendor-access/web/package.json +21 -0
  461. package/templates/vendor-access/web/src/App.tsx +573 -0
  462. package/templates/vendor-access/web/src/lib/forge.ts +20 -0
  463. package/templates/vendor-access/web/src/main.tsx +205 -0
  464. package/templates/vendor-access/web/src/styles.css +682 -0
  465. package/templates/vendor-access/web/tsconfig.json +18 -0
  466. package/templates/vendor-access/web/vite.config.ts +36 -0
  467. package/src/forge/_generated/actionSubscriptions.json +0 -2
  468. package/src/forge/_generated/actionSubscriptions.ts +0 -10
  469. package/src/forge/_generated/agentAdapterManifest.json +0 -2
  470. package/src/forge/_generated/agentAdapterManifest.ts +0 -73
  471. package/src/forge/_generated/agentContract.json +0 -2
  472. package/src/forge/_generated/agentContract.ts +0 -7829
  473. package/src/forge/_generated/agentQuickstart.md +0 -34
  474. package/src/forge/_generated/agentTools.json +0 -2
  475. package/src/forge/_generated/agentTools.md +0 -16
  476. package/src/forge/_generated/agentTools.ts +0 -12
  477. package/src/forge/_generated/aiContext.ts +0 -125
  478. package/src/forge/_generated/aiModels.json +0 -2
  479. package/src/forge/_generated/aiModels.ts +0 -51
  480. package/src/forge/_generated/aiProviders.json +0 -2
  481. package/src/forge/_generated/aiProviders.ts +0 -23
  482. package/src/forge/_generated/aiRegistry.json +0 -2
  483. package/src/forge/_generated/aiRegistry.ts +0 -31
  484. package/src/forge/_generated/api.json +0 -2
  485. package/src/forge/_generated/api.ts +0 -8
  486. package/src/forge/_generated/appGraph.json +0 -2
  487. package/src/forge/_generated/appGraph.ts +0 -14937
  488. package/src/forge/_generated/appMap.md +0 -55
  489. package/src/forge/_generated/artifactManifest.json +0 -2
  490. package/src/forge/_generated/artifactManifest.ts +0 -7
  491. package/src/forge/_generated/authClaims.json +0 -2
  492. package/src/forge/_generated/authClaims.ts +0 -13
  493. package/src/forge/_generated/authConfig.json +0 -2
  494. package/src/forge/_generated/authConfig.ts +0 -17
  495. package/src/forge/_generated/authContext.ts +0 -23
  496. package/src/forge/_generated/authRegistry.json +0 -2
  497. package/src/forge/_generated/authRegistry.ts +0 -25
  498. package/src/forge/_generated/buildInfo.json +0 -2
  499. package/src/forge/_generated/buildInfo.ts +0 -9
  500. package/src/forge/_generated/capabilityMap.json +0 -2
  501. package/src/forge/_generated/capabilityMap.md +0 -15
  502. package/src/forge/_generated/capabilityMap.ts +0 -17
  503. package/src/forge/_generated/client.ts +0 -282
  504. package/src/forge/_generated/clientApi.ts +0 -9
  505. package/src/forge/_generated/clientManifest.json +0 -2
  506. package/src/forge/_generated/clientManifest.ts +0 -39
  507. package/src/forge/_generated/clientTypes.ts +0 -78
  508. package/src/forge/_generated/configRegistry.json +0 -2
  509. package/src/forge/_generated/configRegistry.ts +0 -4
  510. package/src/forge/_generated/dataGraph.json +0 -2
  511. package/src/forge/_generated/dataGraph.ts +0 -8
  512. package/src/forge/_generated/db.json +0 -2
  513. package/src/forge/_generated/db.ts +0 -2
  514. package/src/forge/_generated/dbSecurityManifest.json +0 -2
  515. package/src/forge/_generated/dbSecurityManifest.ts +0 -15
  516. package/src/forge/_generated/dbSessionContext.json +0 -2
  517. package/src/forge/_generated/dbSessionContext.ts +0 -39
  518. package/src/forge/_generated/deployManifest.json +0 -2
  519. package/src/forge/_generated/deployManifest.ts +0 -14
  520. package/src/forge/_generated/devManifest.json +0 -2
  521. package/src/forge/_generated/devManifest.ts +0 -62
  522. package/src/forge/_generated/envSchema.json +0 -2
  523. package/src/forge/_generated/envSchema.ts +0 -59
  524. package/src/forge/_generated/frontendGraph.json +0 -2
  525. package/src/forge/_generated/frontendGraph.ts +0 -27
  526. package/src/forge/_generated/importGuards.json +0 -2
  527. package/src/forge/_generated/importGuards.ts +0 -686
  528. package/src/forge/_generated/index.ts +0 -68
  529. package/src/forge/_generated/liveProductionManifest.json +0 -2
  530. package/src/forge/_generated/liveProductionManifest.ts +0 -23
  531. package/src/forge/_generated/liveProtocol.json +0 -2
  532. package/src/forge/_generated/liveProtocol.ts +0 -21
  533. package/src/forge/_generated/liveQueryRegistry.json +0 -2
  534. package/src/forge/_generated/liveQueryRegistry.ts +0 -9
  535. package/src/forge/_generated/liveTransportConfig.json +0 -2
  536. package/src/forge/_generated/liveTransportConfig.ts +0 -19
  537. package/src/forge/_generated/makeRegistry.json +0 -2
  538. package/src/forge/_generated/makeRegistry.ts +0 -177
  539. package/src/forge/_generated/makeTemplates.json +0 -2
  540. package/src/forge/_generated/makeTemplates.ts +0 -66
  541. package/src/forge/_generated/mockMap.json +0 -2
  542. package/src/forge/_generated/mockMap.ts +0 -7
  543. package/src/forge/_generated/operationPlaybooks.md +0 -167
  544. package/src/forge/_generated/packageGraph.json +0 -2
  545. package/src/forge/_generated/packageGraph.ts +0 -249334
  546. package/src/forge/_generated/packageUpgradeRegistry.json +0 -2
  547. package/src/forge/_generated/packageUpgradeRegistry.ts +0 -15
  548. package/src/forge/_generated/permissionMatrix.json +0 -2
  549. package/src/forge/_generated/permissionMatrix.ts +0 -7
  550. package/src/forge/_generated/policyRegistry.json +0 -2
  551. package/src/forge/_generated/policyRegistry.ts +0 -11
  552. package/src/forge/_generated/queryRegistry.json +0 -2
  553. package/src/forge/_generated/queryRegistry.ts +0 -9
  554. package/src/forge/_generated/react.d.ts +0 -22
  555. package/src/forge/_generated/react.ts +0 -29
  556. package/src/forge/_generated/reactManifest.json +0 -2
  557. package/src/forge/_generated/reactManifest.ts +0 -19
  558. package/src/forge/_generated/rlsPolicies.json +0 -2
  559. package/src/forge/_generated/rlsPolicies.sql +0 -34
  560. package/src/forge/_generated/rlsPolicies.ts +0 -6
  561. package/src/forge/_generated/runtimeGraph.json +0 -2
  562. package/src/forge/_generated/runtimeGraph.ts +0 -8
  563. package/src/forge/_generated/runtimeMatrix.json +0 -2
  564. package/src/forge/_generated/runtimeMatrix.ts +0 -334130
  565. package/src/forge/_generated/runtimeRegistry.ts +0 -2
  566. package/src/forge/_generated/runtimeRules.md +0 -91
  567. package/src/forge/_generated/secretRegistry.json +0 -2
  568. package/src/forge/_generated/secretRegistry.ts +0 -50
  569. package/src/forge/_generated/secretsContext.ts +0 -11
  570. package/src/forge/_generated/serverApi.ts +0 -10
  571. package/src/forge/_generated/sourceMapManifest.json +0 -2
  572. package/src/forge/_generated/sourceMapManifest.ts +0 -7
  573. package/src/forge/_generated/sqlPlan.json +0 -2
  574. package/src/forge/_generated/sqlPlan.ts +0 -88
  575. package/src/forge/_generated/subscriptionManifest.json +0 -2
  576. package/src/forge/_generated/subscriptionManifest.ts +0 -7
  577. package/src/forge/_generated/symbolicationManifest.json +0 -2
  578. package/src/forge/_generated/symbolicationManifest.ts +0 -17
  579. package/src/forge/_generated/telemetryRegistry.json +0 -2
  580. package/src/forge/_generated/telemetryRegistry.ts +0 -9
  581. package/src/forge/_generated/telemetrySinks.json +0 -2
  582. package/src/forge/_generated/telemetrySinks.ts +0 -11
  583. package/src/forge/_generated/tenantScope.json +0 -2
  584. package/src/forge/_generated/tenantScope.ts +0 -8
  585. package/src/forge/_generated/testGraph.json +0 -2
  586. package/src/forge/_generated/testGraph.ts +0 -3452
  587. package/src/forge/_generated/testPlanRegistry.json +0 -2
  588. package/src/forge/_generated/testPlanRegistry.ts +0 -33
  589. package/src/forge/_generated/uiRoutes.json +0 -2
  590. package/src/forge/_generated/uiRoutes.ts +0 -16
  591. package/src/forge/_generated/uiScenarios.json +0 -2
  592. package/src/forge/_generated/uiScenarios.ts +0 -30
  593. package/src/forge/_generated/uiTestManifest.json +0 -2
  594. package/src/forge/_generated/uiTestManifest.ts +0 -27
  595. package/src/forge/_generated/workflowRegistry.json +0 -2
  596. package/src/forge/_generated/workflowRegistry.ts +0 -9
  597. package/src/forge/_generated/workflowSubscriptions.json +0 -2
  598. package/src/forge/_generated/workflowSubscriptions.ts +0 -10
@@ -0,0 +1,3509 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { createHash, randomBytes } from "node:crypto";
6
+ import { GENERATED_DIR } from "../compiler/emitter/constants.ts";
7
+ import { stripDeterministicHeader } from "../compiler/primitives/header.ts";
8
+
9
+ export type WorkOSSubcommand = "install" | "doctor" | "seed" | "setup" | "prove" | "env" | "fga";
10
+ export type WorkOSFgaAction = "plan" | "sync" | "prove" | "doctor";
11
+
12
+ export interface WorkOSCommandOptions {
13
+ subcommand: WorkOSSubcommand;
14
+ fgaAction?: WorkOSFgaAction;
15
+ workspaceRoot: string;
16
+ json: boolean;
17
+ file?: string;
18
+ yes: boolean;
19
+ dryRun: boolean;
20
+ real?: boolean;
21
+ write?: boolean;
22
+ writePath?: string;
23
+ clientId?: string;
24
+ skipCliEnv?: boolean;
25
+ commandRunner?: WorkOSCommandRunner;
26
+ }
27
+
28
+ export interface WorkOSCheck {
29
+ name: string;
30
+ ok: boolean;
31
+ detail: string;
32
+ }
33
+
34
+ export interface WorkOSCommandResult {
35
+ ok: boolean;
36
+ kind: "workos-install" | "workos-doctor" | "workos-seed" | "workos-setup" | "workos-prove" | "workos-env" | "workos-fga";
37
+ checks: WorkOSCheck[];
38
+ command?: string[];
39
+ applied?: boolean;
40
+ data?: unknown;
41
+ stdout?: string;
42
+ stderr?: string;
43
+ exitCode: 0 | 1;
44
+ }
45
+
46
+ export type WorkOSCommandRunner = (
47
+ command: string,
48
+ args: string[],
49
+ options: {
50
+ cwd: string;
51
+ encoding: "utf8";
52
+ stdio: ["ignore", "pipe", "pipe"];
53
+ env?: Record<string, string | undefined>;
54
+ },
55
+ ) => { status: number | null; stdout: string; stderr: string };
56
+
57
+ const DEFAULT_SEED_FILE = "workos-seed.yml";
58
+ const GENERATED_SEED_FILE = `${GENERATED_DIR}/integrations/workos/workos-seed.yml`;
59
+ const WORKOS_SEED_STATE_FILE = ".workos-seed-state.json";
60
+ const WORKOS_FGA_STATE_FILE = ".workos-fga-state.json";
61
+ const WORKOS_FGA_SETUP_GUIDE_FILE = ".forge/workos-fga-setup.md";
62
+ const WORKOS_FGA_SETUP_PLAN_FILE = ".forge/workos-fga-setup.json";
63
+
64
+ function runExternalCommand(
65
+ command: string[],
66
+ options: WorkOSCommandOptions,
67
+ ): { status: number | null; stdout: string; stderr: string } {
68
+ const runner = options.commandRunner ?? spawnSync;
69
+ const result = runner(command[0]!, command.slice(1), {
70
+ cwd: options.workspaceRoot,
71
+ encoding: "utf8",
72
+ stdio: ["ignore", "pipe", "pipe"],
73
+ env: {
74
+ ...process.env,
75
+ ...readRealEnv(options.workspaceRoot),
76
+ WORKOS_MODE: process.env.WORKOS_MODE || "agent",
77
+ },
78
+ });
79
+ return {
80
+ status: result.status,
81
+ stdout: typeof result.stdout === "string" ? result.stdout : result.stdout?.toString("utf8") ?? "",
82
+ stderr: typeof result.stderr === "string" ? result.stderr : result.stderr?.toString("utf8") ?? "",
83
+ };
84
+ }
85
+
86
+ function exists(root: string, path: string): boolean {
87
+ return existsSync(join(root, path));
88
+ }
89
+
90
+ function hasWorkOSFgaArtifacts(root: string): boolean {
91
+ return exists(root, `${GENERATED_DIR}/integrations/workos/fga.ts`) ||
92
+ exists(root, `${GENERATED_DIR}/integrations/workos/resource-map.ts`) ||
93
+ exists(root, WORKOS_FGA_STATE_FILE);
94
+ }
95
+
96
+ function readJson(root: string, path: string): unknown | null {
97
+ const absolute = join(root, path);
98
+ if (!existsSync(absolute)) {
99
+ return null;
100
+ }
101
+ return JSON.parse(stripDeterministicHeader(readFileSync(absolute, "utf8")));
102
+ }
103
+
104
+ function readText(root: string, path: string): string {
105
+ const absolute = join(root, path);
106
+ if (!existsSync(absolute)) {
107
+ return "";
108
+ }
109
+ return stripDeterministicHeader(readFileSync(absolute, "utf8"));
110
+ }
111
+
112
+ function readRawText(root: string, path: string): string {
113
+ const absolute = join(root, path);
114
+ if (!existsSync(absolute)) {
115
+ return "";
116
+ }
117
+ return readFileSync(absolute, "utf8");
118
+ }
119
+
120
+ function includesAll(haystack: string, needles: string[]): boolean {
121
+ return needles.every((needle) => haystack.includes(needle));
122
+ }
123
+
124
+ function webAuthSessionProxyConfigured(workspaceRoot: string): boolean {
125
+ const configText = [
126
+ "web/vite.config.ts",
127
+ "web/vite.config.mts",
128
+ "web/vite.config.js",
129
+ "web/vite.config.mjs",
130
+ "web/next.config.ts",
131
+ "web/next.config.mjs",
132
+ "web/next.config.js",
133
+ ].map((path) => readText(workspaceRoot, path)).join("\n");
134
+ if (!configText.trim()) {
135
+ return false;
136
+ }
137
+ return includesAll(configText, ["/login", "/callback", "/logout", "/session"]);
138
+ }
139
+
140
+ function uniqueSorted(values: Iterable<string>): string[] {
141
+ return [...new Set([...values].filter(Boolean))].sort();
142
+ }
143
+
144
+ function quotedValues(text: string): string[] {
145
+ return [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]!).filter(Boolean);
146
+ }
147
+
148
+ export interface WorkOSSeedSummary {
149
+ exists: boolean;
150
+ valid: boolean;
151
+ path: string;
152
+ permissions: string[];
153
+ roles: string[];
154
+ resourceTypes: string[];
155
+ organizations: string[];
156
+ domains: string[];
157
+ redirectUris: string[];
158
+ corsOrigins: string[];
159
+ homepageUrl?: string;
160
+ webhookEndpoints: Array<{ url: string; events: string[] }>;
161
+ diagnostics: string[];
162
+ }
163
+
164
+ export interface WorkOSSeedStateSummary {
165
+ exists: boolean;
166
+ valid: boolean;
167
+ path: string;
168
+ matchesSeedHash: boolean | null;
169
+ seedHash?: string;
170
+ currentSeedHash?: string;
171
+ appliedAt?: string;
172
+ alreadyApplied?: boolean;
173
+ diagnostics: string[];
174
+ }
175
+
176
+ function parseSlug(line: string): string | null {
177
+ const match = /(?:^|\s)-?\s*slug:\s*["']?([^"',\]\s]+)["']?/.exec(line);
178
+ return match?.[1] ?? null;
179
+ }
180
+
181
+ function parseName(line: string): string | null {
182
+ const match = /(?:^|\s)-?\s*name:\s*["']?([^"'\]]+(?:\s[^"'\]]+)*)["']?/.exec(line);
183
+ return match?.[1]?.trim() ?? null;
184
+ }
185
+
186
+ export function parseSeedFile(workspaceRoot: string, preferredPath = DEFAULT_SEED_FILE): WorkOSSeedSummary {
187
+ const seedPath = exists(workspaceRoot, preferredPath)
188
+ ? preferredPath
189
+ : exists(workspaceRoot, GENERATED_SEED_FILE)
190
+ ? GENERATED_SEED_FILE
191
+ : preferredPath;
192
+ const raw = readText(workspaceRoot, seedPath);
193
+ const permissions = new Set<string>();
194
+ const roles = new Set<string>();
195
+ const resourceTypes = new Set<string>();
196
+ const organizations = new Set<string>();
197
+ const domains = new Set<string>();
198
+ const redirectUris = new Set<string>();
199
+ const corsOrigins = new Set<string>();
200
+ let homepageUrl: string | undefined;
201
+ const webhookEndpoints: Array<{ url: string; events: string[] }> = [];
202
+ const diagnostics: string[] = [];
203
+ let section = "";
204
+ let configKey = "";
205
+ let currentWebhook: { url: string; events: string[] } | undefined;
206
+
207
+ if (!raw.trim()) {
208
+ return {
209
+ exists: false,
210
+ valid: false,
211
+ path: seedPath,
212
+ permissions: [],
213
+ roles: [],
214
+ resourceTypes: [],
215
+ organizations: [],
216
+ domains: [],
217
+ redirectUris: [],
218
+ corsOrigins: [],
219
+ webhookEndpoints: [],
220
+ diagnostics: [`${seedPath} is missing or empty`],
221
+ };
222
+ }
223
+
224
+ for (const line of raw.split(/\r?\n/)) {
225
+ const rootSection = /^([a-zA-Z_][\w-]*):\s*$/.exec(line);
226
+ if (rootSection) {
227
+ section = rootSection[1]!;
228
+ configKey = "";
229
+ currentWebhook = undefined;
230
+ continue;
231
+ }
232
+
233
+ if (section === "config") {
234
+ const configEntry = /^\s{2}([a-zA-Z_][\w-]*):\s*(.*)$/.exec(line);
235
+ if (configEntry) {
236
+ configKey = configEntry[1]!;
237
+ const rest = configEntry[2] ?? "";
238
+ if (configKey === "redirect_uris") {
239
+ for (const value of quotedValues(rest)) redirectUris.add(value);
240
+ } else if (configKey === "cors_origins") {
241
+ for (const value of quotedValues(rest)) corsOrigins.add(value);
242
+ } else if (configKey === "homepage_url") {
243
+ homepageUrl = quotedValues(rest)[0] ?? (rest.trim().replace(/^["']|["']$/g, "") || homepageUrl);
244
+ } else if (configKey === "webhook_endpoints") {
245
+ currentWebhook = undefined;
246
+ }
247
+ }
248
+ const bulletValue = /^\s*-\s*["']?([^"'\]\s]+)["']?\s*$/.exec(line)?.[1];
249
+ if (bulletValue && configKey === "redirect_uris") {
250
+ redirectUris.add(bulletValue);
251
+ } else if (bulletValue && configKey === "cors_origins") {
252
+ corsOrigins.add(bulletValue);
253
+ } else if (bulletValue && configKey === "webhook_events" && currentWebhook) {
254
+ currentWebhook.events.push(bulletValue);
255
+ }
256
+ const webhookUrl = /^\s*-\s*url:\s*["']?([^"']+)["']?\s*$/.exec(line)?.[1] ??
257
+ /^\s{4}url:\s*["']?([^"']+)["']?\s*$/.exec(line)?.[1];
258
+ if (webhookUrl) {
259
+ currentWebhook = { url: webhookUrl.trim(), events: [] };
260
+ webhookEndpoints.push(currentWebhook);
261
+ configKey = "webhook_endpoints";
262
+ }
263
+ if (/^\s{4}events:\s*/.test(line) && currentWebhook) {
264
+ configKey = "webhook_events";
265
+ for (const value of quotedValues(line)) currentWebhook.events.push(value);
266
+ }
267
+ }
268
+
269
+ const slug = parseSlug(line);
270
+ if (slug) {
271
+ if (section === "permissions") {
272
+ permissions.add(slug);
273
+ } else if (section === "roles") {
274
+ roles.add(slug);
275
+ } else if (section === "resource_types") {
276
+ resourceTypes.add(slug);
277
+ }
278
+ }
279
+
280
+ if (section === "organizations") {
281
+ const name = parseName(line);
282
+ if (name) {
283
+ organizations.add(name);
284
+ }
285
+ for (const value of quotedValues(line)) {
286
+ if (value.includes(".")) {
287
+ domains.add(value);
288
+ }
289
+ }
290
+ }
291
+
292
+ for (const match of line.matchAll(/-\s*["']?([a-zA-Z0-9_.-]+:[a-zA-Z0-9_.-]+)["']?/g)) {
293
+ permissions.add(match[1]!);
294
+ }
295
+ }
296
+
297
+ if (permissions.size === 0) diagnostics.push("no permission slugs found");
298
+ if (roles.size === 0) diagnostics.push("no role slugs found");
299
+ if (organizations.size === 0) diagnostics.push("no organizations found");
300
+
301
+ return {
302
+ exists: true,
303
+ valid: diagnostics.length === 0,
304
+ path: seedPath,
305
+ permissions: uniqueSorted(permissions),
306
+ roles: uniqueSorted(roles),
307
+ resourceTypes: uniqueSorted(resourceTypes),
308
+ organizations: uniqueSorted(organizations),
309
+ domains: uniqueSorted(domains),
310
+ redirectUris: uniqueSorted(redirectUris),
311
+ corsOrigins: uniqueSorted(corsOrigins),
312
+ ...(homepageUrl ? { homepageUrl } : {}),
313
+ webhookEndpoints: webhookEndpoints.map((endpoint) => ({
314
+ url: endpoint.url,
315
+ events: uniqueSorted(endpoint.events),
316
+ })),
317
+ diagnostics,
318
+ };
319
+ }
320
+
321
+ function parseEnvText(text: string): Record<string, string> {
322
+ const values: Record<string, string> = {};
323
+ for (const line of text.split(/\r?\n/)) {
324
+ const match = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/.exec(line);
325
+ if (!match) continue;
326
+ const raw = match[2] ?? "";
327
+ values[match[1]!] = raw.trim().replace(/^["']|["']$/g, "");
328
+ }
329
+ return values;
330
+ }
331
+
332
+ function readRealEnv(workspaceRoot: string): Record<string, string> {
333
+ return {
334
+ ...parseEnvText(readRawText(workspaceRoot, ".env")),
335
+ ...parseEnvText(readRawText(workspaceRoot, "deploy/.env.production")),
336
+ ...parseEnvText(readRawText(workspaceRoot, ".env.local")),
337
+ ...Object.fromEntries(
338
+ Object.entries(process.env)
339
+ .filter(([key]) => key.startsWith("FORGE_") || key.startsWith("WORKOS_") || key.startsWith("VITE_WORKOS_"))
340
+ .filter((entry): entry is [string, string] => typeof entry[1] === "string"),
341
+ ),
342
+ };
343
+ }
344
+
345
+ function hasValue(env: Record<string, string>, name: string): boolean {
346
+ return typeof env[name] === "string" && env[name]!.trim().length > 0;
347
+ }
348
+
349
+ function workosJwksUri(clientId: string | undefined): string {
350
+ return clientId ? `https://api.workos.com/sso/jwks/${clientId}` : "https://api.workos.com/sso/jwks/<WORKOS_CLIENT_ID>";
351
+ }
352
+
353
+ export interface WorkOSCliAuthSummary {
354
+ required: boolean;
355
+ ok: boolean;
356
+ method: "api-key" | "cli";
357
+ skippedReason?: string;
358
+ statusCommand: string[];
359
+ statusShellCommand?: string;
360
+ loginCommand?: string[];
361
+ loginShellCommand?: string;
362
+ rerunCommand?: string;
363
+ loginAttempted: boolean;
364
+ authenticated?: boolean;
365
+ email?: string;
366
+ userId?: string;
367
+ tokenExpired?: boolean;
368
+ hasRefreshToken?: boolean;
369
+ activeEnvironment?: unknown;
370
+ status?: number | null;
371
+ loginStatus?: number | null;
372
+ detail: string;
373
+ loginInstructions?: {
374
+ url?: string;
375
+ code?: string;
376
+ message?: string;
377
+ };
378
+ nextActions: string[];
379
+ }
380
+
381
+ function parseJsonObject(text: string): Record<string, unknown> | null {
382
+ try {
383
+ const parsed = JSON.parse(text);
384
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
385
+ ? parsed as Record<string, unknown>
386
+ : null;
387
+ } catch {
388
+ return null;
389
+ }
390
+ }
391
+
392
+ function stringField(value: unknown): string | undefined {
393
+ return typeof value === "string" && value.trim() ? value : undefined;
394
+ }
395
+
396
+ function booleanField(value: unknown): boolean | undefined {
397
+ return typeof value === "boolean" ? value : undefined;
398
+ }
399
+
400
+ function extractWorkOSLoginInstructions(stdout: string, stderr: string): WorkOSCliAuthSummary["loginInstructions"] | undefined {
401
+ const output = `${stdout}\n${stderr}`.trim();
402
+ if (!output) {
403
+ return undefined;
404
+ }
405
+ const url = output.match(/https?:\/\/[^\s"'<>]+/)?.[0];
406
+ const code = output.match(/(?:code|verification code|user code)[^\w]*([A-Z0-9-]{4,})/i)?.[1]
407
+ ?? output.match(/\b([A-Z0-9]{4}-[A-Z0-9]{4}|[A-Z0-9]{6,12})\b/)?.[1];
408
+ const message = output
409
+ .split(/\r?\n/)
410
+ .map((line) => line.trim())
411
+ .filter(Boolean)
412
+ .slice(0, 12)
413
+ .join("\n");
414
+ return {
415
+ ...(url ? { url } : {}),
416
+ ...(code ? { code } : {}),
417
+ ...(message ? { message } : {}),
418
+ };
419
+ }
420
+
421
+ function workOSCliAuthCheck(auth: WorkOSCliAuthSummary): WorkOSCheck {
422
+ return {
423
+ name: "workos-cli-auth",
424
+ ok: auth.ok,
425
+ detail: auth.detail,
426
+ };
427
+ }
428
+
429
+ function workOSCliAuthShellCommand(command: string[]): string {
430
+ return `WORKOS_MODE=agent ${command.join(" ")}`;
431
+ }
432
+
433
+ function workOSHostedRerunCommand(options: WorkOSCommandOptions): string {
434
+ const file = options.file ?? DEFAULT_SEED_FILE;
435
+ const subcommand = options.subcommand === "setup" ? "setup" : "prove";
436
+ return `forge workos ${subcommand} --real --file ${file} --json`;
437
+ }
438
+
439
+ function ensureWorkOSCliAuthForHosted(options: WorkOSCommandOptions): WorkOSCliAuthSummary {
440
+ const env = readRealEnv(options.workspaceRoot);
441
+ const statusCommand = ["npx", "--yes", "workos@latest", "auth", "status", "--json"];
442
+ const statusShellCommand = workOSCliAuthShellCommand(statusCommand);
443
+ const rerunCommand = workOSHostedRerunCommand(options);
444
+ if (hasValue(env, "WORKOS_API_KEY")) {
445
+ return {
446
+ required: false,
447
+ ok: true,
448
+ method: "api-key",
449
+ skippedReason: "WORKOS_API_KEY is present; WorkOS CLI browser login is optional for hosted setup",
450
+ statusCommand,
451
+ statusShellCommand,
452
+ loginAttempted: false,
453
+ detail: "WORKOS_API_KEY is present; hosted WorkOS setup can use API key authentication",
454
+ nextActions: [],
455
+ };
456
+ }
457
+
458
+ const status = runExternalCommand(statusCommand, options);
459
+ const parsed = parseJsonObject(status.stdout);
460
+ const authenticated = booleanField(parsed?.authenticated) ?? false;
461
+ const tokenExpired = booleanField(parsed?.tokenExpired) ?? false;
462
+ const hasRefreshToken = booleanField(parsed?.hasRefreshToken) ?? false;
463
+ const email = stringField(parsed?.email);
464
+ const userId = stringField(parsed?.userId);
465
+ const activeEnvironment = parsed?.activeEnvironment;
466
+ if (status.status === 0 && authenticated && (!tokenExpired || hasRefreshToken)) {
467
+ return {
468
+ required: true,
469
+ ok: true,
470
+ method: "cli",
471
+ statusCommand,
472
+ statusShellCommand,
473
+ loginAttempted: false,
474
+ authenticated,
475
+ ...(email ? { email } : {}),
476
+ ...(userId ? { userId } : {}),
477
+ tokenExpired,
478
+ hasRefreshToken,
479
+ ...(activeEnvironment ? { activeEnvironment } : {}),
480
+ status: status.status,
481
+ detail: tokenExpired && hasRefreshToken
482
+ ? `WorkOS CLI is authenticated${email ? ` as ${email}` : ""}; token is expired but a refresh token is available`
483
+ : `WorkOS CLI is authenticated${email ? ` as ${email}` : ""}`,
484
+ nextActions: [],
485
+ };
486
+ }
487
+
488
+ const loginCommand = ["npx", "--yes", "workos@latest", "auth", "login", "--json"];
489
+ const loginShellCommand = workOSCliAuthShellCommand(loginCommand);
490
+ const login = runExternalCommand(loginCommand, options);
491
+ const instructions = extractWorkOSLoginInstructions(login.stdout, login.stderr);
492
+ if (login.status === 0) {
493
+ return {
494
+ required: true,
495
+ ok: true,
496
+ method: "cli",
497
+ statusCommand,
498
+ statusShellCommand,
499
+ loginCommand,
500
+ loginShellCommand,
501
+ loginAttempted: true,
502
+ authenticated: true,
503
+ status: status.status,
504
+ loginStatus: login.status,
505
+ detail: "WorkOS CLI login completed or was already authenticated",
506
+ ...(instructions ? { loginInstructions: instructions } : {}),
507
+ nextActions: [],
508
+ };
509
+ }
510
+
511
+ const nextActions = [
512
+ loginShellCommand,
513
+ "complete the WorkOS CLI OAuth/device-code login shown in loginInstructions",
514
+ `rerun ${rerunCommand}`,
515
+ ];
516
+ return {
517
+ required: true,
518
+ ok: false,
519
+ method: "cli",
520
+ statusCommand,
521
+ statusShellCommand,
522
+ loginCommand,
523
+ loginShellCommand,
524
+ rerunCommand,
525
+ loginAttempted: true,
526
+ authenticated,
527
+ ...(email ? { email } : {}),
528
+ ...(userId ? { userId } : {}),
529
+ tokenExpired,
530
+ hasRefreshToken,
531
+ ...(activeEnvironment ? { activeEnvironment } : {}),
532
+ status: status.status,
533
+ loginStatus: login.status,
534
+ detail: instructions?.url || instructions?.code
535
+ ? "WorkOS CLI login is required; open the URL and enter the code from loginInstructions, then rerun the command"
536
+ : "WorkOS CLI login is required; run WORKOS_MODE=agent npx --yes workos@latest auth login --json and rerun the Forge command",
537
+ ...(instructions ? { loginInstructions: instructions } : {}),
538
+ nextActions,
539
+ };
540
+ }
541
+
542
+ function collectWorkOSRealEnvChecks(workspaceRoot: string, cliAuth?: WorkOSCliAuthSummary): WorkOSCheck[] {
543
+ const env = readRealEnv(workspaceRoot);
544
+ const authMode = env.FORGE_AUTH_MODE;
545
+ const clientId = env.WORKOS_CLIENT_ID || env.VITE_WORKOS_CLIENT_ID;
546
+ const required = [
547
+ ["WORKOS_CLIENT_ID", "WorkOS client ID is required for AuthKit and JWKS discovery"],
548
+ ["WORKOS_COOKIE_PASSWORD", "cookie password is required for AuthKit session signing"],
549
+ ["FORGE_AUTH_ISSUER", "Forge OIDC issuer must be https://api.workos.com"],
550
+ ["FORGE_AUTH_AUDIENCE", "Forge OIDC audience must match the WorkOS client/application audience"],
551
+ ["FORGE_AUTH_JWKS_URI", `Forge JWKS URI must be configured, usually ${workosJwksUri(clientId)}`],
552
+ ] as const;
553
+ return [
554
+ {
555
+ name: "real-env-auth-mode",
556
+ ok: authMode === "oidc" || authMode === "jwt",
557
+ detail: authMode === "oidc" || authMode === "jwt"
558
+ ? `FORGE_AUTH_MODE=${authMode} is production-capable`
559
+ : "FORGE_AUTH_MODE must be oidc or jwt before running hosted WorkOS proof",
560
+ },
561
+ {
562
+ name: "real-env-workos_api_key-or-cli-auth",
563
+ ok: hasValue(env, "WORKOS_API_KEY") || cliAuth?.ok === true,
564
+ detail: hasValue(env, "WORKOS_API_KEY")
565
+ ? "WORKOS_API_KEY is present"
566
+ : cliAuth?.ok
567
+ ? "WORKOS_API_KEY is missing, but WorkOS CLI authentication is available for no-dashboard hosted setup"
568
+ : "WORKOS_API_KEY is missing and WorkOS CLI authentication is not complete; run WorkOS CLI login",
569
+ },
570
+ ...required.map(([name, detail]) => ({
571
+ name: `real-env-${name.toLowerCase()}`,
572
+ ok: hasValue(env, name),
573
+ detail: hasValue(env, name) ? `${name} is present` : `${name} is missing: ${detail}`,
574
+ })),
575
+ ];
576
+ }
577
+
578
+ function renderMergedDotEnv(current: string | null, values: Record<string, string>): string {
579
+ const lines = (current ?? "").split(/\r?\n/);
580
+ const seen = new Set<string>();
581
+ const next = lines.map((line) => {
582
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
583
+ if (!match || !(match[1]! in values)) {
584
+ return line;
585
+ }
586
+ seen.add(match[1]!);
587
+ return `${match[1]}=${values[match[1]!]}`;
588
+ });
589
+ for (const [key, value] of Object.entries(values)) {
590
+ if (!seen.has(key)) {
591
+ next.push(`${key}=${value}`);
592
+ }
593
+ }
594
+ return `${next.filter((line, index, all) => line.length > 0 || index < all.length - 1).join("\n").trimEnd()}\n`;
595
+ }
596
+
597
+ function syncWorkOSPublicWebEnv(workspaceRoot: string, write: boolean, explicitClientId?: string): { ok: boolean; changed: string[]; path?: string; skipped?: string } {
598
+ const frontendWorkspace = findWorkOSFrontendWorkspace(workspaceRoot);
599
+ if (!frontendWorkspace) {
600
+ return { ok: true, changed: [], skipped: "no web workspace detected" };
601
+ }
602
+ const env = readRealEnv(workspaceRoot);
603
+ const clientId = explicitClientId || env.VITE_WORKOS_CLIENT_ID || env.WORKOS_CLIENT_ID || "";
604
+ const redirectUri = env.VITE_WORKOS_REDIRECT_URI || env.WORKOS_REDIRECT_URI || "http://localhost:5173/callback";
605
+ const values: Record<string, string> = {
606
+ VITE_FORGE_URL: env.VITE_FORGE_URL || "http://localhost:3765",
607
+ VITE_WORKOS_REDIRECT_URI: redirectUri,
608
+ };
609
+ if (clientId) {
610
+ values.VITE_WORKOS_CLIENT_ID = clientId;
611
+ }
612
+ const relPath = join(frontendWorkspace, ".env.local");
613
+ const path = join(workspaceRoot, relPath);
614
+ const current = readRawText(workspaceRoot, relPath);
615
+ const next = renderMergedDotEnv(current, values);
616
+ if (current === next) {
617
+ return { ok: true, changed: [], path: relPath };
618
+ }
619
+ if (write) {
620
+ mkdirSync(dirname(path), { recursive: true });
621
+ writeFileSync(path, next, "utf8");
622
+ }
623
+ return { ok: true, changed: [relPath], path: relPath };
624
+ }
625
+
626
+ function workOSDefaultCallback(seed: WorkOSSeedSummary): string {
627
+ return seed.redirectUris.find((uri) => uri.includes("/callback")) ??
628
+ seed.redirectUris[0] ??
629
+ "http://localhost:5173/callback";
630
+ }
631
+
632
+ function workOSDefaultHome(seed: WorkOSSeedSummary): string {
633
+ return seed.homepageUrl ?? seed.corsOrigins[0] ?? "http://localhost:5173";
634
+ }
635
+
636
+ function generatedCookiePassword(): string {
637
+ return randomBytes(32).toString("hex");
638
+ }
639
+
640
+ function readWorkOSCliEnvSummary(options: WorkOSCommandOptions): unknown {
641
+ if (options.skipCliEnv) {
642
+ return { ok: true, skipped: true, message: "WorkOS CLI env list skipped for local status check" };
643
+ }
644
+ const child = runExternalCommand(["npx", "--yes", "workos@latest", "env", "list", "--json"], options);
645
+ const parsed = parseJsonObject(child.stdout);
646
+ return parsed ?? {
647
+ ok: false,
648
+ status: child.status,
649
+ message: child.stderr.trim() || child.stdout.trim() || "failed to read WorkOS CLI env list",
650
+ };
651
+ }
652
+
653
+ function envValueStatus(env: Record<string, string>, key: string): "present" | "missing" {
654
+ return hasValue(env, key) ? "present" : "missing";
655
+ }
656
+
657
+ function workOSEnvNextCommand(clientId?: string): string {
658
+ return clientId
659
+ ? `forge workos env --client-id ${clientId} --write --json`
660
+ : "forge workos env --client-id client_... --write --json";
661
+ }
662
+
663
+ function runWorkOSEnvCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
664
+ const file = options.file ?? DEFAULT_SEED_FILE;
665
+ const seed = parseSeedFile(options.workspaceRoot, file);
666
+ const env = readRealEnv(options.workspaceRoot);
667
+ const clientId = options.clientId || env.WORKOS_CLIENT_ID || env.VITE_WORKOS_CLIENT_ID || "";
668
+ const callback = env.WORKOS_REDIRECT_URI || env.VITE_WORKOS_REDIRECT_URI || workOSDefaultCallback(seed);
669
+ const home = env.WORKOS_POST_LOGIN_REDIRECT_URI || env.WORKOS_POST_LOGOUT_REDIRECT_URI || workOSDefaultHome(seed);
670
+ const rootValues: Record<string, string> = {
671
+ FORGE_AUTH_MODE: env.FORGE_AUTH_MODE || "oidc",
672
+ FORGE_AUTH_ISSUER: env.FORGE_AUTH_ISSUER || "https://api.workos.com",
673
+ WORKOS_REDIRECT_URI: callback,
674
+ WORKOS_POST_LOGIN_REDIRECT_URI: home,
675
+ WORKOS_POST_LOGOUT_REDIRECT_URI: home,
676
+ WORKOS_COOKIE_PASSWORD: env.WORKOS_COOKIE_PASSWORD || generatedCookiePassword(),
677
+ };
678
+ if (clientId) {
679
+ rootValues.WORKOS_CLIENT_ID = clientId;
680
+ rootValues.FORGE_AUTH_AUDIENCE = env.FORGE_AUTH_AUDIENCE || clientId;
681
+ rootValues.FORGE_AUTH_JWKS_URI = env.FORGE_AUTH_JWKS_URI || workosJwksUri(clientId);
682
+ }
683
+
684
+ const rootEnvPath = ".env.local";
685
+ const current = readRawText(options.workspaceRoot, rootEnvPath);
686
+ const next = renderMergedDotEnv(current, rootValues);
687
+ const changed = current !== next ? [rootEnvPath] : [];
688
+ if (options.write && current !== next) {
689
+ writeFileSync(join(options.workspaceRoot, rootEnvPath), next, "utf8");
690
+ }
691
+ const publicEnvSync = syncWorkOSPublicWebEnv(options.workspaceRoot, Boolean(options.write), clientId);
692
+ const missing = [
693
+ ...(!clientId ? ["WORKOS_CLIENT_ID"] : []),
694
+ ...(!clientId ? ["FORGE_AUTH_AUDIENCE"] : []),
695
+ ...(!clientId ? ["FORGE_AUTH_JWKS_URI"] : []),
696
+ ];
697
+ const cliEnv = readWorkOSCliEnvSummary(options);
698
+ const checks: WorkOSCheck[] = [
699
+ {
700
+ name: "env-file",
701
+ ok: options.write ? exists(options.workspaceRoot, rootEnvPath) : true,
702
+ detail: options.write ? `${rootEnvPath} has been written or was already current` : `${rootEnvPath} plan is ready; pass --write to apply`,
703
+ },
704
+ {
705
+ name: "env-auth-mode",
706
+ ok: rootValues.FORGE_AUTH_MODE === "oidc" || rootValues.FORGE_AUTH_MODE === "jwt",
707
+ detail: `FORGE_AUTH_MODE=${rootValues.FORGE_AUTH_MODE}`,
708
+ },
709
+ {
710
+ name: "env-workos-client-id",
711
+ ok: Boolean(clientId),
712
+ detail: clientId
713
+ ? "WORKOS_CLIENT_ID is present and will drive audience/JWKS values"
714
+ : "WORKOS_CLIENT_ID is missing; WorkOS CLI may report hasClientId but does not expose the value to ForgeOS",
715
+ },
716
+ {
717
+ name: "env-cookie-password",
718
+ ok: hasValue(rootValues, "WORKOS_COOKIE_PASSWORD"),
719
+ detail: env.WORKOS_COOKIE_PASSWORD ? "WORKOS_COOKIE_PASSWORD is already present" : "WORKOS_COOKIE_PASSWORD will be generated on write",
720
+ },
721
+ {
722
+ name: "env-forge-issuer",
723
+ ok: rootValues.FORGE_AUTH_ISSUER === "https://api.workos.com",
724
+ detail: `FORGE_AUTH_ISSUER=${rootValues.FORGE_AUTH_ISSUER}`,
725
+ },
726
+ {
727
+ name: "env-forge-audience",
728
+ ok: !missing.includes("FORGE_AUTH_AUDIENCE"),
729
+ detail: missing.includes("FORGE_AUTH_AUDIENCE")
730
+ ? "FORGE_AUTH_AUDIENCE needs WORKOS_CLIENT_ID"
731
+ : "FORGE_AUTH_AUDIENCE will match WORKOS_CLIENT_ID unless already set",
732
+ },
733
+ {
734
+ name: "env-forge-jwks-uri",
735
+ ok: !missing.includes("FORGE_AUTH_JWKS_URI"),
736
+ detail: missing.includes("FORGE_AUTH_JWKS_URI")
737
+ ? "FORGE_AUTH_JWKS_URI needs WORKOS_CLIENT_ID"
738
+ : `FORGE_AUTH_JWKS_URI=${rootValues.FORGE_AUTH_JWKS_URI}`,
739
+ },
740
+ ];
741
+ const ok = checks.every((check) => check.ok);
742
+ return {
743
+ ok,
744
+ kind: "workos-env",
745
+ checks,
746
+ applied: options.write,
747
+ data: {
748
+ file,
749
+ write: options.write,
750
+ envFile: rootEnvPath,
751
+ changed: [...changed, ...publicEnvSync.changed],
752
+ publicEnvSync,
753
+ values: Object.fromEntries(Object.keys(rootValues).map((key) => [
754
+ key,
755
+ key.includes("PASSWORD") || key.includes("API_KEY") ? "<redacted>" : rootValues[key],
756
+ ])),
757
+ requiredStillMissing: missing,
758
+ status: {
759
+ WORKOS_CLIENT_ID: clientId ? "present" : envValueStatus(env, "WORKOS_CLIENT_ID"),
760
+ WORKOS_COOKIE_PASSWORD: env.WORKOS_COOKIE_PASSWORD ? "present" : "generated-on-write",
761
+ FORGE_AUTH_MODE: rootValues.FORGE_AUTH_MODE,
762
+ FORGE_AUTH_AUDIENCE: missing.includes("FORGE_AUTH_AUDIENCE") ? "missing" : "present",
763
+ FORGE_AUTH_JWKS_URI: missing.includes("FORGE_AUTH_JWKS_URI") ? "missing" : "present",
764
+ },
765
+ workosCliEnv: cliEnv,
766
+ nextCommand: ok
767
+ ? `forge auth prove --provider workos --real --file ${file} --json`
768
+ : workOSEnvNextCommand(),
769
+ nextActions: [
770
+ ...(options.write ? [] : [clientId ? workOSEnvNextCommand(clientId) : workOSEnvNextCommand()]),
771
+ ...(!clientId ? ["pass the AuthKit client id with forge workos env --client-id client_... --write --json"] : []),
772
+ ...(ok ? [`forge auth prove --provider workos --real --file ${file} --json`] : []),
773
+ ],
774
+ },
775
+ exitCode: ok ? 0 : 1,
776
+ };
777
+ }
778
+
779
+ function findWorkOSFrontendWorkspace(workspaceRoot: string): string | undefined {
780
+ for (const candidate of ["web", "frontend", "client", "apps/web", "packages/web"]) {
781
+ if (existsSync(join(workspaceRoot, candidate, "package.json"))) {
782
+ return candidate;
783
+ }
784
+ }
785
+ return undefined;
786
+ }
787
+
788
+
789
+ export function collectPolicyPermissions(workspaceRoot: string): string[] {
790
+ const registry = readJson(workspaceRoot, `${GENERATED_DIR}/policyRegistry.json`) as {
791
+ policies?: Array<{ permissions?: string[] }>;
792
+ } | null;
793
+ const permissions = new Set<string>();
794
+ for (const policy of registry?.policies ?? []) {
795
+ for (const permission of policy.permissions ?? []) {
796
+ permissions.add(permission);
797
+ }
798
+ }
799
+ for (const path of ["src/policies.ts", "src/policies.workos.ts"]) {
800
+ const text = readText(workspaceRoot, path);
801
+ for (const match of text.matchAll(/canPermission\s*\(([^)]*)\)/g)) {
802
+ for (const value of quotedValues(match[1] ?? "")) {
803
+ permissions.add(value);
804
+ }
805
+ }
806
+ }
807
+ return uniqueSorted(permissions);
808
+ }
809
+
810
+ function singularResourceName(name: string): string {
811
+ if (name.endsWith("ies")) {
812
+ return `${name.slice(0, -3)}y`;
813
+ }
814
+ if (name.endsWith("ses")) {
815
+ return name.slice(0, -2);
816
+ }
817
+ if (name.endsWith("s") && name.length > 1) {
818
+ return name.slice(0, -1);
819
+ }
820
+ return name;
821
+ }
822
+
823
+ export function collectExpectedResourceTypes(workspaceRoot: string): string[] {
824
+ const dataGraph = readJson(workspaceRoot, `${GENERATED_DIR}/dataGraph.json`) as {
825
+ tables?: Array<{ name?: string; fields?: Array<{ name?: string }> }>;
826
+ } | null;
827
+ const agentContract = readJson(workspaceRoot, `${GENERATED_DIR}/agentContract.json`) as {
828
+ auth?: { requiresTenant?: boolean };
829
+ } | null;
830
+ const resourceTypes = new Set<string>();
831
+ const tenantScopedTables = (dataGraph?.tables ?? []).filter((table) =>
832
+ (table.fields ?? []).some((field) => field.name === "tenantId")
833
+ );
834
+ if (agentContract?.auth?.requiresTenant || tenantScopedTables.length > 0) {
835
+ resourceTypes.add("organization");
836
+ }
837
+ for (const table of tenantScopedTables) {
838
+ if (!table.name || ["organizations", "organization", "memberships", "membership"].includes(table.name)) {
839
+ continue;
840
+ }
841
+ resourceTypes.add(singularResourceName(table.name));
842
+ }
843
+ return uniqueSorted(resourceTypes);
844
+ }
845
+
846
+ export function missingValues(expected: string[], actual: string[]): string[] {
847
+ const actualSet = new Set(actual);
848
+ return expected.filter((value) => !actualSet.has(value));
849
+ }
850
+
851
+ export interface WorkOSFgaResource {
852
+ externalId: string;
853
+ type: string;
854
+ tenant: string;
855
+ name: string;
856
+ parentExternalId?: string;
857
+ parentType?: string;
858
+ }
859
+
860
+ export interface WorkOSFgaProofScenario {
861
+ name: string;
862
+ expected: "allow" | "deny";
863
+ permission: string;
864
+ resourceExternalId: string;
865
+ resourceTypeSlug: string;
866
+ organization: string;
867
+ reason: string;
868
+ }
869
+
870
+ export interface WorkOSFgaManifest {
871
+ schemaVersion: "0.1.0";
872
+ provider: "workos";
873
+ kind: "fga-manifest";
874
+ seedFile: string;
875
+ seedHash?: string;
876
+ manifestHash: string;
877
+ permissions: string[];
878
+ roles: string[];
879
+ resourceTypes: string[];
880
+ organizations: string[];
881
+ resources: WorkOSFgaResource[];
882
+ proofScenarios: WorkOSFgaProofScenario[];
883
+ diagnostics: string[];
884
+ }
885
+
886
+ export interface WorkOSFgaStateSummary {
887
+ exists: boolean;
888
+ valid: boolean;
889
+ path: string;
890
+ matchesManifestHash: boolean | null;
891
+ manifestHash?: string;
892
+ currentManifestHash?: string;
893
+ syncedAt?: string;
894
+ provedAt?: string;
895
+ mode?: "local" | "real";
896
+ sdkOk?: boolean;
897
+ diagnostics: string[];
898
+ }
899
+
900
+ export interface WorkOSFgaMembershipEnvSummary {
901
+ requiredEnv: string[];
902
+ presentEnv: string[];
903
+ missingEnv: string[];
904
+ jsonEnvPresent: boolean;
905
+ complete: boolean;
906
+ }
907
+
908
+ export interface WorkOSFgaHostedSetup {
909
+ requiredResourceTypes: string[];
910
+ rootResourceType: "organization";
911
+ missingResourceTypes: string[];
912
+ requiredMembershipEnv: string[];
913
+ managedBy: "hosted-workos";
914
+ resourceTypeAutomation: "not-supported-by-workos-api" | "not-needed";
915
+ cliSupport: "resources-and-checks";
916
+ sdkSupport: "resources-and-checks";
917
+ docs: string[];
918
+ nextActions: string[];
919
+ }
920
+
921
+ export interface WorkOSFgaResourceTypeSetup {
922
+ slug: string;
923
+ displayName: string;
924
+ hostedAction: "none" | "configure-resource-type";
925
+ requiredBeforeRealSync: boolean;
926
+ permissions: string[];
927
+ roles: string[];
928
+ parentTypes: string[];
929
+ childTypes: string[];
930
+ exampleExternalIds: string[];
931
+ proofScenarios: string[];
932
+ notes: string[];
933
+ }
934
+
935
+ export interface WorkOSFgaSetupGuide {
936
+ schemaVersion: "0.1.0";
937
+ kind: "workos-fga-setup";
938
+ resourceTypes: WorkOSFgaResourceTypeSetup[];
939
+ hostedSetup: WorkOSFgaHostedSetup;
940
+ commands: {
941
+ plan: string;
942
+ syncReal: string;
943
+ proveReal: string;
944
+ deployCheck: string;
945
+ };
946
+ markdown: string;
947
+ docs: string[];
948
+ unsupportedAutomation: string[];
949
+ }
950
+
951
+ export interface WorkOSFgaReadiness {
952
+ real: boolean;
953
+ planReady: boolean;
954
+ seedReady: boolean;
955
+ resourceTypesConfigured: boolean;
956
+ membershipEnvReady: boolean;
957
+ synced: boolean;
958
+ proved: boolean;
959
+ productionReady: boolean;
960
+ nextCommand: string;
961
+ nextActions: string[];
962
+ }
963
+
964
+ function slugifyExternalIdPart(value: string): string {
965
+ return value
966
+ .trim()
967
+ .toLowerCase()
968
+ .replace(/[^a-z0-9]+/g, "-")
969
+ .replace(/^-+|-+$/g, "") || "demo";
970
+ }
971
+
972
+ function preferredParentType(resourceType: string, resourceTypes: string[]): string | undefined {
973
+ const has = (value: string) => resourceTypes.includes(value);
974
+ if (resourceType === "organization") return undefined;
975
+ if (["access_request", "accessRequest", "evidence_document", "evidenceDocument"].includes(resourceType)) {
976
+ if (has("vendor")) return "vendor";
977
+ }
978
+ if (["task", "taskGroup"].includes(resourceType)) {
979
+ if (has("team")) return "team";
980
+ if (has("project")) return "project";
981
+ }
982
+ if (resourceType === "team" && has("project")) return "project";
983
+ return has("organization") ? "organization" : undefined;
984
+ }
985
+
986
+ function stableJson(value: unknown): string {
987
+ if (Array.isArray(value)) {
988
+ return `[${value.map(stableJson).join(",")}]`;
989
+ }
990
+ if (value && typeof value === "object") {
991
+ return `{${Object.entries(value as Record<string, unknown>)
992
+ .sort(([a], [b]) => a.localeCompare(b))
993
+ .map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`)
994
+ .join(",")}}`;
995
+ }
996
+ return JSON.stringify(value);
997
+ }
998
+
999
+ function hashObject(value: unknown): string {
1000
+ return createHash("sha256").update(stableJson(value)).digest("hex");
1001
+ }
1002
+
1003
+ export function collectWorkOSFgaManifest(
1004
+ workspaceRoot: string,
1005
+ preferredSeedPath = DEFAULT_SEED_FILE,
1006
+ ): WorkOSFgaManifest {
1007
+ const seed = parseSeedFile(workspaceRoot, preferredSeedPath);
1008
+ const activePermissions = collectPolicyPermissions(workspaceRoot);
1009
+ const expectedResourceTypes = collectExpectedResourceTypes(workspaceRoot);
1010
+ const permissions = uniqueSorted([...seed.permissions, ...activePermissions]);
1011
+ const resourceTypes = uniqueSorted([...seed.resourceTypes, ...expectedResourceTypes]);
1012
+ const organizations = seed.organizations.length > 0 ? seed.organizations : ["Demo Organization"];
1013
+ const diagnostics: string[] = [];
1014
+ const missingSeedPermissions = missingValues(activePermissions, seed.permissions);
1015
+ const missingSeedResources = missingValues(expectedResourceTypes, seed.resourceTypes);
1016
+ if (!seed.exists) diagnostics.push(`${preferredSeedPath} is missing; run forge add auth workos or forge workos seed --dry-run first`);
1017
+ if (missingSeedPermissions.length > 0) diagnostics.push(`seed missing active policy permission(s): ${missingSeedPermissions.join(", ")}`);
1018
+ if (missingSeedResources.length > 0) diagnostics.push(`seed missing app resource type(s): ${missingSeedResources.join(", ")}`);
1019
+ if (!resourceTypes.includes("organization")) diagnostics.push("FGA graph should include an organization resource type for tenant roots");
1020
+ if (permissions.length === 0) diagnostics.push("no WorkOS permission slugs were discovered");
1021
+ if (seed.roles.length === 0) diagnostics.push("no WorkOS roles were discovered");
1022
+
1023
+ const resources: WorkOSFgaResource[] = [];
1024
+ for (const organization of organizations) {
1025
+ const orgSlug = slugifyExternalIdPart(organization);
1026
+ const orgExternalId = `organization:${orgSlug}`;
1027
+ if (resourceTypes.includes("organization")) {
1028
+ resources.push({
1029
+ externalId: orgExternalId,
1030
+ type: "organization",
1031
+ tenant: organization,
1032
+ name: organization,
1033
+ });
1034
+ }
1035
+ for (const resourceType of resourceTypes.filter((type) => type !== "organization")) {
1036
+ const parentType = preferredParentType(resourceType, resourceTypes);
1037
+ const parentExternalId = parentType
1038
+ ? parentType === "organization"
1039
+ ? orgExternalId
1040
+ : `${parentType}:${orgSlug}:demo`
1041
+ : undefined;
1042
+ resources.push({
1043
+ externalId: `${resourceType}:${orgSlug}:demo`,
1044
+ type: resourceType,
1045
+ tenant: organization,
1046
+ name: `${organization} ${resourceType}`,
1047
+ ...(parentType ? { parentType } : {}),
1048
+ ...(parentExternalId ? { parentExternalId } : {}),
1049
+ });
1050
+ }
1051
+ }
1052
+
1053
+ const firstOrg = organizations[0] ?? "Demo Organization";
1054
+ const secondOrg = organizations[1] ?? `${firstOrg} Other`;
1055
+ const firstResource = resources.find((resource) => resource.type !== "organization") ?? resources[0];
1056
+ const firstPermission = permissions[0] ?? "app:read";
1057
+ const proofScenarios: WorkOSFgaProofScenario[] = firstResource
1058
+ ? [
1059
+ {
1060
+ name: "allowed-same-tenant",
1061
+ expected: "allow",
1062
+ permission: firstPermission,
1063
+ resourceExternalId: firstResource.externalId,
1064
+ resourceTypeSlug: firstResource.type,
1065
+ organization: firstOrg,
1066
+ reason: "membership, permission, and resource tenant match",
1067
+ },
1068
+ {
1069
+ name: "cross-tenant-read-denied",
1070
+ expected: "deny",
1071
+ permission: firstPermission,
1072
+ resourceExternalId: firstResource.externalId,
1073
+ resourceTypeSlug: firstResource.type,
1074
+ organization: secondOrg,
1075
+ reason: "resource belongs to a different organization tenant",
1076
+ },
1077
+ ]
1078
+ : [];
1079
+
1080
+ const hashInput = {
1081
+ seedFile: seed.path,
1082
+ seedHash: seed.exists ? hashSeedFile(workspaceRoot, seed.path) : undefined,
1083
+ permissions,
1084
+ roles: seed.roles,
1085
+ resourceTypes,
1086
+ organizations,
1087
+ resources,
1088
+ proofScenarios,
1089
+ };
1090
+ return {
1091
+ schemaVersion: "0.1.0",
1092
+ provider: "workos",
1093
+ kind: "fga-manifest",
1094
+ seedFile: seed.path,
1095
+ ...(seed.exists ? { seedHash: hashSeedFile(workspaceRoot, seed.path) } : {}),
1096
+ manifestHash: hashObject(hashInput),
1097
+ permissions,
1098
+ roles: seed.roles,
1099
+ resourceTypes,
1100
+ organizations,
1101
+ resources,
1102
+ proofScenarios,
1103
+ diagnostics,
1104
+ };
1105
+ }
1106
+
1107
+ export function readWorkOSFgaState(
1108
+ workspaceRoot: string,
1109
+ manifest: WorkOSFgaManifest,
1110
+ ): WorkOSFgaStateSummary {
1111
+ const path = WORKOS_FGA_STATE_FILE;
1112
+ const absolute = join(workspaceRoot, path);
1113
+ if (!existsSync(absolute)) {
1114
+ return {
1115
+ exists: false,
1116
+ valid: false,
1117
+ path,
1118
+ matchesManifestHash: null,
1119
+ currentManifestHash: manifest.manifestHash,
1120
+ diagnostics: [`${path} is missing; run forge workos fga sync --json`],
1121
+ };
1122
+ }
1123
+ try {
1124
+ const parsed = JSON.parse(readFileSync(absolute, "utf8")) as {
1125
+ manifestHash?: unknown;
1126
+ syncedAt?: unknown;
1127
+ provedAt?: unknown;
1128
+ mode?: unknown;
1129
+ sdkOk?: unknown;
1130
+ };
1131
+ const diagnostics: string[] = [];
1132
+ const manifestHash = typeof parsed.manifestHash === "string" ? parsed.manifestHash : undefined;
1133
+ const mode = parsed.mode === "real" ? "real" : parsed.mode === "local" ? "local" : undefined;
1134
+ const sdkOk = typeof parsed.sdkOk === "boolean" ? parsed.sdkOk : undefined;
1135
+ if (!manifestHash) diagnostics.push("FGA state is missing manifestHash");
1136
+ if (typeof parsed.syncedAt !== "string") diagnostics.push("FGA state is missing syncedAt");
1137
+ if (!mode) diagnostics.push("FGA state is missing mode");
1138
+ if (mode === "real" && sdkOk !== true) diagnostics.push("real FGA state is missing sdkOk:true from WorkOS Authorization API sync/proof");
1139
+ return {
1140
+ exists: true,
1141
+ valid: diagnostics.length === 0,
1142
+ path,
1143
+ matchesManifestHash: manifestHash ? manifestHash === manifest.manifestHash : null,
1144
+ ...(manifestHash ? { manifestHash } : {}),
1145
+ currentManifestHash: manifest.manifestHash,
1146
+ ...(typeof parsed.syncedAt === "string" ? { syncedAt: parsed.syncedAt } : {}),
1147
+ ...(typeof parsed.provedAt === "string" ? { provedAt: parsed.provedAt } : {}),
1148
+ ...(mode ? { mode } : {}),
1149
+ ...(sdkOk !== undefined ? { sdkOk } : {}),
1150
+ diagnostics,
1151
+ };
1152
+ } catch (error) {
1153
+ return {
1154
+ exists: true,
1155
+ valid: false,
1156
+ path,
1157
+ matchesManifestHash: null,
1158
+ currentManifestHash: manifest.manifestHash,
1159
+ diagnostics: [`failed to parse ${path}: ${error instanceof Error ? error.message : String(error)}`],
1160
+ };
1161
+ }
1162
+ }
1163
+
1164
+ function writeWorkOSFgaState(input: {
1165
+ workspaceRoot: string;
1166
+ manifest: WorkOSFgaManifest;
1167
+ mode: "local" | "real";
1168
+ proved: boolean;
1169
+ sdkOk?: boolean;
1170
+ sdk?: unknown;
1171
+ }): string {
1172
+ const now = new Date().toISOString();
1173
+ const payload = {
1174
+ schemaVersion: "0.1.0",
1175
+ provider: "workos",
1176
+ kind: "fga-state",
1177
+ mode: input.mode,
1178
+ seedFile: input.manifest.seedFile,
1179
+ seedHash: input.manifest.seedHash,
1180
+ manifestHash: input.manifest.manifestHash,
1181
+ syncedAt: now,
1182
+ ...(input.proved ? { provedAt: now } : {}),
1183
+ ...(input.sdkOk !== undefined ? { sdkOk: input.sdkOk } : {}),
1184
+ ...(input.sdk ? { sdk: input.sdk } : {}),
1185
+ permissions: input.manifest.permissions,
1186
+ roles: input.manifest.roles,
1187
+ resourceTypes: input.manifest.resourceTypes,
1188
+ organizations: input.manifest.organizations,
1189
+ resources: input.manifest.resources,
1190
+ proofScenarios: input.manifest.proofScenarios,
1191
+ };
1192
+ writeFileSync(join(input.workspaceRoot, WORKOS_FGA_STATE_FILE), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
1193
+ return WORKOS_FGA_STATE_FILE;
1194
+ }
1195
+
1196
+ function hostedFgaResourceTypes(manifest: WorkOSFgaManifest): string[] {
1197
+ return manifest.resourceTypes.filter((resourceType) => resourceType !== "organization");
1198
+ }
1199
+
1200
+ function fgaMembershipEnvKey(organization: string): string {
1201
+ const suffix = slugifyExternalIdPart(organization).replace(/-/g, "_").toUpperCase();
1202
+ return `WORKOS_FGA_MEMBERSHIP_${suffix}`;
1203
+ }
1204
+
1205
+ function workOSFgaMembershipEnvSummary(
1206
+ workspaceRoot: string,
1207
+ organizations: string[],
1208
+ ): WorkOSFgaMembershipEnvSummary {
1209
+ const env = readRealEnv(workspaceRoot);
1210
+ const requiredEnv = organizations.map(fgaMembershipEnvKey);
1211
+ const jsonEnvPresent = hasValue(env, "WORKOS_FGA_MEMBERSHIPS_JSON") || hasValue(env, "WORKOS_FGA_TEST_MEMBERSHIPS");
1212
+ const presentEnv = requiredEnv.filter((name) => hasValue(env, name));
1213
+ const missingEnv = jsonEnvPresent ? [] : requiredEnv.filter((name) => !presentEnv.includes(name));
1214
+ return {
1215
+ requiredEnv,
1216
+ presentEnv,
1217
+ missingEnv,
1218
+ jsonEnvPresent,
1219
+ complete: jsonEnvPresent || missingEnv.length === 0,
1220
+ };
1221
+ }
1222
+
1223
+ function extractMissingWorkOSFgaResourceTypes(data: unknown, manifest?: WorkOSFgaManifest): string[] {
1224
+ const missing = new Set<string>();
1225
+ const add = (value: unknown) => {
1226
+ if (typeof value === "string" && value && value !== "organization") {
1227
+ missing.add(value);
1228
+ }
1229
+ };
1230
+ if (data && typeof data === "object") {
1231
+ const errors = (data as { errors?: unknown }).errors;
1232
+ if (Array.isArray(errors)) {
1233
+ for (const error of errors) {
1234
+ if (!error || typeof error !== "object") continue;
1235
+ const record = error as Record<string, unknown>;
1236
+ const message = typeof record.message === "string" ? record.message : "";
1237
+ const status = String(record.status ?? "");
1238
+ if (message.includes("AuthorizationResourceType not found") || status === "404") {
1239
+ add(record.resourceTypeSlug);
1240
+ }
1241
+ const match = /AuthorizationResourceType not found:\s*['"]([^'"]+)['"]/.exec(message);
1242
+ add(match?.[1]);
1243
+ }
1244
+ }
1245
+ }
1246
+ const text = JSON.stringify(data) ?? "";
1247
+ const missingTypePattern = /AuthorizationResourceType not found:\s*['"]([^'"]+)['"]/g;
1248
+ let match: RegExpExecArray | null;
1249
+ while ((match = missingTypePattern.exec(text))) {
1250
+ add(match[1]);
1251
+ }
1252
+ if (missing.size === 0 && text.includes("AuthorizationResourceType not found") && manifest) {
1253
+ for (const resourceType of hostedFgaResourceTypes(manifest)) {
1254
+ missing.add(resourceType);
1255
+ }
1256
+ }
1257
+ return uniqueSorted(missing);
1258
+ }
1259
+
1260
+ function workOSFgaHostedSetup(manifest: WorkOSFgaManifest, sdkData?: unknown): WorkOSFgaHostedSetup {
1261
+ const requiredResourceTypes = hostedFgaResourceTypes(manifest);
1262
+ const missingResourceTypes = extractMissingWorkOSFgaResourceTypes(sdkData, manifest);
1263
+ const resourceList = (missingResourceTypes.length > 0 ? missingResourceTypes : requiredResourceTypes).join(", ") || "none";
1264
+ const requiredMembershipEnv = manifest.organizations.map(fgaMembershipEnvKey);
1265
+ return {
1266
+ requiredResourceTypes,
1267
+ rootResourceType: "organization",
1268
+ missingResourceTypes,
1269
+ requiredMembershipEnv,
1270
+ managedBy: "hosted-workos",
1271
+ resourceTypeAutomation: requiredResourceTypes.length > 0 ? "not-supported-by-workos-api" : "not-needed",
1272
+ cliSupport: "resources-and-checks",
1273
+ sdkSupport: "resources-and-checks",
1274
+ docs: [
1275
+ "https://workos.com/docs/fga/resource-types",
1276
+ "https://workos.com/docs/fga/resources",
1277
+ "https://workos.com/docs/fga/access-checks",
1278
+ ],
1279
+ nextActions: [
1280
+ missingResourceTypes.length > 0
1281
+ ? `configure missing WorkOS FGA resource type(s): ${resourceList}`
1282
+ : `confirm WorkOS FGA resource type(s) exist: ${resourceList}`,
1283
+ "treat organization as the WorkOS tenant root; ForgeOS does not create it as an authorization resource",
1284
+ `set WorkOS FGA membership env for real access checks: WORKOS_FGA_MEMBERSHIPS_JSON or ${requiredMembershipEnv.join(", ") || "WORKOS_FGA_MEMBERSHIP_<ORG>"}`,
1285
+ "rerun forge workos fga sync --real --file workos-seed.yml --json",
1286
+ "rerun forge workos fga prove --real --file workos-seed.yml --json",
1287
+ "rerun forge deploy check --production --json",
1288
+ ],
1289
+ };
1290
+ }
1291
+
1292
+ function displayResourceType(slug: string): string {
1293
+ return slug
1294
+ .replace(/[_-]+/g, " ")
1295
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
1296
+ }
1297
+
1298
+ function permissionResourcePart(permission: string): string {
1299
+ return permission.split(":")[0] ?? permission;
1300
+ }
1301
+
1302
+ function permissionMatchesResourceType(permission: string, resourceType: string): boolean {
1303
+ const part = permissionResourcePart(permission);
1304
+ const normalizedPart = singularResourceName(part.replace(/[-_]/g, ""));
1305
+ const normalizedType = singularResourceName(resourceType.replace(/[-_]/g, ""));
1306
+ return part === resourceType ||
1307
+ singularResourceName(part) === resourceType ||
1308
+ normalizedPart === normalizedType ||
1309
+ normalizedType.startsWith(normalizedPart) ||
1310
+ normalizedPart === `${normalizedType}request`;
1311
+ }
1312
+
1313
+ function permissionsForResourceType(manifest: WorkOSFgaManifest, resourceType: string): string[] {
1314
+ if (resourceType === "organization") {
1315
+ return manifest.permissions.filter((permission) => permissionResourcePart(permission) === "organization");
1316
+ }
1317
+ return manifest.permissions.filter((permission) => permissionMatchesResourceType(permission, resourceType));
1318
+ }
1319
+
1320
+ function roleLooksRelevantToResourceType(role: string, resourceType: string): boolean {
1321
+ const normalizedRole = role.replace(/[-_]/g, "");
1322
+ const normalizedType = resourceType.replace(/[-_]/g, "");
1323
+ return normalizedRole.includes(normalizedType) ||
1324
+ normalizedRole.includes(singularResourceName(normalizedType)) ||
1325
+ ["owner", "admin", "manager", "member", "auditor", "reviewer", "requester", "security"].some((shared) => normalizedRole.includes(shared));
1326
+ }
1327
+
1328
+ function rolesForResourceType(manifest: WorkOSFgaManifest, resourceType: string): string[] {
1329
+ const relevant = manifest.roles.filter((role) => roleLooksRelevantToResourceType(role, resourceType));
1330
+ return relevant.length > 0 ? relevant : manifest.roles;
1331
+ }
1332
+
1333
+ function workOSFgaSetupGuide(manifest: WorkOSFgaManifest, hostedSetup: WorkOSFgaHostedSetup): WorkOSFgaSetupGuide {
1334
+ const resourcesByType = new Map<string, WorkOSFgaResource[]>();
1335
+ for (const resource of manifest.resources) {
1336
+ resourcesByType.set(resource.type, [...resourcesByType.get(resource.type) ?? [], resource]);
1337
+ }
1338
+ const resourceTypes = manifest.resourceTypes.map((slug): WorkOSFgaResourceTypeSetup => {
1339
+ const resources = resourcesByType.get(slug) ?? [];
1340
+ const parentTypes = uniqueSorted(resources.map((resource) => resource.parentType ?? "").filter(Boolean));
1341
+ const childTypes = uniqueSorted(manifest.resources
1342
+ .filter((resource) => resource.parentType === slug)
1343
+ .map((resource) => resource.type));
1344
+ const proofScenarios = manifest.proofScenarios
1345
+ .filter((scenario) => scenario.resourceTypeSlug === slug)
1346
+ .map((scenario) => `${scenario.name}:${scenario.expected}`);
1347
+ const permissions = permissionsForResourceType(manifest, slug);
1348
+ const roles = rolesForResourceType(manifest, slug);
1349
+ const isRoot = slug === hostedSetup.rootResourceType;
1350
+ return {
1351
+ slug,
1352
+ displayName: displayResourceType(slug),
1353
+ hostedAction: isRoot ? "none" : "configure-resource-type",
1354
+ requiredBeforeRealSync: !isRoot,
1355
+ permissions,
1356
+ roles,
1357
+ parentTypes,
1358
+ childTypes,
1359
+ exampleExternalIds: resources.map((resource) => resource.externalId).slice(0, 4),
1360
+ proofScenarios,
1361
+ notes: isRoot
1362
+ ? [
1363
+ "Treat WorkOS organization as the tenant root.",
1364
+ "ForgeOS keeps organization in the graph for parent/tenant reasoning, but real sync does not create it as an authorization resource.",
1365
+ ]
1366
+ : [
1367
+ "Create/configure this resource type in hosted WorkOS before real sync.",
1368
+ parentTypes.length > 0
1369
+ ? `Expected parent type(s): ${parentTypes.join(", ")}.`
1370
+ : "No parent type inferred from the app graph.",
1371
+ ],
1372
+ };
1373
+ });
1374
+ const markdown = [
1375
+ "# WorkOS FGA Setup",
1376
+ "",
1377
+ "ForgeOS derived this resource graph from the app contract, policies, and workos-seed.yml.",
1378
+ "WorkOS resource type configuration is hosted WorkOS configuration. ForgeOS uses the WorkOS CLI/API or SDK to sync resources and prove authorization checks after those resource types exist.",
1379
+ "",
1380
+ "## Automation Boundary",
1381
+ "",
1382
+ "- ForgeOS can derive the FGA graph, generate this setup guide, sync WorkOS FGA resources, and run Authorization API access checks.",
1383
+ "- ForgeOS does not invent WorkOS CLI/API calls for resource type creation. The current public WorkOS automation surface supports resources and checks after resource types exist.",
1384
+ "- Configure each non-organization resource type below in hosted WorkOS, then rerun the real sync/proof commands.",
1385
+ "- The `organization` node is treated as the WorkOS tenant root and is not created as an authorization resource.",
1386
+ "",
1387
+ "## Resource Types",
1388
+ "",
1389
+ ...resourceTypes.flatMap((resourceType) => [
1390
+ `### ${resourceType.slug}`,
1391
+ "",
1392
+ `- Display name: ${resourceType.displayName}`,
1393
+ `- Hosted action: ${resourceType.hostedAction}`,
1394
+ `- Required before real sync: ${resourceType.requiredBeforeRealSync ? "yes" : "no"}`,
1395
+ `- Parent types: ${resourceType.parentTypes.join(", ") || "none"}`,
1396
+ `- Child types: ${resourceType.childTypes.join(", ") || "none"}`,
1397
+ `- Permissions to attach/model: ${resourceType.permissions.join(", ") || "none inferred"}`,
1398
+ `- Roles to review for this type: ${resourceType.roles.join(", ") || "none inferred"}`,
1399
+ `- Example external IDs: ${resourceType.exampleExternalIds.join(", ") || "none"}`,
1400
+ `- Proof scenarios: ${resourceType.proofScenarios.join(", ") || "none"}`,
1401
+ ...resourceType.notes.map((note) => `- Note: ${note}`),
1402
+ "",
1403
+ ]),
1404
+ "## Permission And Role Coverage",
1405
+ "",
1406
+ `- Permissions discovered: ${manifest.permissions.join(", ") || "none"}`,
1407
+ `- Roles discovered: ${manifest.roles.join(", ") || "none"}`,
1408
+ "- In hosted WorkOS, ensure each permission is scoped to the intended resource type and each role includes the permissions needed by your Forge policies.",
1409
+ "- ForgeOS will not claim production readiness until real Authorization API checks pass for the generated proof scenarios.",
1410
+ "",
1411
+ "## Required Membership Environment",
1412
+ "",
1413
+ "- WORKOS_FGA_MEMBERSHIPS_JSON: JSON object mapping organization name to organizationMembershipId",
1414
+ ...hostedSetup.requiredMembershipEnv.map((env) => `- ${env}: organizationMembershipId for that organization`),
1415
+ "",
1416
+ "## Commands",
1417
+ "",
1418
+ "```bash",
1419
+ "forge workos fga plan --file workos-seed.yml --write --json",
1420
+ "forge workos fga sync --real --file workos-seed.yml --json",
1421
+ "forge workos fga prove --real --file workos-seed.yml --json",
1422
+ "forge deploy check --production --json",
1423
+ "```",
1424
+ "",
1425
+ ].join("\n");
1426
+ return {
1427
+ schemaVersion: "0.1.0",
1428
+ kind: "workos-fga-setup",
1429
+ resourceTypes,
1430
+ hostedSetup,
1431
+ commands: {
1432
+ plan: "forge workos fga plan --file workos-seed.yml --write --json",
1433
+ syncReal: "forge workos fga sync --real --file workos-seed.yml --json",
1434
+ proveReal: "forge workos fga prove --real --file workos-seed.yml --json",
1435
+ deployCheck: "forge deploy check --production --json",
1436
+ },
1437
+ markdown,
1438
+ docs: hostedSetup.docs,
1439
+ unsupportedAutomation: [
1440
+ "ForgeOS does not invent WorkOS CLI/API calls for resource type creation.",
1441
+ "ForgeOS can create/read FGA resources and run authorization checks through the WorkOS CLI/API or SDK after resource types exist.",
1442
+ ],
1443
+ };
1444
+ }
1445
+
1446
+ function resolveWorkOSFgaSetupGuidePath(options: WorkOSCommandOptions): string | undefined {
1447
+ if (!options.write && !options.writePath) {
1448
+ return undefined;
1449
+ }
1450
+ const candidate = options.writePath?.trim();
1451
+ return candidate && candidate !== "true" && !candidate.startsWith("--") ? candidate : WORKOS_FGA_SETUP_GUIDE_FILE;
1452
+ }
1453
+
1454
+ function workOSFgaSetupPlanPath(markdownPath: string): string {
1455
+ if (markdownPath === WORKOS_FGA_SETUP_GUIDE_FILE) {
1456
+ return WORKOS_FGA_SETUP_PLAN_FILE;
1457
+ }
1458
+ return markdownPath.endsWith(".md")
1459
+ ? `${markdownPath.slice(0, -3)}.json`
1460
+ : `${markdownPath}.json`;
1461
+ }
1462
+
1463
+ function writeWorkOSFgaSetupGuide(
1464
+ workspaceRoot: string,
1465
+ guide: WorkOSFgaSetupGuide,
1466
+ preferredPath = WORKOS_FGA_SETUP_GUIDE_FILE,
1467
+ ): { markdownPath: string; jsonPath: string } {
1468
+ const relativePath = preferredPath.startsWith("/") ? preferredPath.slice(1) : preferredPath;
1469
+ const absolutePath = join(workspaceRoot, relativePath);
1470
+ mkdirSync(dirname(absolutePath), { recursive: true });
1471
+ writeFileSync(absolutePath, guide.markdown, "utf8");
1472
+ const jsonPath = workOSFgaSetupPlanPath(relativePath);
1473
+ const absoluteJsonPath = join(workspaceRoot, jsonPath);
1474
+ mkdirSync(dirname(absoluteJsonPath), { recursive: true });
1475
+ const { markdown: _markdown, ...jsonGuide } = guide;
1476
+ writeFileSync(absoluteJsonPath, `${JSON.stringify({
1477
+ ...jsonGuide,
1478
+ markdownPath: relativePath,
1479
+ jsonPath,
1480
+ }, null, 2)}\n`, "utf8");
1481
+ return { markdownPath: relativePath, jsonPath };
1482
+ }
1483
+
1484
+ function fgaData(input: {
1485
+ action: WorkOSFgaAction;
1486
+ workspaceRoot: string;
1487
+ manifest: WorkOSFgaManifest;
1488
+ state: WorkOSFgaStateSummary;
1489
+ seedState?: WorkOSSeedStateSummary;
1490
+ real?: boolean;
1491
+ cliAuth?: WorkOSCliAuthSummary;
1492
+ workosSdk?: unknown;
1493
+ readiness?: WorkOSFgaReadiness;
1494
+ stateFile?: string;
1495
+ nextCommand?: string;
1496
+ nextActions?: string[];
1497
+ setupGuidePath?: string;
1498
+ setupGuideJsonPath?: string;
1499
+ }): Record<string, unknown> {
1500
+ const hostedSetup = workOSFgaHostedSetup(input.manifest, input.workosSdk);
1501
+ const setupGuide = workOSFgaSetupGuide(input.manifest, hostedSetup);
1502
+ const membershipEnv = workOSFgaMembershipEnvSummary(input.workspaceRoot, input.manifest.organizations);
1503
+ return {
1504
+ action: input.action,
1505
+ real: input.real ?? false,
1506
+ manifest: input.manifest,
1507
+ state: input.state,
1508
+ ...(input.seedState ? { seedState: input.seedState } : {}),
1509
+ ...(input.readiness ? { readiness: input.readiness } : {}),
1510
+ hostedSetup,
1511
+ membershipEnv,
1512
+ resourceTypeSetup: setupGuide.resourceTypes,
1513
+ setupGuide,
1514
+ ...(input.cliAuth ? { cliAuth: input.cliAuth } : {}),
1515
+ ...(input.workosSdk ? { workosSdk: input.workosSdk } : {}),
1516
+ ...(input.stateFile ? { stateFile: input.stateFile } : {}),
1517
+ ...(input.setupGuidePath ? { setupGuidePath: input.setupGuidePath } : {}),
1518
+ ...(input.setupGuideJsonPath ? { setupGuideJsonPath: input.setupGuideJsonPath } : {}),
1519
+ nextCommand: input.nextCommand,
1520
+ nextActions: input.nextActions ?? hostedSetup.nextActions,
1521
+ notes: [
1522
+ "ForgeOS derives resource graph and proof scenarios from app contract, policies, and workos-seed.yml.",
1523
+ "WorkOS FGA resource types are hosted WorkOS configuration; ForgeOS syncs/proves resources and gates production deploys through .workos-fga-state.json.",
1524
+ ],
1525
+ };
1526
+ }
1527
+
1528
+ function runWorkOSFgaSdk(
1529
+ options: WorkOSCommandOptions,
1530
+ manifest: WorkOSFgaManifest,
1531
+ action: "sync" | "prove",
1532
+ ): { ok: boolean; command: string[]; data: Record<string, unknown>; status: number | null; stdout?: string; stderr?: string } {
1533
+ const script = String.raw`
1534
+ const { spawnSync } = await import("node:child_process");
1535
+ const payload = JSON.parse(process.env.FORGE_WORKOS_FGA_PAYLOAD || "{}");
1536
+ const out = { ok: true, action: payload.action, resources: [], checks: [], skipped: [], errors: [] };
1537
+ function message(error) {
1538
+ return error && typeof error === "object" && "message" in error ? String(error.message) : String(error);
1539
+ }
1540
+ function status(error) {
1541
+ return error && typeof error === "object" && "status" in error ? error.status : error && typeof error === "object" && "statusCode" in error ? error.statusCode : undefined;
1542
+ }
1543
+ async function listAll(page) {
1544
+ if (!page) return [];
1545
+ if (Array.isArray(page.data)) return page.data;
1546
+ if (Symbol.asyncIterator in Object(page)) {
1547
+ const values = [];
1548
+ for await (const item of page) values.push(item);
1549
+ return values;
1550
+ }
1551
+ return [];
1552
+ }
1553
+ function slugify(value) {
1554
+ return String(value || "")
1555
+ .trim()
1556
+ .toLowerCase()
1557
+ .replace(/[^a-z0-9]+/g, "-")
1558
+ .replace(/^-+|-+$/g, "") || "demo";
1559
+ }
1560
+ function membershipEnvKey(organization) {
1561
+ return "WORKOS_FGA_MEMBERSHIP_" + slugify(organization).replace(/-/g, "_").toUpperCase();
1562
+ }
1563
+ function readMembershipMap(organizations) {
1564
+ const memberships = new Map();
1565
+ for (const name of ["WORKOS_FGA_MEMBERSHIPS_JSON", "WORKOS_FGA_TEST_MEMBERSHIPS"]) {
1566
+ const raw = process.env[name];
1567
+ if (!raw) continue;
1568
+ try {
1569
+ const parsed = JSON.parse(raw);
1570
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1571
+ for (const [organization, membershipId] of Object.entries(parsed)) {
1572
+ if (typeof membershipId === "string" && membershipId.trim()) {
1573
+ memberships.set(organization, membershipId.trim());
1574
+ memberships.set(slugify(organization), membershipId.trim());
1575
+ }
1576
+ }
1577
+ }
1578
+ } catch (error) {
1579
+ out.ok = false;
1580
+ out.errors.push({ operation: "membership.env", env: name, message: "failed to parse JSON membership map: " + message(error) });
1581
+ }
1582
+ }
1583
+ for (const organization of organizations || []) {
1584
+ const envKey = membershipEnvKey(organization);
1585
+ const membershipId = process.env[envKey];
1586
+ if (membershipId && membershipId.trim()) {
1587
+ memberships.set(organization, membershipId.trim());
1588
+ memberships.set(slugify(organization), membershipId.trim());
1589
+ }
1590
+ }
1591
+ const legacy = process.env.WORKOS_FGA_TEST_MEMBERSHIP_ID || process.env.WORKOS_FGA_ORGANIZATION_MEMBERSHIP_ID;
1592
+ if (legacy && legacy.trim() && organizations && organizations.length === 1) {
1593
+ memberships.set(organizations[0], legacy.trim());
1594
+ memberships.set(slugify(organizations[0]), legacy.trim());
1595
+ }
1596
+ return memberships;
1597
+ }
1598
+ function parseCliJson(text) {
1599
+ try {
1600
+ const parsed = JSON.parse(text || "{}");
1601
+ return parsed && typeof parsed === "object" ? parsed : {};
1602
+ } catch (error) {
1603
+ return { parseError: message(error), raw: text };
1604
+ }
1605
+ }
1606
+ function cliApi(path, options = {}) {
1607
+ const args = ["--yes", "workos@latest", "api", path, "--method", options.method || "GET", "--json"];
1608
+ if (options.data) args.push("--data", JSON.stringify(options.data));
1609
+ if (options.yes) args.push("--yes");
1610
+ const child = spawnSync("npx", args, {
1611
+ encoding: "utf8",
1612
+ env: { ...process.env, WORKOS_MODE: process.env.WORKOS_MODE || "agent" },
1613
+ });
1614
+ const parsed = parseCliJson(child.stdout);
1615
+ return {
1616
+ ok: child.status === 0,
1617
+ status: child.status,
1618
+ data: parsed,
1619
+ stdout: child.stdout,
1620
+ stderr: child.stderr,
1621
+ };
1622
+ }
1623
+ function dataList(payload) {
1624
+ if (Array.isArray(payload)) return payload;
1625
+ if (Array.isArray(payload?.data)) return payload.data;
1626
+ if (Array.isArray(payload?.list)) return payload.list;
1627
+ return [];
1628
+ }
1629
+ function encodePathPart(value) {
1630
+ return encodeURIComponent(String(value));
1631
+ }
1632
+ async function runWithWorkOSCli() {
1633
+ const orgResponse = cliApi("/organizations", { method: "GET" });
1634
+ if (!orgResponse.ok) {
1635
+ out.ok = false;
1636
+ out.errors.push({ operation: "cli.organizations.list", status: orgResponse.status, message: orgResponse.stderr || JSON.stringify(orgResponse.data) });
1637
+ return;
1638
+ }
1639
+ const orgs = dataList(orgResponse.data);
1640
+ const orgByName = new Map(orgs.map((org) => [org.name, org]));
1641
+ const sortedResources = [...payload.manifest.resources].sort((a, b) => {
1642
+ if (a.type === "organization") return -1;
1643
+ if (b.type === "organization") return 1;
1644
+ if (!a.parentExternalId && b.parentExternalId) return -1;
1645
+ if (a.parentExternalId && !b.parentExternalId) return 1;
1646
+ return a.externalId.localeCompare(b.externalId);
1647
+ });
1648
+ for (const resource of sortedResources) {
1649
+ if (resource.type === "organization") {
1650
+ out.resources.push({ externalId: resource.externalId, resourceTypeSlug: resource.type, organizationName: resource.tenant, status: "root-organization" });
1651
+ continue;
1652
+ }
1653
+ const org = orgByName.get(resource.tenant);
1654
+ if (!org?.id) {
1655
+ out.ok = false;
1656
+ out.errors.push({ operation: "organization.lookup", organization: resource.tenant, message: "organization not found in WorkOS environment" });
1657
+ continue;
1658
+ }
1659
+ const getPath = "/authorization/organizations/" + encodePathPart(org.id) + "/resources/" + encodePathPart(resource.type) + "/" + encodePathPart(resource.externalId);
1660
+ const existing = cliApi(getPath, { method: "GET" });
1661
+ if (existing.ok) {
1662
+ out.resources.push({ externalId: resource.externalId, resourceTypeSlug: resource.type, organizationId: org.id, status: "existing", id: existing.data?.id });
1663
+ continue;
1664
+ }
1665
+ const created = cliApi("/authorization/resources", {
1666
+ method: "POST",
1667
+ yes: true,
1668
+ data: {
1669
+ organization_id: org.id,
1670
+ resource_type_slug: resource.type,
1671
+ external_id: resource.externalId,
1672
+ name: resource.name,
1673
+ ...(resource.parentExternalId && resource.parentType !== "organization" ? { parent_resource_external_id: resource.parentExternalId, parent_resource_type_slug: resource.parentType } : {}),
1674
+ },
1675
+ });
1676
+ if (!created.ok) {
1677
+ out.ok = false;
1678
+ out.errors.push({ operation: "resource.create", externalId: resource.externalId, resourceTypeSlug: resource.type, status: created.status, message: created.stderr || JSON.stringify(created.data) });
1679
+ continue;
1680
+ }
1681
+ out.resources.push({ externalId: resource.externalId, resourceTypeSlug: resource.type, organizationId: org.id, status: "created", id: created.data?.id });
1682
+ }
1683
+ if (payload.action === "prove") {
1684
+ const memberships = readMembershipMap(payload.manifest.organizations);
1685
+ for (const scenario of payload.manifest.proofScenarios) {
1686
+ const membershipId = memberships.get(scenario.organization) || memberships.get(slugify(scenario.organization));
1687
+ if (!membershipId) {
1688
+ out.ok = false;
1689
+ out.errors.push({
1690
+ operation: "authorization.check",
1691
+ scenario: scenario.name,
1692
+ organization: scenario.organization,
1693
+ expected: scenario.expected,
1694
+ message: "missing organizationMembershipId for WorkOS FGA proof scenario",
1695
+ env: ["WORKOS_FGA_MEMBERSHIPS_JSON", membershipEnvKey(scenario.organization)],
1696
+ });
1697
+ continue;
1698
+ }
1699
+ const checked = cliApi("/authorization/organization_memberships/" + encodePathPart(membershipId) + "/check", {
1700
+ method: "POST",
1701
+ yes: true,
1702
+ data: {
1703
+ permission_slug: scenario.permission,
1704
+ resource_external_id: scenario.resourceExternalId,
1705
+ resource_type_slug: scenario.resourceTypeSlug,
1706
+ },
1707
+ });
1708
+ if (!checked.ok) {
1709
+ out.ok = false;
1710
+ out.errors.push({ operation: "authorization.check", scenario: scenario.name, status: checked.status, message: checked.stderr || JSON.stringify(checked.data) });
1711
+ continue;
1712
+ }
1713
+ const expectedAuthorized = scenario.expected === "allow";
1714
+ const passed = Boolean(checked.data?.authorized) === expectedAuthorized;
1715
+ if (!passed) out.ok = false;
1716
+ out.checks.push({ name: scenario.name, organization: scenario.organization, expected: scenario.expected, authorized: Boolean(checked.data?.authorized), ok: passed });
1717
+ }
1718
+ }
1719
+ }
1720
+ try {
1721
+ if (!process.env.WORKOS_API_KEY) {
1722
+ await runWithWorkOSCli();
1723
+ console.log(JSON.stringify(out));
1724
+ process.exit(out.ok ? 0 : 1);
1725
+ }
1726
+ const mod = await import("@workos-inc/node");
1727
+ const WorkOS = mod.WorkOS || mod.default?.WorkOS;
1728
+ if (!WorkOS) throw new Error("@workos-inc/node did not export WorkOS");
1729
+ const workos = new WorkOS(process.env.WORKOS_API_KEY);
1730
+ const orgs = await listAll(await workos.organizations.listOrganizations());
1731
+ const orgByName = new Map(orgs.map((org) => [org.name, org]));
1732
+ const sortedResources = [...payload.manifest.resources].sort((a, b) => {
1733
+ if (a.type === "organization") return -1;
1734
+ if (b.type === "organization") return 1;
1735
+ if (!a.parentExternalId && b.parentExternalId) return -1;
1736
+ if (a.parentExternalId && !b.parentExternalId) return 1;
1737
+ return a.externalId.localeCompare(b.externalId);
1738
+ });
1739
+ for (const resource of sortedResources) {
1740
+ if (resource.type === "organization") {
1741
+ out.resources.push({ externalId: resource.externalId, resourceTypeSlug: resource.type, organizationName: resource.tenant, status: "root-organization" });
1742
+ continue;
1743
+ }
1744
+ const org = orgByName.get(resource.tenant);
1745
+ if (!org) {
1746
+ out.ok = false;
1747
+ out.errors.push({ operation: "organization.lookup", organization: resource.tenant, message: "organization not found in WorkOS environment" });
1748
+ continue;
1749
+ }
1750
+ try {
1751
+ const existing = await workos.authorization.getResourceByExternalId({
1752
+ organizationId: org.id,
1753
+ resourceTypeSlug: resource.type,
1754
+ externalId: resource.externalId,
1755
+ });
1756
+ out.resources.push({ externalId: resource.externalId, resourceTypeSlug: resource.type, organizationId: org.id, status: "existing", id: existing.id });
1757
+ } catch (error) {
1758
+ if (![404, "404"].includes(status(error))) {
1759
+ out.ok = false;
1760
+ out.errors.push({ operation: "resource.get", externalId: resource.externalId, resourceTypeSlug: resource.type, status: status(error), message: message(error) });
1761
+ continue;
1762
+ }
1763
+ try {
1764
+ const created = await workos.authorization.createResource({
1765
+ organizationId: org.id,
1766
+ resourceTypeSlug: resource.type,
1767
+ externalId: resource.externalId,
1768
+ name: resource.name,
1769
+ ...(resource.parentExternalId && resource.parentType !== "organization" ? { parentResourceExternalId: resource.parentExternalId, parentResourceTypeSlug: resource.parentType } : {}),
1770
+ });
1771
+ out.resources.push({ externalId: resource.externalId, resourceTypeSlug: resource.type, organizationId: org.id, status: "created", id: created.id });
1772
+ } catch (createError) {
1773
+ out.ok = false;
1774
+ out.errors.push({ operation: "resource.create", externalId: resource.externalId, resourceTypeSlug: resource.type, status: status(createError), message: message(createError) });
1775
+ }
1776
+ }
1777
+ }
1778
+ if (payload.action === "prove") {
1779
+ const memberships = readMembershipMap(payload.manifest.organizations);
1780
+ for (const scenario of payload.manifest.proofScenarios) {
1781
+ const membershipId = memberships.get(scenario.organization) || memberships.get(slugify(scenario.organization));
1782
+ if (!membershipId) {
1783
+ out.ok = false;
1784
+ out.errors.push({
1785
+ operation: "authorization.check",
1786
+ scenario: scenario.name,
1787
+ organization: scenario.organization,
1788
+ expected: scenario.expected,
1789
+ message: "missing organizationMembershipId for WorkOS FGA proof scenario",
1790
+ env: ["WORKOS_FGA_MEMBERSHIPS_JSON", membershipEnvKey(scenario.organization)],
1791
+ });
1792
+ continue;
1793
+ }
1794
+ try {
1795
+ const check = await workos.authorization.check({
1796
+ organizationMembershipId: membershipId,
1797
+ permissionSlug: scenario.permission,
1798
+ resourceExternalId: scenario.resourceExternalId,
1799
+ resourceTypeSlug: scenario.resourceTypeSlug,
1800
+ });
1801
+ const expectedAuthorized = scenario.expected === "allow";
1802
+ const passed = Boolean(check.authorized) === expectedAuthorized;
1803
+ if (!passed) out.ok = false;
1804
+ out.checks.push({ name: scenario.name, organization: scenario.organization, expected: scenario.expected, authorized: Boolean(check.authorized), ok: passed });
1805
+ } catch (checkError) {
1806
+ out.ok = false;
1807
+ out.errors.push({ operation: "authorization.check", scenario: scenario.name, status: status(checkError), message: message(checkError) });
1808
+ }
1809
+ }
1810
+ }
1811
+ } catch (error) {
1812
+ out.ok = false;
1813
+ out.errors.push({ operation: "sdk", message: message(error), status: status(error) });
1814
+ }
1815
+ console.log(JSON.stringify(out));
1816
+ process.exit(out.ok ? 0 : 1);
1817
+ `;
1818
+ const command = ["node", "--input-type=module", "-e", script];
1819
+ const displayCommand = ["node", "--input-type=module", "-e", "<forge-workos-fga-sdk>"];
1820
+ const previousPayload = process.env.FORGE_WORKOS_FGA_PAYLOAD;
1821
+ let child: { status: number | null; stdout: string; stderr: string };
1822
+ try {
1823
+ process.env.FORGE_WORKOS_FGA_PAYLOAD = JSON.stringify({ action, manifest });
1824
+ child = runExternalCommand(command, {
1825
+ ...options,
1826
+ commandRunner: options.commandRunner ?? spawnSync,
1827
+ });
1828
+ } finally {
1829
+ if (previousPayload === undefined) {
1830
+ delete process.env.FORGE_WORKOS_FGA_PAYLOAD;
1831
+ } else {
1832
+ process.env.FORGE_WORKOS_FGA_PAYLOAD = previousPayload;
1833
+ }
1834
+ }
1835
+ const parsed = parseJsonObject(child.stdout);
1836
+ return {
1837
+ ok: child.status === 0 && parsed?.ok === true,
1838
+ command: displayCommand,
1839
+ data: parsed ?? {
1840
+ ok: false,
1841
+ errors: [{ operation: "sdk.parse", message: "failed to parse WorkOS FGA SDK output" }],
1842
+ },
1843
+ status: child.status,
1844
+ stdout: child.stdout,
1845
+ stderr: child.stderr,
1846
+ };
1847
+ }
1848
+
1849
+ function workOSFgaSdkFailureDetail(data: unknown, manifest?: WorkOSFgaManifest): string {
1850
+ const text = JSON.stringify(data);
1851
+ if (text.includes("AuthorizationResourceType not found")) {
1852
+ const missing = extractMissingWorkOSFgaResourceTypes(data, manifest);
1853
+ const suffix = missing.length > 0 ? `: ${missing.join(", ")}` : "";
1854
+ return `WorkOS Authorization API returned missing FGA resource type(s)${suffix}; configure them in hosted WorkOS, then rerun forge workos fga sync --real --json`;
1855
+ }
1856
+ if (text.includes("Cannot add resource to organization resource type")) {
1857
+ return "WorkOS treats organizations as tenant roots, not creatable authorization resources; Forge will keep organization in the graph and skip resource creation for it";
1858
+ }
1859
+ if (text.includes("missing organizationMembershipId") || text.includes("WORKOS_FGA_MEMBERSHIPS_JSON")) {
1860
+ const env = manifest?.organizations.map(fgaMembershipEnvKey).join(", ") || "WORKOS_FGA_MEMBERSHIP_<ORG>";
1861
+ return `WorkOS FGA real proof requires organizationMembershipId values per organization; set WORKOS_FGA_MEMBERSHIPS_JSON or ${env}`;
1862
+ }
1863
+ return "WorkOS Authorization API resource sync failed; inspect workosSdk.errors";
1864
+ }
1865
+
1866
+ function workOSFgaSdkProofComplete(data: unknown, manifest: WorkOSFgaManifest): boolean {
1867
+ if (!data || typeof data !== "object") return false;
1868
+ const record = data as { ok?: unknown; checks?: unknown; skipped?: unknown; errors?: unknown };
1869
+ if (record.ok !== true) return false;
1870
+ if (Array.isArray(record.errors) && record.errors.length > 0) return false;
1871
+ if (Array.isArray(record.skipped) && record.skipped.length > 0) return false;
1872
+ if (!Array.isArray(record.checks)) return false;
1873
+ const checks = record.checks as Array<{ name?: unknown; ok?: unknown }>;
1874
+ const checkByName = new Map(checks.map((check) => [String(check.name ?? ""), check]));
1875
+ return manifest.proofScenarios.every((scenario) => checkByName.get(scenario.name)?.ok === true);
1876
+ }
1877
+
1878
+ export interface WorkOSDoctorData {
1879
+ seed: WorkOSSeedSummary;
1880
+ seedState: WorkOSSeedStateSummary;
1881
+ fgaManifest: WorkOSFgaManifest;
1882
+ fgaState: WorkOSFgaStateSummary;
1883
+ activePermissions: string[];
1884
+ expectedResourceTypes: string[];
1885
+ missingSeedPermissions: string[];
1886
+ missingSeedResources: string[];
1887
+ unusedSeedPermissions: string[];
1888
+ }
1889
+
1890
+ function collectWorkOSDoctorData(
1891
+ workspaceRoot: string,
1892
+ preferredSeedPath = DEFAULT_SEED_FILE,
1893
+ ): WorkOSDoctorData {
1894
+ const seed = parseSeedFile(workspaceRoot, preferredSeedPath);
1895
+ const seedState = readWorkOSSeedState(workspaceRoot, seed);
1896
+ const fgaManifest = collectWorkOSFgaManifest(workspaceRoot, preferredSeedPath);
1897
+ const fgaState = readWorkOSFgaState(workspaceRoot, fgaManifest);
1898
+ const activePermissions = collectPolicyPermissions(workspaceRoot);
1899
+ const expectedResourceTypes = collectExpectedResourceTypes(workspaceRoot);
1900
+ const missingSeedPermissions = missingValues(activePermissions, seed.permissions);
1901
+ const missingSeedResources = missingValues(expectedResourceTypes, seed.resourceTypes);
1902
+ const unusedSeedPermissions = activePermissions.length === 0
1903
+ ? []
1904
+ : seed.permissions.filter((permission) => !activePermissions.includes(permission));
1905
+ return {
1906
+ seed,
1907
+ seedState,
1908
+ fgaManifest,
1909
+ fgaState,
1910
+ activePermissions,
1911
+ expectedResourceTypes,
1912
+ missingSeedPermissions,
1913
+ missingSeedResources,
1914
+ unusedSeedPermissions,
1915
+ };
1916
+ }
1917
+
1918
+ function collectWorkOSChecks(workspaceRoot: string, preferredSeedPath = DEFAULT_SEED_FILE): WorkOSCheck[] {
1919
+ const packageJson = readJson(workspaceRoot, "package.json") as {
1920
+ dependencies?: Record<string, string>;
1921
+ devDependencies?: Record<string, string>;
1922
+ } | null;
1923
+ const authRegistry = readJson(workspaceRoot, `${GENERATED_DIR}/authRegistry.json`) as {
1924
+ claims?: Record<string, string | undefined>;
1925
+ } | null;
1926
+ const secretRegistry = readJson(workspaceRoot, `${GENERATED_DIR}/secretRegistry.json`) as {
1927
+ secrets?: Array<{ envVar?: string; name?: string }>;
1928
+ } | null;
1929
+ const secretNames = new Set((secretRegistry?.secrets ?? []).map((secret) => secret.envVar ?? secret.name));
1930
+ const deps = {
1931
+ ...(packageJson?.dependencies ?? {}),
1932
+ ...(packageJson?.devDependencies ?? {}),
1933
+ };
1934
+ const webPackageJson = readJson(workspaceRoot, "web/package.json") as {
1935
+ dependencies?: Record<string, string>;
1936
+ devDependencies?: Record<string, string>;
1937
+ } | null;
1938
+ const webDeps = {
1939
+ ...(webPackageJson?.dependencies ?? {}),
1940
+ ...(webPackageJson?.devDependencies ?? {}),
1941
+ };
1942
+ const hasWeb = webPackageJson !== null;
1943
+ const {
1944
+ seed,
1945
+ seedState,
1946
+ fgaManifest,
1947
+ fgaState,
1948
+ activePermissions,
1949
+ expectedResourceTypes,
1950
+ missingSeedPermissions,
1951
+ missingSeedResources,
1952
+ unusedSeedPermissions,
1953
+ } = collectWorkOSDoctorData(workspaceRoot, preferredSeedPath);
1954
+ const realEnv = readRealEnv(workspaceRoot);
1955
+ const authMode = realEnv.FORGE_AUTH_MODE;
1956
+ const productionAuthEnabled = authMode === "oidc" || authMode === "jwt";
1957
+ const issuerConfigured = hasValue(realEnv, "FORGE_AUTH_ISSUER");
1958
+ const jwksConfigured = hasValue(realEnv, "FORGE_AUTH_JWKS_URI");
1959
+ const clientId = realEnv.WORKOS_CLIENT_ID || realEnv.VITE_WORKOS_CLIENT_ID;
1960
+ const authRoutes = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/auth-routes.ts`);
1961
+ const fga = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/fga.ts`);
1962
+ const resourceMap = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/resource-map.ts`);
1963
+ const fgaEnabled = hasWorkOSFgaArtifacts(workspaceRoot);
1964
+ const httpHandler = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/http-handler.ts`);
1965
+ const policies = readText(workspaceRoot, "src/policies.workos.ts");
1966
+ const session = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/session.ts`);
1967
+ const webhook = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/webhook.ts`);
1968
+ const generatedFrontendAuthBridge = readText(workspaceRoot, "web/src/lib/workos-auth.tsx");
1969
+ const frontendAppShell = [
1970
+ readText(workspaceRoot, "web/src/main.tsx"),
1971
+ readText(workspaceRoot, "web/src/App.tsx"),
1972
+ ].join("\n");
1973
+ const appShellUsesWorkOSProvider = frontendAppShell.includes("ForgeWorkOSAuthProvider");
1974
+ const authBridgeUsesBackendRoutes =
1975
+ generatedFrontendAuthBridge.includes("workOSApiUrl") &&
1976
+ ["/login", "/logout", "/session"].every((route) => generatedFrontendAuthBridge.includes(route));
1977
+ const authBridgeProvidesSessionClaims =
1978
+ includesAll(generatedFrontendAuthBridge, ["useForgeWorkOSSession", "/session", "claims"]);
1979
+ const authBridgePassesClaimsToForge =
1980
+ includesAll(generatedFrontendAuthBridge, [
1981
+ "workOSSessionToForgeAuth",
1982
+ "forgeTenantIdForWorkOSOrganization",
1983
+ "workOSAuthProvider",
1984
+ "auth={workOSAuthProvider}",
1985
+ "permissions: claims.permissions",
1986
+ "organizationId: forgeTenantId",
1987
+ ]);
1988
+ const authSessionProxyConfigured = webAuthSessionProxyConfigured(workspaceRoot);
1989
+
1990
+ return [
1991
+ {
1992
+ name: "package",
1993
+ ok: "@workos-inc/node" in deps,
1994
+ detail: "@workos-inc/node is present in package dependencies",
1995
+ },
1996
+ {
1997
+ name: "auth-registry",
1998
+ ok: authRegistry?.claims?.tenantId === "organization_id" && authRegistry?.claims?.userId === "sub",
1999
+ detail: "authRegistry maps sub and organization_id claims",
2000
+ },
2001
+ {
2002
+ name: "secrets",
2003
+ ok: ["WORKOS_API_KEY", "WORKOS_CLIENT_ID", "WORKOS_COOKIE_PASSWORD"].every((name) => secretNames.has(name)),
2004
+ detail: "required WorkOS secret names are registered",
2005
+ },
2006
+ {
2007
+ name: "env-example",
2008
+ ok: exists(workspaceRoot, ".env.example") &&
2009
+ includesAll(readRawText(workspaceRoot, ".env.example"), [
2010
+ "FORGE_AUTH_MODE=oidc",
2011
+ "FORGE_AUTH_ISSUER=https://api.workos.com",
2012
+ "FORGE_AUTH_JWKS_URI=",
2013
+ "VITE_WORKOS_CLIENT_ID=",
2014
+ "VITE_WORKOS_REDIRECT_URI=",
2015
+ ]),
2016
+ detail: ".env.example exists with Forge OIDC and browser AuthKit variables",
2017
+ },
2018
+ {
2019
+ name: "production-auth-readiness",
2020
+ ok: !productionAuthEnabled || (issuerConfigured && jwksConfigured),
2021
+ detail: !productionAuthEnabled
2022
+ ? "production OIDC/JWT env not enabled in .env/.env.local"
2023
+ : issuerConfigured && jwksConfigured
2024
+ ? "FORGE_AUTH_MODE uses production auth and issuer/JWKS are configured"
2025
+ : `FORGE_AUTH_MODE=${authMode} requires FORGE_AUTH_ISSUER=https://api.workos.com and FORGE_AUTH_JWKS_URI=${workosJwksUri(clientId)}`,
2026
+ },
2027
+ {
2028
+ name: "browser-authkit-env",
2029
+ ok: !hasWeb || (hasValue(realEnv, "VITE_WORKOS_CLIENT_ID") || readRawText(workspaceRoot, ".env.example").includes("VITE_WORKOS_CLIENT_ID=")) &&
2030
+ (hasValue(realEnv, "VITE_WORKOS_REDIRECT_URI") || readRawText(workspaceRoot, ".env.example").includes("VITE_WORKOS_REDIRECT_URI=")),
2031
+ detail: hasWeb
2032
+ ? "web workspace has browser WorkOS client ID and redirect URI guidance for backend-owned AuthKit routes"
2033
+ : "no web workspace detected",
2034
+ },
2035
+ {
2036
+ name: "browser-authkit-package",
2037
+ ok: !hasWeb || "@workos-inc/authkit-react" in webDeps,
2038
+ detail: hasWeb
2039
+ ? "@workos-inc/authkit-react is present in web/package.json"
2040
+ : "no web workspace detected",
2041
+ },
2042
+ {
2043
+ name: "browser-authkit-bridge",
2044
+ ok: !hasWeb || generatedFrontendAuthBridge.includes("ForgeProvider") &&
2045
+ authBridgeUsesBackendRoutes &&
2046
+ authBridgeProvidesSessionClaims &&
2047
+ authBridgePassesClaimsToForge,
2048
+ detail: hasWeb
2049
+ ? "generated web/src/lib/workos-auth.tsx bridge uses backend-owned AuthKit routes and passes normalized WorkOS claims into ForgeProvider auth"
2050
+ : "no web workspace detected",
2051
+ },
2052
+ {
2053
+ name: "browser-authkit-session-proxy",
2054
+ ok: !hasWeb || authSessionProxyConfigured,
2055
+ detail: hasWeb
2056
+ ? authSessionProxyConfigured
2057
+ ? "web dev config proxies /login, /callback, /logout, and /session to the Forge API runtime"
2058
+ : "web dev config should proxy /session with /login, /callback, and /logout so AuthKit UI can read Forge-normalized claims"
2059
+ : "no web workspace detected",
2060
+ },
2061
+ {
2062
+ name: "browser-authkit-provider",
2063
+ ok: !hasWeb || appShellUsesWorkOSProvider,
2064
+ detail: hasWeb
2065
+ ? "web app shell mounts ForgeWorkOSAuthProvider"
2066
+ : "no web workspace detected",
2067
+ },
2068
+ {
2069
+ name: "seed-file",
2070
+ ok: seed.exists,
2071
+ detail: seed.exists
2072
+ ? `${seed.path} exists with ${seed.permissions.length} permission(s), ${seed.roles.length} role(s), ${seed.resourceTypes.length} optional FGA resource type(s)`
2073
+ : `${DEFAULT_SEED_FILE} or ${GENERATED_SEED_FILE} is required`,
2074
+ },
2075
+ {
2076
+ name: "seed-organizations",
2077
+ ok: seed.organizations.length > 0,
2078
+ detail: seed.organizations.length > 0
2079
+ ? `seed file contains organization(s): ${seed.organizations.join(", ")}`
2080
+ : "seed file should contain at least one demo organization",
2081
+ },
2082
+ {
2083
+ name: "seed-roles-permissions",
2084
+ ok: seed.roles.length > 0 &&
2085
+ (activePermissions.length === 0 ? seed.permissions.length > 0 : missingSeedPermissions.length === 0),
2086
+ detail: missingSeedPermissions.length === 0
2087
+ ? `seed covers ${activePermissions.length} active policy permission(s) with role(s): ${seed.roles.join(", ")}`
2088
+ : `seed missing active policy permission(s): ${missingSeedPermissions.join(", ")}`,
2089
+ },
2090
+ {
2091
+ name: "seed-unused-permissions",
2092
+ ok: true,
2093
+ detail: activePermissions.length === 0
2094
+ ? "no active policy permissions were discovered; unused seed permission check skipped"
2095
+ : unusedSeedPermissions.length === 0
2096
+ ? "seed permissions are all referenced by active policies"
2097
+ : `seed includes permission(s) not referenced by active policies: ${unusedSeedPermissions.join(", ")}`,
2098
+ },
2099
+ {
2100
+ name: "seed-resource-types",
2101
+ ok: !fgaEnabled || (seed.resourceTypes.length > 0 &&
2102
+ (expectedResourceTypes.length === 0 || missingSeedResources.length === 0)),
2103
+ detail: !fgaEnabled
2104
+ ? "FGA is not enabled; resource_types are optional for AuthKit/RBAC apps"
2105
+ : missingSeedResources.length === 0
2106
+ ? `seed resource_types cover app graph: ${expectedResourceTypes.length > 0 ? expectedResourceTypes.join(", ") : seed.resourceTypes.join(", ")}`
2107
+ : `seed missing resource_type(s) for app graph: ${missingSeedResources.join(", ")}`,
2108
+ },
2109
+ {
2110
+ name: "seed-auth-config",
2111
+ ok: seed.redirectUris.length > 0 && seed.corsOrigins.length > 0 && Boolean(seed.homepageUrl),
2112
+ detail: seed.redirectUris.length > 0 && seed.corsOrigins.length > 0 && Boolean(seed.homepageUrl)
2113
+ ? `seed config contains ${seed.redirectUris.length} redirect URI(s), ${seed.corsOrigins.length} CORS origin(s), and homepage URL`
2114
+ : "seed config should include redirect_uris, cors_origins, and homepage_url for no-dashboard setup",
2115
+ },
2116
+ {
2117
+ name: "seed-state",
2118
+ ok: true,
2119
+ detail: !seedState.exists
2120
+ ? `${WORKOS_SEED_STATE_FILE} not found; hosted WorkOS seed has not been proven locally yet`
2121
+ : seedState.matchesSeedHash
2122
+ ? `${WORKOS_SEED_STATE_FILE} matches ${seed.path}${seedState.alreadyApplied ? " and records an already-applied hosted seed" : ""}`
2123
+ : seedState.valid
2124
+ ? `${WORKOS_SEED_STATE_FILE} exists but does not match current ${seed.path}; rerun forge workos seed --file ${seed.path} --json after reviewing seed changes`
2125
+ : `${WORKOS_SEED_STATE_FILE} exists but is invalid: ${seedState.diagnostics.join("; ")}`,
2126
+ },
2127
+ {
2128
+ name: "fga-plan",
2129
+ ok: !fgaEnabled || fgaManifest.diagnostics.length === 0,
2130
+ detail: !fgaEnabled
2131
+ ? "FGA is not enabled; run forge add auth workos --with-fga for resource-level WorkOS Authorization"
2132
+ : fgaManifest.diagnostics.length === 0
2133
+ ? `FGA plan covers ${fgaManifest.resourceTypes.length} resource type(s), ${fgaManifest.resources.length} resource(s), and ${fgaManifest.proofScenarios.length} proof scenario(s)`
2134
+ : `FGA plan has gap(s): ${fgaManifest.diagnostics.join("; ")}`,
2135
+ },
2136
+ {
2137
+ name: "fga-state",
2138
+ ok: true,
2139
+ detail: !fgaEnabled
2140
+ ? "FGA is not enabled; no FGA state file is required"
2141
+ : !fgaState.exists
2142
+ ? `${WORKOS_FGA_STATE_FILE} not found; run forge workos fga sync --json before production deploy`
2143
+ : fgaState.matchesManifestHash
2144
+ ? `${WORKOS_FGA_STATE_FILE} matches current FGA manifest${fgaState.mode === "real" ? " in real mode" : ""}`
2145
+ : fgaState.valid
2146
+ ? `${WORKOS_FGA_STATE_FILE} exists but does not match current FGA manifest; rerun forge workos fga sync --json`
2147
+ : `${WORKOS_FGA_STATE_FILE} exists but is invalid: ${fgaState.diagnostics.join("; ")}`,
2148
+ },
2149
+ {
2150
+ name: "authkit-routes",
2151
+ ok: includesAll(authRoutes, ["handleWorkOSAuthRequest", "/login", "/callback", "/logout", "/session"]),
2152
+ detail: "AuthKit Request/Response route helper exists for login, callback, logout, and session",
2153
+ },
2154
+ {
2155
+ name: "authkit-session",
2156
+ ok: includesAll(session, ["encodeWorkOSSession", "decodeWorkOSSession", "workOSSessionToClaims"]),
2157
+ detail: "AuthKit session helper signs cookies and maps sessions to Forge claims",
2158
+ },
2159
+ {
2160
+ name: "webhook-helper",
2161
+ ok: includesAll(webhook, ["verifyWorkOSWebhook", "handleWorkOSWebhook", 'provider: "workos"']),
2162
+ detail: "WorkOS webhook verifier/helper exists and uses WorkOS signature verification",
2163
+ },
2164
+ {
2165
+ name: "webhook-http-handler",
2166
+ ok: includesAll(httpHandler, ["handleWorkOSWebhookRequest", "workosWebhookHttpRoute", "/webhooks/workos"]),
2167
+ detail: "WorkOS Request/Response webhook handler exists for POST /webhooks/workos",
2168
+ },
2169
+ {
2170
+ name: "fga-bridge",
2171
+ ok: !fgaEnabled || (includesAll(resourceMap, [
2172
+ "canWorkOS",
2173
+ "assertWorkOSResourceTenant",
2174
+ "FORGE_WORKOS_CROSS_TENANT_RESOURCE",
2175
+ "syncWorkOSResourceGraph",
2176
+ "workOSResourceRecords",
2177
+ "ForgeWorkOSFgaDecisionCache",
2178
+ "permissionSlug",
2179
+ "resourceExternalId",
2180
+ ]) &&
2181
+ includesAll(fga, ["forgeWorkOSResourceTypes"])),
2182
+ detail: fgaEnabled
2183
+ ? "WorkOS FGA resource-map bridge exists with sync, cache, telemetry, official check shape, and cross-tenant guard"
2184
+ : "FGA bridge is optional and not enabled for this WorkOS AuthKit/RBAC app",
2185
+ },
2186
+ {
2187
+ name: "policies",
2188
+ ok: exists(workspaceRoot, "src/policies.workos.ts") &&
2189
+ policies.includes("canPermission") &&
2190
+ missingValues(activePermissions, quotedValues(policies)).length === 0,
2191
+ detail: activePermissions.length === 0
2192
+ ? "WorkOS-derived Forge policy template exists and is permission-first"
2193
+ : `WorkOS-derived policy template covers active permission(s): ${activePermissions.join(", ")}`,
2194
+ },
2195
+ ];
2196
+ }
2197
+
2198
+ function prepareSeedFileForWorkOSCli(
2199
+ workspaceRoot: string,
2200
+ file: string,
2201
+ ): { file: string; sanitized: boolean; cleanup: () => void } {
2202
+ const raw = readRawText(workspaceRoot, file);
2203
+ const stripped = stripDeterministicHeader(raw);
2204
+ if (!raw || raw === stripped) {
2205
+ return { file, sanitized: false, cleanup: () => undefined };
2206
+ }
2207
+
2208
+ const tempDir = mkdtempSync(join(tmpdir(), "forge-workos-seed-"));
2209
+ const preparedFile = join(tempDir, basename(file));
2210
+ writeFileSync(preparedFile, stripped, "utf8");
2211
+ return {
2212
+ file: preparedFile,
2213
+ sanitized: true,
2214
+ cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
2215
+ };
2216
+ }
2217
+
2218
+ function isWorkOSSeedAlreadyApplied(stdout: string, stderr: string): boolean {
2219
+ const output = `${stdout}\n${stderr}`;
2220
+ return /(?:permission|role|resource type|organization|slug|domain)[^\n]*(?:already in use|already exists|exists already)|already in use/i
2221
+ .test(output);
2222
+ }
2223
+
2224
+ function seedData(input: {
2225
+ seed: WorkOSSeedSummary;
2226
+ activePermissions: string[];
2227
+ expectedResourceTypes: string[];
2228
+ unusedSeedPermissions: string[];
2229
+ seedState?: WorkOSSeedStateSummary;
2230
+ dryRun?: boolean;
2231
+ seedFileSanitized?: boolean;
2232
+ seedAlreadyApplied?: boolean;
2233
+ seedAlreadyAppliedReason?: string;
2234
+ seedStateFile?: string;
2235
+ cliAuth?: WorkOSCliAuthSummary;
2236
+ workosCli?: Record<string, unknown>;
2237
+ configActions?: WorkOSConfigActionResult[];
2238
+ nextCommand?: string;
2239
+ }): Record<string, unknown> {
2240
+ return {
2241
+ seed: input.seed,
2242
+ activePermissions: input.activePermissions,
2243
+ expectedResourceTypes: input.expectedResourceTypes,
2244
+ unusedSeedPermissions: input.unusedSeedPermissions,
2245
+ ...(input.seedState ? { seedState: input.seedState } : {}),
2246
+ dryRun: input.dryRun ?? false,
2247
+ seedFileSanitized: input.seedFileSanitized ?? false,
2248
+ seedAlreadyApplied: input.seedAlreadyApplied ?? false,
2249
+ ...(input.seedAlreadyAppliedReason ? { seedAlreadyAppliedReason: input.seedAlreadyAppliedReason } : {}),
2250
+ ...(input.seedStateFile ? { seedStateFile: input.seedStateFile } : {}),
2251
+ ...(input.cliAuth ? { cliAuth: input.cliAuth } : {}),
2252
+ ...(input.workosCli ? { workosCli: input.workosCli } : {}),
2253
+ configActions: input.configActions ?? [],
2254
+ ...(input.nextCommand ? { nextCommand: input.nextCommand } : {}),
2255
+ };
2256
+ }
2257
+
2258
+ function hashSeedFile(workspaceRoot: string, path: string): string {
2259
+ return createHash("sha256")
2260
+ .update(readText(workspaceRoot, path))
2261
+ .digest("hex");
2262
+ }
2263
+
2264
+ function readWorkOSSeedState(workspaceRoot: string, seed: WorkOSSeedSummary): WorkOSSeedStateSummary {
2265
+ const path = WORKOS_SEED_STATE_FILE;
2266
+ const absolute = join(workspaceRoot, path);
2267
+ if (!existsSync(absolute)) {
2268
+ return {
2269
+ exists: false,
2270
+ valid: false,
2271
+ path,
2272
+ matchesSeedHash: null,
2273
+ diagnostics: [`${path} is missing; run forge workos seed --file ${seed.path} --json after dry-run validation`],
2274
+ };
2275
+ }
2276
+ try {
2277
+ const parsed = JSON.parse(readFileSync(absolute, "utf8")) as {
2278
+ seedHash?: unknown;
2279
+ appliedAt?: unknown;
2280
+ alreadyApplied?: unknown;
2281
+ };
2282
+ const diagnostics: string[] = [];
2283
+ const seedHash = typeof parsed.seedHash === "string" ? parsed.seedHash : undefined;
2284
+ const currentSeedHash = seed.exists ? hashSeedFile(workspaceRoot, seed.path) : undefined;
2285
+ if (!seedHash) diagnostics.push("seed state is missing seedHash");
2286
+ if (typeof parsed.appliedAt !== "string") diagnostics.push("seed state is missing appliedAt");
2287
+ if (typeof parsed.alreadyApplied !== "boolean") diagnostics.push("seed state is missing alreadyApplied");
2288
+ return {
2289
+ exists: true,
2290
+ valid: diagnostics.length === 0,
2291
+ path,
2292
+ matchesSeedHash: seedHash && currentSeedHash ? seedHash === currentSeedHash : null,
2293
+ ...(seedHash ? { seedHash } : {}),
2294
+ ...(currentSeedHash ? { currentSeedHash } : {}),
2295
+ ...(typeof parsed.appliedAt === "string" ? { appliedAt: parsed.appliedAt } : {}),
2296
+ ...(typeof parsed.alreadyApplied === "boolean" ? { alreadyApplied: parsed.alreadyApplied } : {}),
2297
+ diagnostics,
2298
+ };
2299
+ } catch (error) {
2300
+ return {
2301
+ exists: true,
2302
+ valid: false,
2303
+ path,
2304
+ matchesSeedHash: null,
2305
+ diagnostics: [`failed to parse ${path}: ${error instanceof Error ? error.message : String(error)}`],
2306
+ };
2307
+ }
2308
+ }
2309
+
2310
+ function writeWorkOSSeedState(input: {
2311
+ workspaceRoot: string;
2312
+ seed: WorkOSSeedSummary;
2313
+ command: string[];
2314
+ alreadyApplied: boolean;
2315
+ status: number | null;
2316
+ }): string {
2317
+ const payload = {
2318
+ schemaVersion: "0.1.0",
2319
+ provider: "workos",
2320
+ kind: "seed-state",
2321
+ seedFile: input.seed.path,
2322
+ seedHash: hashSeedFile(input.workspaceRoot, input.seed.path),
2323
+ appliedAt: new Date().toISOString(),
2324
+ alreadyApplied: input.alreadyApplied,
2325
+ exitStatus: input.status,
2326
+ command: input.command,
2327
+ permissions: input.seed.permissions,
2328
+ roles: input.seed.roles,
2329
+ resourceTypes: input.seed.resourceTypes,
2330
+ organizations: input.seed.organizations,
2331
+ redirectUris: input.seed.redirectUris,
2332
+ corsOrigins: input.seed.corsOrigins,
2333
+ };
2334
+ writeFileSync(
2335
+ join(input.workspaceRoot, WORKOS_SEED_STATE_FILE),
2336
+ `${JSON.stringify(payload, null, 2)}\n`,
2337
+ "utf8",
2338
+ );
2339
+ return WORKOS_SEED_STATE_FILE;
2340
+ }
2341
+
2342
+ export interface WorkOSConfigActionResult {
2343
+ name: string;
2344
+ command?: string[];
2345
+ ok: boolean;
2346
+ skipped: boolean;
2347
+ reason?: string;
2348
+ stdout?: string;
2349
+ stderr?: string;
2350
+ status?: number | null;
2351
+ }
2352
+
2353
+ function configActionsForSeed(seed: WorkOSSeedSummary): Array<{ name: string; command: string[]; skipReason?: string }> {
2354
+ const actions: Array<{ name: string; command: string[]; skipReason?: string }> = [];
2355
+ for (const uri of seed.redirectUris) {
2356
+ actions.push({
2357
+ name: "redirect-uri",
2358
+ command: ["npx", "--yes", "workos@latest", "config", "redirect", "add", uri],
2359
+ });
2360
+ }
2361
+ for (const origin of seed.corsOrigins) {
2362
+ actions.push({
2363
+ name: "cors-origin",
2364
+ command: ["npx", "--yes", "workos@latest", "config", "cors", "add", origin],
2365
+ });
2366
+ }
2367
+ if (seed.homepageUrl) {
2368
+ actions.push({
2369
+ name: "homepage-url",
2370
+ command: ["npx", "--yes", "workos@latest", "config", "homepage-url", "set", seed.homepageUrl],
2371
+ });
2372
+ }
2373
+ for (const endpoint of seed.webhookEndpoints) {
2374
+ const events = endpoint.events.length > 0 ? endpoint.events.join(",") : "user.created,organization_membership.updated";
2375
+ actions.push({
2376
+ name: "webhook",
2377
+ command: ["npx", "--yes", "workos@latest", "webhook", "create", "--url", endpoint.url, "--events", events],
2378
+ skipReason: endpoint.url.startsWith("https://")
2379
+ ? undefined
2380
+ : "WorkOS hosted webhook endpoints require HTTPS; use a tunnel or production URL for this endpoint.",
2381
+ });
2382
+ }
2383
+ return actions;
2384
+ }
2385
+
2386
+ function runWorkOSConfigActions(
2387
+ seed: WorkOSSeedSummary,
2388
+ options: WorkOSCommandOptions,
2389
+ dryRun: boolean,
2390
+ ): WorkOSConfigActionResult[] {
2391
+ return configActionsForSeed(seed).map((action) => {
2392
+ if (action.skipReason) {
2393
+ return {
2394
+ name: action.name,
2395
+ command: action.command,
2396
+ ok: true,
2397
+ skipped: true,
2398
+ reason: action.skipReason,
2399
+ };
2400
+ }
2401
+ if (dryRun) {
2402
+ return {
2403
+ name: action.name,
2404
+ command: action.command,
2405
+ ok: true,
2406
+ skipped: true,
2407
+ reason: "dry-run",
2408
+ };
2409
+ }
2410
+ const child = runExternalCommand(action.command, options);
2411
+ return {
2412
+ name: action.name,
2413
+ command: action.command,
2414
+ ok: child.status === 0 || isWorkOSSeedAlreadyApplied(child.stdout, child.stderr),
2415
+ skipped: false,
2416
+ stdout: child.stdout,
2417
+ stderr: child.stderr,
2418
+ status: child.status,
2419
+ ...(child.status !== 0 && isWorkOSSeedAlreadyApplied(child.stdout, child.stderr)
2420
+ ? { reason: "already-applied" }
2421
+ : {}),
2422
+ };
2423
+ });
2424
+ }
2425
+
2426
+ export function runWorkOSDoctorCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
2427
+ const seedFile = options.file ?? DEFAULT_SEED_FILE;
2428
+ const checks = collectWorkOSChecks(options.workspaceRoot, seedFile);
2429
+ const data = collectWorkOSDoctorData(options.workspaceRoot, seedFile);
2430
+ const ok = checks.every((check) => check.ok);
2431
+ const command = ["npx", "--yes", "workos@latest", "doctor"];
2432
+ if (!ok) {
2433
+ return {
2434
+ ok: false,
2435
+ kind: "workos-doctor",
2436
+ checks,
2437
+ command,
2438
+ applied: false,
2439
+ data,
2440
+ exitCode: 1,
2441
+ };
2442
+ }
2443
+ if (!options.yes || options.dryRun) {
2444
+ return {
2445
+ ok: true,
2446
+ kind: "workos-doctor",
2447
+ checks,
2448
+ command,
2449
+ applied: false,
2450
+ data,
2451
+ exitCode: 0,
2452
+ };
2453
+ }
2454
+ const child = runExternalCommand(command, options);
2455
+ return {
2456
+ ok: child.status === 0,
2457
+ kind: "workos-doctor",
2458
+ checks,
2459
+ command,
2460
+ applied: child.status === 0,
2461
+ data,
2462
+ stdout: child.stdout,
2463
+ stderr: child.stderr,
2464
+ exitCode: child.status === 0 ? 0 : 1,
2465
+ };
2466
+ }
2467
+
2468
+ export function runWorkOSInstallCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
2469
+ const checks = collectWorkOSChecks(options.workspaceRoot);
2470
+ const ok = checks.every((check) => check.ok);
2471
+ const command = ["npx", "--yes", "workos@latest", "install"];
2472
+ if (!ok) {
2473
+ return {
2474
+ ok: false,
2475
+ kind: "workos-install",
2476
+ checks,
2477
+ command,
2478
+ applied: false,
2479
+ exitCode: 1,
2480
+ };
2481
+ }
2482
+ if (!options.yes || options.dryRun) {
2483
+ return {
2484
+ ok: true,
2485
+ kind: "workos-install",
2486
+ checks,
2487
+ command,
2488
+ applied: false,
2489
+ exitCode: 0,
2490
+ };
2491
+ }
2492
+ const child = runExternalCommand(command, options);
2493
+ return {
2494
+ ok: child.status === 0,
2495
+ kind: "workos-install",
2496
+ checks,
2497
+ command,
2498
+ applied: child.status === 0,
2499
+ stdout: child.stdout,
2500
+ stderr: child.stderr,
2501
+ exitCode: child.status === 0 ? 0 : 1,
2502
+ };
2503
+ }
2504
+
2505
+ export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
2506
+ const file = options.file ?? DEFAULT_SEED_FILE;
2507
+ const seed = parseSeedFile(options.workspaceRoot, file);
2508
+ const seedState = readWorkOSSeedState(options.workspaceRoot, seed);
2509
+ const activePermissions = collectPolicyPermissions(options.workspaceRoot);
2510
+ const expectedResourceTypes = collectExpectedResourceTypes(options.workspaceRoot);
2511
+ const missingSeedPermissions = missingValues(activePermissions, seed.permissions);
2512
+ const missingSeedResources = missingValues(expectedResourceTypes, seed.resourceTypes);
2513
+ const unusedSeedPermissions = seed.permissions.filter((permission) => !activePermissions.includes(permission));
2514
+ const fgaEnabled = hasWorkOSFgaArtifacts(options.workspaceRoot);
2515
+ const checks = [
2516
+ {
2517
+ name: "seed-file",
2518
+ ok: seed.exists,
2519
+ detail: seed.exists ? `${seed.path} exists` : `${file} exists`,
2520
+ },
2521
+ {
2522
+ name: "seed-yaml-shape",
2523
+ ok: seed.valid,
2524
+ detail: seed.valid
2525
+ ? `seed contains permissions, roles, organizations${seed.resourceTypes.length > 0 ? ", and optional FGA resource_types" : ""}`
2526
+ : seed.diagnostics.join("; "),
2527
+ },
2528
+ {
2529
+ name: "seed-policy-coverage",
2530
+ ok: missingSeedPermissions.length === 0,
2531
+ detail: missingSeedPermissions.length === 0
2532
+ ? `seed covers ${activePermissions.length} active permission(s)`
2533
+ : `seed missing active permission(s): ${missingSeedPermissions.join(", ")}`,
2534
+ },
2535
+ {
2536
+ name: "seed-unused-permissions",
2537
+ ok: true,
2538
+ detail: activePermissions.length === 0
2539
+ ? "no active policy permissions were discovered; unused seed permission check skipped"
2540
+ : unusedSeedPermissions.length === 0
2541
+ ? "seed permissions are all referenced by active policies"
2542
+ : `seed includes permission(s) not referenced by active policies: ${unusedSeedPermissions.join(", ")}`,
2543
+ },
2544
+ {
2545
+ name: "seed-resource-coverage",
2546
+ ok: !fgaEnabled || missingSeedResources.length === 0,
2547
+ detail: !fgaEnabled
2548
+ ? "FGA is not enabled; seed resource type coverage is not required"
2549
+ : missingSeedResources.length === 0
2550
+ ? `seed covers app resource type(s): ${expectedResourceTypes.join(", ") || "none required"}`
2551
+ : `seed missing resource type(s): ${missingSeedResources.join(", ")}`,
2552
+ },
2553
+ {
2554
+ name: "seed-hosted-config",
2555
+ ok: seed.redirectUris.length > 0 && seed.corsOrigins.length > 0 && Boolean(seed.homepageUrl),
2556
+ detail: seed.redirectUris.length > 0 && seed.corsOrigins.length > 0 && Boolean(seed.homepageUrl)
2557
+ ? `seed includes hosted config: ${seed.redirectUris.length} redirect URI(s), ${seed.corsOrigins.length} CORS origin(s), homepage ${seed.homepageUrl}`
2558
+ : "seed should include config.redirect_uris, config.cors_origins, and config.homepage_url",
2559
+ },
2560
+ ];
2561
+ const command = ["npx", "--yes", "workos@latest", "seed", "--file", file];
2562
+ if (!checks.every((check) => check.ok)) {
2563
+ return {
2564
+ ok: false,
2565
+ kind: "workos-seed",
2566
+ checks,
2567
+ command,
2568
+ applied: false,
2569
+ data: seedData({ seed, activePermissions, expectedResourceTypes, unusedSeedPermissions, seedState }),
2570
+ exitCode: 1,
2571
+ };
2572
+ }
2573
+ if (options.dryRun) {
2574
+ const configActions = runWorkOSConfigActions(seed, options, true);
2575
+ return {
2576
+ ok: true,
2577
+ kind: "workos-seed",
2578
+ checks,
2579
+ command,
2580
+ applied: false,
2581
+ data: seedData({
2582
+ seed,
2583
+ activePermissions,
2584
+ expectedResourceTypes,
2585
+ unusedSeedPermissions,
2586
+ seedState,
2587
+ dryRun: true,
2588
+ configActions,
2589
+ nextCommand: `forge workos seed --file ${file} --json`,
2590
+ }),
2591
+ exitCode: 0,
2592
+ };
2593
+ }
2594
+ const cliAuth = ensureWorkOSCliAuthForHosted(options);
2595
+ if (!cliAuth.ok) {
2596
+ return {
2597
+ ok: false,
2598
+ kind: "workos-seed",
2599
+ checks: [...checks, workOSCliAuthCheck(cliAuth)],
2600
+ command: cliAuth.loginCommand ?? cliAuth.statusCommand ?? command,
2601
+ applied: false,
2602
+ data: seedData({
2603
+ seed,
2604
+ activePermissions,
2605
+ expectedResourceTypes,
2606
+ unusedSeedPermissions,
2607
+ seedState,
2608
+ cliAuth,
2609
+ nextCommand: `forge workos seed --file ${file} --json`,
2610
+ }),
2611
+ exitCode: 1,
2612
+ };
2613
+ }
2614
+ const preparedSeed = prepareSeedFileForWorkOSCli(options.workspaceRoot, seed.path);
2615
+ const delegatedCommand = [
2616
+ "npx",
2617
+ "--yes",
2618
+ "workos@latest",
2619
+ "seed",
2620
+ "--file",
2621
+ preparedSeed.file,
2622
+ ];
2623
+ try {
2624
+ const configActions = runWorkOSConfigActions(seed, options, false);
2625
+ const configOk = configActions.every((action) => action.ok);
2626
+ if (!configOk) {
2627
+ return {
2628
+ ok: false,
2629
+ kind: "workos-seed",
2630
+ checks,
2631
+ command: configActions.find((action) => !action.ok)?.command ?? delegatedCommand,
2632
+ applied: false,
2633
+ data: seedData({
2634
+ seed,
2635
+ activePermissions,
2636
+ expectedResourceTypes,
2637
+ unusedSeedPermissions,
2638
+ seedState,
2639
+ cliAuth,
2640
+ seedFileSanitized: preparedSeed.sanitized,
2641
+ configActions,
2642
+ }),
2643
+ exitCode: 1,
2644
+ };
2645
+ }
2646
+ const child = runExternalCommand(delegatedCommand, options);
2647
+ const seedAlreadyApplied =
2648
+ child.status !== 0 && isWorkOSSeedAlreadyApplied(child.stdout, child.stderr);
2649
+ const seedAlreadyAppliedReason = seedAlreadyApplied
2650
+ ? "workos-cli-existing-resource"
2651
+ : undefined;
2652
+ const seedStateFile = child.status === 0 || seedAlreadyApplied
2653
+ ? writeWorkOSSeedState({
2654
+ workspaceRoot: options.workspaceRoot,
2655
+ seed,
2656
+ command: delegatedCommand,
2657
+ alreadyApplied: seedAlreadyApplied,
2658
+ status: child.status,
2659
+ })
2660
+ : undefined;
2661
+ const latestSeedState = seedStateFile
2662
+ ? readWorkOSSeedState(options.workspaceRoot, seed)
2663
+ : seedState;
2664
+ return {
2665
+ ok: child.status === 0 || seedAlreadyApplied,
2666
+ kind: "workos-seed",
2667
+ checks,
2668
+ command: delegatedCommand,
2669
+ applied: child.status === 0,
2670
+ data: seedData({
2671
+ seed,
2672
+ activePermissions,
2673
+ expectedResourceTypes,
2674
+ unusedSeedPermissions,
2675
+ seedState: latestSeedState,
2676
+ cliAuth,
2677
+ seedFileSanitized: preparedSeed.sanitized,
2678
+ seedAlreadyApplied,
2679
+ seedAlreadyAppliedReason,
2680
+ seedStateFile,
2681
+ workosCli: {
2682
+ status: child.status,
2683
+ idempotentConflict: seedAlreadyApplied,
2684
+ stderrSuppressed: seedAlreadyApplied && Boolean(child.stderr.trim()),
2685
+ },
2686
+ configActions,
2687
+ }),
2688
+ stdout: child.stdout,
2689
+ stderr: seedAlreadyApplied ? undefined : child.stderr,
2690
+ exitCode: child.status === 0 || seedAlreadyApplied ? 0 : 1,
2691
+ };
2692
+ } finally {
2693
+ preparedSeed.cleanup();
2694
+ }
2695
+ }
2696
+
2697
+ export function runWorkOSSetupCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
2698
+ const file = options.file ?? DEFAULT_SEED_FILE;
2699
+ const checks = collectWorkOSChecks(options.workspaceRoot);
2700
+ const cliAuth = options.real ? ensureWorkOSCliAuthForHosted(options) : undefined;
2701
+ const realEnvChecks = options.real ? collectWorkOSRealEnvChecks(options.workspaceRoot, cliAuth) : [];
2702
+ const allChecks = [...checks, ...realEnvChecks];
2703
+ if (options.real && cliAuth && !cliAuth.ok) {
2704
+ allChecks.push(workOSCliAuthCheck(cliAuth));
2705
+ }
2706
+ const localOk = allChecks.every((check) => check.ok);
2707
+ const realEnvMissing = realEnvChecks.some((check) => !check.ok);
2708
+ const envCommand = hasValue(readRealEnv(options.workspaceRoot), "WORKOS_CLIENT_ID") ||
2709
+ hasValue(readRealEnv(options.workspaceRoot), "VITE_WORKOS_CLIENT_ID")
2710
+ ? "forge workos env --write --json"
2711
+ : workOSEnvNextCommand();
2712
+ const seed = parseSeedFile(options.workspaceRoot, file);
2713
+ const seedState = readWorkOSSeedState(options.workspaceRoot, seed);
2714
+ const setupDryRun = options.dryRun || !options.real;
2715
+ const publicEnvSync = syncWorkOSPublicWebEnv(options.workspaceRoot, localOk && !setupDryRun);
2716
+ const configActions = runWorkOSConfigActions(seed, options, true);
2717
+ const command = ["npx", "--yes", "workos@latest", "seed", "--file", file];
2718
+ if (!localOk) {
2719
+ return {
2720
+ ok: false,
2721
+ kind: "workos-setup",
2722
+ checks: allChecks,
2723
+ command,
2724
+ applied: false,
2725
+ data: {
2726
+ dryRun: setupDryRun,
2727
+ real: options.real ?? false,
2728
+ seed,
2729
+ seedState,
2730
+ publicEnvSync,
2731
+ ...(cliAuth ? { cliAuth } : {}),
2732
+ configActions,
2733
+ nextCommand: realEnvMissing
2734
+ ? envCommand
2735
+ : cliAuth && !cliAuth.ok
2736
+ ? `forge workos setup --real --file ${file} --json`
2737
+ : "forge workos doctor --json",
2738
+ nextActions: [
2739
+ ...(realEnvMissing ? [envCommand] : []),
2740
+ ...(realEnvMissing ? ["pass the AuthKit client id with forge workos env --client-id client_... --write --json if ForgeOS cannot infer it"] : []),
2741
+ ...(cliAuth && !cliAuth.ok ? cliAuth.nextActions : []),
2742
+ ],
2743
+ },
2744
+ exitCode: 1,
2745
+ };
2746
+ }
2747
+ if (setupDryRun) {
2748
+ return {
2749
+ ok: true,
2750
+ kind: "workos-setup",
2751
+ checks,
2752
+ command,
2753
+ applied: false,
2754
+ data: {
2755
+ dryRun: true,
2756
+ real: false,
2757
+ seed,
2758
+ seedState,
2759
+ publicEnvSync,
2760
+ ...(cliAuth ? { cliAuth } : {}),
2761
+ configActions,
2762
+ nextCommand: `forge workos setup --real --file ${file} --json`,
2763
+ },
2764
+ exitCode: 0,
2765
+ };
2766
+ }
2767
+ const seedResult = runWorkOSSeedCommand({
2768
+ ...options,
2769
+ subcommand: "seed",
2770
+ dryRun: false,
2771
+ file,
2772
+ });
2773
+ const seedResultData = seedResult.data && typeof seedResult.data === "object"
2774
+ ? seedResult.data as { seedState?: WorkOSSeedStateSummary }
2775
+ : {};
2776
+ return {
2777
+ ok: seedResult.ok,
2778
+ kind: "workos-setup",
2779
+ checks: [...seedResult.checks, ...realEnvChecks],
2780
+ command: seedResult.command,
2781
+ applied: seedResult.ok,
2782
+ data: {
2783
+ real: true,
2784
+ seed,
2785
+ publicEnvSync,
2786
+ ...(cliAuth ? { cliAuth } : {}),
2787
+ seedState: seedResultData.seedState ?? readWorkOSSeedState(options.workspaceRoot, seed),
2788
+ seedResult: seedResult.data,
2789
+ nextCommand: "forge workos doctor --json",
2790
+ },
2791
+ stdout: seedResult.stdout,
2792
+ stderr: seedResult.stderr,
2793
+ exitCode: seedResult.exitCode,
2794
+ };
2795
+ }
2796
+
2797
+ export function runWorkOSProveCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
2798
+ const file = options.file ?? DEFAULT_SEED_FILE;
2799
+ const doctor = runWorkOSDoctorCommand({
2800
+ ...options,
2801
+ subcommand: "doctor",
2802
+ yes: false,
2803
+ dryRun: true,
2804
+ file,
2805
+ });
2806
+ const seed = runWorkOSSeedCommand({
2807
+ ...options,
2808
+ subcommand: "seed",
2809
+ dryRun: true,
2810
+ file,
2811
+ });
2812
+ const setup = runWorkOSSetupCommand({
2813
+ ...options,
2814
+ subcommand: "setup",
2815
+ dryRun: !options.real,
2816
+ real: options.real ?? false,
2817
+ file,
2818
+ });
2819
+ const setupBlockingChecks = setup.checks
2820
+ .filter((check) => !check.ok)
2821
+ .map((check) => ({ ...check, name: `setup:${check.name}` }));
2822
+ const checks: WorkOSCheck[] = [
2823
+ ...doctor.checks.map((check) => ({ ...check, name: `doctor:${check.name}` })),
2824
+ {
2825
+ name: "seed:dry-run",
2826
+ ok: seed.ok,
2827
+ detail: seed.ok
2828
+ ? `${file} is valid, app-aware, and ready for hosted application`
2829
+ : `${file} failed seed validation before hosted application`,
2830
+ },
2831
+ ...setupBlockingChecks,
2832
+ {
2833
+ name: options.real ? "setup:real" : "setup:dry-run",
2834
+ ok: setup.ok,
2835
+ detail: options.real
2836
+ ? setup.ok
2837
+ ? "WorkOS hosted setup and seed were applied or already present"
2838
+ : "WorkOS hosted setup failed; inspect setup.seedResult/configActions"
2839
+ : setup.ok
2840
+ ? "No-dashboard WorkOS setup plan is complete; pass --real to apply hosted changes"
2841
+ : "No-dashboard WorkOS setup plan is incomplete",
2842
+ },
2843
+ ];
2844
+ const ok = checks.every((check) => check.ok);
2845
+ const setupData = setup.data && typeof setup.data === "object" ? setup.data as { nextCommand?: string; nextActions?: string[] } : {};
2846
+ return {
2847
+ ok,
2848
+ kind: "workos-prove",
2849
+ checks,
2850
+ command: setup.command ?? seed.command ?? doctor.command,
2851
+ applied: Boolean(options.real && setup.ok && setup.applied),
2852
+ data: {
2853
+ real: options.real ?? false,
2854
+ file,
2855
+ doctor: doctor.data,
2856
+ seed: seed.data,
2857
+ setup: setup.data,
2858
+ nextCommand: !ok && setupData.nextCommand
2859
+ ? setupData.nextCommand
2860
+ : options.real
2861
+ ? "forge workos doctor --json"
2862
+ : `forge workos prove --real --file ${file} --json`,
2863
+ nextActions: setupData.nextActions ?? [],
2864
+ },
2865
+ stdout: setup.stdout ?? seed.stdout ?? doctor.stdout,
2866
+ stderr: setup.stderr ?? seed.stderr ?? doctor.stderr,
2867
+ exitCode: ok ? 0 : 1,
2868
+ };
2869
+ }
2870
+
2871
+ function collectWorkOSFgaChecks(input: {
2872
+ manifest: WorkOSFgaManifest;
2873
+ state: WorkOSFgaStateSummary;
2874
+ requireState?: boolean;
2875
+ requireRealState?: boolean;
2876
+ requireProof?: boolean;
2877
+ }): WorkOSCheck[] {
2878
+ return [
2879
+ {
2880
+ name: "fga-manifest",
2881
+ ok: input.manifest.diagnostics.length === 0,
2882
+ detail: input.manifest.diagnostics.length === 0
2883
+ ? `manifest ${input.manifest.manifestHash.slice(0, 12)} covers ${input.manifest.resourceTypes.length} resource type(s) and ${input.manifest.resources.length} resource(s)`
2884
+ : input.manifest.diagnostics.join("; "),
2885
+ },
2886
+ {
2887
+ name: "fga-resource-types",
2888
+ ok: input.manifest.resourceTypes.length > 0 && input.manifest.resourceTypes.includes("organization"),
2889
+ detail: input.manifest.resourceTypes.length > 0
2890
+ ? `resource types: ${input.manifest.resourceTypes.join(", ")}`
2891
+ : "at least organization plus app resource types are required",
2892
+ },
2893
+ {
2894
+ name: "fga-proof-scenarios",
2895
+ ok: input.manifest.proofScenarios.some((scenario) => scenario.expected === "allow") &&
2896
+ input.manifest.proofScenarios.some((scenario) => scenario.expected === "deny"),
2897
+ detail: `proof scenarios: ${input.manifest.proofScenarios.map((scenario) => `${scenario.name}:${scenario.expected}`).join(", ") || "none"}`,
2898
+ },
2899
+ {
2900
+ name: "fga-state",
2901
+ ok: !input.requireState || Boolean(input.state.exists && input.state.valid && input.state.matchesManifestHash === true),
2902
+ detail: !input.state.exists
2903
+ ? `${WORKOS_FGA_STATE_FILE} is missing`
2904
+ : !input.state.valid
2905
+ ? `${WORKOS_FGA_STATE_FILE} is invalid: ${input.state.diagnostics.join("; ")}`
2906
+ : input.state.matchesManifestHash
2907
+ ? `${WORKOS_FGA_STATE_FILE} matches current manifest`
2908
+ : `${WORKOS_FGA_STATE_FILE} is stale or invalid: ${input.state.diagnostics.join("; ") || "manifest hash mismatch"}`,
2909
+ },
2910
+ {
2911
+ name: "fga-real-state",
2912
+ ok: !input.requireRealState || input.state.mode === "real",
2913
+ detail: input.state.mode === "real"
2914
+ ? `${WORKOS_FGA_STATE_FILE} records real sync/proof mode`
2915
+ : input.requireRealState
2916
+ ? `${WORKOS_FGA_STATE_FILE} must be produced by forge workos fga sync --real --json`
2917
+ : `${WORKOS_FGA_STATE_FILE} real mode not required for this command`,
2918
+ },
2919
+ {
2920
+ name: "fga-proof-state",
2921
+ ok: !input.requireProof || Boolean(input.state.provedAt),
2922
+ detail: input.state.provedAt
2923
+ ? input.state.mode === "real"
2924
+ ? `${WORKOS_FGA_STATE_FILE} records real proof at ${input.state.provedAt}`
2925
+ : `${WORKOS_FGA_STATE_FILE} records local proof at ${input.state.provedAt}`
2926
+ : input.requireProof
2927
+ ? `${WORKOS_FGA_STATE_FILE} must be produced by forge workos fga prove --real --json`
2928
+ : `${WORKOS_FGA_STATE_FILE} proof timestamp not required for this command`,
2929
+ },
2930
+ ];
2931
+ }
2932
+
2933
+ function workOSFgaReadiness(input: {
2934
+ workspaceRoot: string;
2935
+ file: string;
2936
+ manifest: WorkOSFgaManifest;
2937
+ state: WorkOSFgaStateSummary;
2938
+ seedState: WorkOSSeedStateSummary;
2939
+ real: boolean;
2940
+ }): WorkOSFgaReadiness {
2941
+ const membershipEnv = workOSFgaMembershipEnvSummary(input.workspaceRoot, input.manifest.organizations);
2942
+ const planReady = input.manifest.diagnostics.length === 0 &&
2943
+ input.manifest.resourceTypes.includes("organization") &&
2944
+ input.manifest.proofScenarios.some((scenario) => scenario.expected === "allow") &&
2945
+ input.manifest.proofScenarios.some((scenario) => scenario.expected === "deny");
2946
+ const seedReady = !input.real || Boolean(input.seedState.exists && input.seedState.valid && input.seedState.matchesSeedHash === true);
2947
+ const synced = Boolean(input.state.exists && input.state.valid && input.state.matchesManifestHash === true);
2948
+ const resourceTypesConfigured = !input.real || Boolean(synced && input.state.mode === "real" && input.state.sdkOk === true);
2949
+ const membershipEnvReady = !input.real || membershipEnv.complete;
2950
+ const proved = !input.real
2951
+ ? Boolean(input.state.provedAt)
2952
+ : Boolean(synced && input.state.mode === "real" && input.state.sdkOk === true && input.state.provedAt);
2953
+ const productionReady = Boolean(planReady && seedReady && resourceTypesConfigured && membershipEnvReady && proved);
2954
+ let nextCommand = `forge workos fga plan --file ${input.file} --write --json`;
2955
+ if (planReady && input.real && !seedReady) {
2956
+ nextCommand = `forge workos prove --real --file ${input.file} --json`;
2957
+ } else if (planReady && input.real && !resourceTypesConfigured) {
2958
+ nextCommand = `forge workos fga sync --real --file ${input.file} --write --json`;
2959
+ } else if (planReady && input.real && !membershipEnvReady) {
2960
+ nextCommand = `forge workos fga prove --real --file ${input.file} --json`;
2961
+ } else if (planReady && input.real && !proved) {
2962
+ nextCommand = `forge workos fga prove --real --file ${input.file} --json`;
2963
+ } else if (planReady && !input.real && !synced) {
2964
+ nextCommand = `forge workos fga sync --file ${input.file} --json`;
2965
+ } else if (planReady && !input.real && !proved) {
2966
+ nextCommand = `forge workos fga prove --file ${input.file} --json`;
2967
+ } else if (productionReady || (planReady && !input.real)) {
2968
+ nextCommand = "forge deploy check --production --json";
2969
+ }
2970
+ const nextActions = [
2971
+ ...(planReady ? [] : [`repair FGA manifest gaps, then run forge workos fga plan --file ${input.file} --write --json`]),
2972
+ ...(input.real && !seedReady ? [`apply/prove hosted WorkOS seed: forge workos prove --real --file ${input.file} --json`] : []),
2973
+ ...(input.real && !resourceTypesConfigured
2974
+ ? [
2975
+ "configure hosted WorkOS FGA resource types listed in resourceTypeSetup",
2976
+ `sync real WorkOS FGA resources: forge workos fga sync --real --file ${input.file} --write --json`,
2977
+ ]
2978
+ : []),
2979
+ ...(input.real && !membershipEnvReady
2980
+ ? [`set WORKOS_FGA_MEMBERSHIPS_JSON or ${membershipEnv.missingEnv.join(", ") || "WORKOS_FGA_MEMBERSHIP_<ORG>"} before real access checks`]
2981
+ : []),
2982
+ ...(input.real && membershipEnvReady && resourceTypesConfigured && !proved
2983
+ ? [`prove real WorkOS FGA access checks: forge workos fga prove --real --file ${input.file} --json`]
2984
+ : []),
2985
+ ...(productionReady ? ["rerun forge deploy check --production --json"] : []),
2986
+ ...(!input.real && planReady && !synced ? [`run forge workos fga sync --file ${input.file} --json`] : []),
2987
+ ...(!input.real && planReady && synced && !proved ? [`run forge workos fga prove --file ${input.file} --json`] : []),
2988
+ ];
2989
+ return {
2990
+ real: input.real,
2991
+ planReady,
2992
+ seedReady,
2993
+ resourceTypesConfigured,
2994
+ membershipEnvReady,
2995
+ synced,
2996
+ proved,
2997
+ productionReady,
2998
+ nextCommand,
2999
+ nextActions,
3000
+ };
3001
+ }
3002
+
3003
+ function collectWorkOSFgaDoctorChecks(input: {
3004
+ workspaceRoot: string;
3005
+ file: string;
3006
+ manifest: WorkOSFgaManifest;
3007
+ state: WorkOSFgaStateSummary;
3008
+ seedState: WorkOSSeedStateSummary;
3009
+ real: boolean;
3010
+ }): WorkOSCheck[] {
3011
+ const readiness = workOSFgaReadiness(input);
3012
+ const membershipEnv = workOSFgaMembershipEnvSummary(input.workspaceRoot, input.manifest.organizations);
3013
+ return [
3014
+ ...collectWorkOSFgaChecks({
3015
+ manifest: input.manifest,
3016
+ state: input.state,
3017
+ requireState: input.real,
3018
+ requireRealState: input.real,
3019
+ requireProof: input.real,
3020
+ }),
3021
+ {
3022
+ name: "fga-seed-state",
3023
+ ok: !input.real || readiness.seedReady,
3024
+ detail: !input.real
3025
+ ? "hosted seed evidence is only required for --real doctor"
3026
+ : readiness.seedReady
3027
+ ? `${WORKOS_SEED_STATE_FILE} matches ${input.file}`
3028
+ : `real FGA proof requires hosted seed evidence matching ${input.file}; run forge workos prove --real --file ${input.file} --json`,
3029
+ },
3030
+ {
3031
+ name: "fga-hosted-resource-types",
3032
+ ok: !input.real || readiness.resourceTypesConfigured,
3033
+ detail: !input.real
3034
+ ? "hosted WorkOS resource type existence is only verified by --real sync/prove"
3035
+ : readiness.resourceTypesConfigured
3036
+ ? `${WORKOS_FGA_STATE_FILE} records successful real Authorization API resource sync`
3037
+ : "real FGA requires hosted WorkOS resource types for every non-organization resource; run forge workos fga plan --write and configure any listed resource types before sync",
3038
+ },
3039
+ {
3040
+ name: "fga-membership-env",
3041
+ ok: !input.real || readiness.membershipEnvReady,
3042
+ detail: !input.real
3043
+ ? "organizationMembershipId env is only required for real WorkOS access checks"
3044
+ : readiness.membershipEnvReady
3045
+ ? `membership env is present${membershipEnv.jsonEnvPresent ? " through WORKOS_FGA_MEMBERSHIPS_JSON" : ` through ${membershipEnv.presentEnv.join(", ")}`}`
3046
+ : `missing organizationMembershipId env for real checks: WORKOS_FGA_MEMBERSHIPS_JSON or ${membershipEnv.missingEnv.join(", ")}`,
3047
+ },
3048
+ {
3049
+ name: "fga-production-readiness",
3050
+ ok: !input.real || readiness.productionReady,
3051
+ detail: !input.real
3052
+ ? "run forge workos fga doctor --real --json for production FGA gates"
3053
+ : readiness.productionReady
3054
+ ? "real WorkOS FGA seed, resource sync, membership env, and proof are current"
3055
+ : `real WorkOS FGA is not production-ready; next command: ${readiness.nextCommand}`,
3056
+ },
3057
+ ];
3058
+ }
3059
+
3060
+ export function runWorkOSFgaCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
3061
+ const action = options.fgaAction ?? "doctor";
3062
+ const file = options.file ?? DEFAULT_SEED_FILE;
3063
+ const manifest = collectWorkOSFgaManifest(options.workspaceRoot, file);
3064
+ const initialHostedSetup = workOSFgaHostedSetup(manifest);
3065
+ const setupGuidePath = resolveWorkOSFgaSetupGuidePath(options);
3066
+ const writtenSetupGuide = setupGuidePath
3067
+ ? writeWorkOSFgaSetupGuide(options.workspaceRoot, workOSFgaSetupGuide(manifest, initialHostedSetup), setupGuidePath)
3068
+ : undefined;
3069
+ const writtenSetupGuideData = writtenSetupGuide
3070
+ ? { setupGuidePath: writtenSetupGuide.markdownPath, setupGuideJsonPath: writtenSetupGuide.jsonPath }
3071
+ : {};
3072
+ let state = readWorkOSFgaState(options.workspaceRoot, manifest);
3073
+ const command = ["forge", "workos", "fga", action, "--file", file];
3074
+ const real = options.real ?? false;
3075
+ const requireState = action === "prove" || action === "doctor";
3076
+ const requireRealState = real && action !== "plan";
3077
+ const requireProof = real && action === "doctor";
3078
+ const seed = parseSeedFile(options.workspaceRoot, file);
3079
+ const seedState = readWorkOSSeedState(options.workspaceRoot, seed);
3080
+ const checks: WorkOSCheck[] = collectWorkOSFgaChecks({
3081
+ manifest,
3082
+ state,
3083
+ requireState,
3084
+ requireRealState,
3085
+ requireProof,
3086
+ });
3087
+
3088
+ if (action === "plan") {
3089
+ const ok = checks.filter((check) => check.name !== "fga-state" && check.name !== "fga-real-state" && check.name !== "fga-proof-state").every((check) => check.ok);
3090
+ return {
3091
+ ok,
3092
+ kind: "workos-fga",
3093
+ checks,
3094
+ command,
3095
+ applied: false,
3096
+ data: fgaData({
3097
+ action,
3098
+ workspaceRoot: options.workspaceRoot,
3099
+ manifest,
3100
+ state,
3101
+ seedState,
3102
+ real,
3103
+ ...writtenSetupGuideData,
3104
+ nextCommand: `forge workos fga sync --file ${file} --json`,
3105
+ }),
3106
+ exitCode: ok ? 0 : 1,
3107
+ };
3108
+ }
3109
+
3110
+ if (action === "sync") {
3111
+ const cliAuth = real ? ensureWorkOSCliAuthForHosted(options) : undefined;
3112
+ const realChecks: WorkOSCheck[] = real
3113
+ ? [
3114
+ ...(cliAuth && !cliAuth.ok ? [workOSCliAuthCheck(cliAuth)] : []),
3115
+ {
3116
+ name: "fga-seed-state",
3117
+ ok: Boolean(seedState.exists && seedState.valid && seedState.matchesSeedHash === true),
3118
+ detail: seedState.matchesSeedHash
3119
+ ? `${WORKOS_SEED_STATE_FILE} matches ${seed.path}`
3120
+ : `real FGA sync requires hosted seed evidence; run forge workos prove --real --file ${file} --json first`,
3121
+ },
3122
+ ]
3123
+ : [];
3124
+ const allChecks = [...checks.filter((check) => check.name !== "fga-state" && check.name !== "fga-real-state" && check.name !== "fga-proof-state"), ...realChecks];
3125
+ if (!allChecks.every((check) => check.ok)) {
3126
+ return {
3127
+ ok: false,
3128
+ kind: "workos-fga",
3129
+ checks: allChecks,
3130
+ command: cliAuth && !cliAuth.ok ? cliAuth.loginCommand ?? cliAuth.statusCommand : command,
3131
+ applied: false,
3132
+ data: fgaData({
3133
+ action,
3134
+ workspaceRoot: options.workspaceRoot,
3135
+ manifest,
3136
+ state,
3137
+ seedState,
3138
+ real,
3139
+ ...(cliAuth ? { cliAuth } : {}),
3140
+ ...writtenSetupGuideData,
3141
+ nextCommand: real ? `forge workos fga sync --real --file ${file} --json` : `forge workos fga plan --file ${file} --json`,
3142
+ }),
3143
+ exitCode: 1,
3144
+ };
3145
+ }
3146
+ if (options.dryRun) {
3147
+ return {
3148
+ ok: true,
3149
+ kind: "workos-fga",
3150
+ checks: allChecks,
3151
+ command,
3152
+ applied: false,
3153
+ data: fgaData({
3154
+ action,
3155
+ workspaceRoot: options.workspaceRoot,
3156
+ manifest,
3157
+ state,
3158
+ seedState,
3159
+ real,
3160
+ ...(cliAuth ? { cliAuth } : {}),
3161
+ ...writtenSetupGuideData,
3162
+ nextCommand: real ? `forge workos fga sync --real --file ${file} --json` : `forge workos fga sync --file ${file} --json`,
3163
+ }),
3164
+ exitCode: 0,
3165
+ };
3166
+ }
3167
+ const sdk = real ? runWorkOSFgaSdk(options, manifest, "sync") : undefined;
3168
+ if (sdk && !sdk.ok) {
3169
+ return {
3170
+ ok: false,
3171
+ kind: "workos-fga",
3172
+ checks: [
3173
+ ...allChecks,
3174
+ {
3175
+ name: "fga-real-sdk-sync",
3176
+ ok: false,
3177
+ detail: workOSFgaSdkFailureDetail(sdk.data, manifest),
3178
+ },
3179
+ ],
3180
+ command: sdk.command,
3181
+ applied: false,
3182
+ data: fgaData({
3183
+ action,
3184
+ workspaceRoot: options.workspaceRoot,
3185
+ manifest,
3186
+ state,
3187
+ seedState,
3188
+ real,
3189
+ ...(cliAuth ? { cliAuth } : {}),
3190
+ workosSdk: sdk.data,
3191
+ ...writtenSetupGuideData,
3192
+ nextCommand: `forge workos fga sync --real --file ${file} --json`,
3193
+ nextActions: workOSFgaHostedSetup(manifest, sdk.data).nextActions,
3194
+ }),
3195
+ stdout: sdk.stdout,
3196
+ stderr: sdk.stderr,
3197
+ exitCode: 1,
3198
+ };
3199
+ }
3200
+ const stateFile = writeWorkOSFgaState({
3201
+ workspaceRoot: options.workspaceRoot,
3202
+ manifest,
3203
+ mode: real ? "real" : "local",
3204
+ proved: false,
3205
+ ...(sdk ? { sdkOk: sdk.ok, sdk: sdk.data } : {}),
3206
+ });
3207
+ state = readWorkOSFgaState(options.workspaceRoot, manifest);
3208
+ return {
3209
+ ok: true,
3210
+ kind: "workos-fga",
3211
+ checks: collectWorkOSFgaChecks({ manifest, state, requireState: true, requireRealState: real }),
3212
+ command,
3213
+ applied: true,
3214
+ data: fgaData({
3215
+ action,
3216
+ workspaceRoot: options.workspaceRoot,
3217
+ manifest,
3218
+ state,
3219
+ seedState,
3220
+ real,
3221
+ ...(cliAuth ? { cliAuth } : {}),
3222
+ ...(sdk ? { workosSdk: sdk.data } : {}),
3223
+ stateFile,
3224
+ ...writtenSetupGuideData,
3225
+ nextCommand: real ? `forge workos fga prove --real --file ${file} --json` : `forge workos fga prove --file ${file} --json`,
3226
+ }),
3227
+ exitCode: 0,
3228
+ };
3229
+ }
3230
+
3231
+ if (action === "prove") {
3232
+ const cliAuth = real ? ensureWorkOSCliAuthForHosted(options) : undefined;
3233
+ const sdk = real && state.exists && state.valid && state.matchesManifestHash === true && state.mode === "real"
3234
+ ? runWorkOSFgaSdk(options, manifest, "prove")
3235
+ : undefined;
3236
+ const sdkProofComplete = sdk ? workOSFgaSdkProofComplete(sdk.data, manifest) : false;
3237
+ const proofChecks: WorkOSCheck[] = [
3238
+ ...checks,
3239
+ ...(cliAuth && !cliAuth.ok ? [workOSCliAuthCheck(cliAuth)] : []),
3240
+ ...(sdk
3241
+ ? [
3242
+ {
3243
+ name: "fga-real-sdk-proof",
3244
+ ok: sdk.ok && sdkProofComplete,
3245
+ detail: sdk.ok && sdkProofComplete
3246
+ ? "WorkOS Authorization API resource sync/check proof completed for every scenario"
3247
+ : workOSFgaSdkFailureDetail(sdk.data, manifest),
3248
+ },
3249
+ ]
3250
+ : real
3251
+ ? [
3252
+ {
3253
+ name: "fga-real-sdk-proof",
3254
+ ok: false,
3255
+ detail: "real FGA proof requires a fresh real FGA state from forge workos fga sync --real --json",
3256
+ },
3257
+ ]
3258
+ : []),
3259
+ {
3260
+ name: "fga-cross-tenant-proof",
3261
+ ok: manifest.proofScenarios.some((scenario) => scenario.name.includes("cross-tenant") && scenario.expected === "deny"),
3262
+ detail: "FGA proof includes cross-tenant denial scenario using resourceExternalId and resourceTypeSlug",
3263
+ },
3264
+ {
3265
+ name: "fga-authorization-api-shape",
3266
+ ok: true,
3267
+ detail: "proof contract uses organizationMembershipId, resourceExternalId, resourceTypeSlug, and permission slugs",
3268
+ },
3269
+ ];
3270
+ const ok = proofChecks.every((check) => check.ok);
3271
+ const stateFile = ok && !options.dryRun
3272
+ ? writeWorkOSFgaState({
3273
+ workspaceRoot: options.workspaceRoot,
3274
+ manifest,
3275
+ mode: real ? "real" : "local",
3276
+ proved: true,
3277
+ ...(sdk ? { sdkOk: sdk.ok, sdk: sdk.data } : {}),
3278
+ })
3279
+ : undefined;
3280
+ state = stateFile ? readWorkOSFgaState(options.workspaceRoot, manifest) : state;
3281
+ return {
3282
+ ok,
3283
+ kind: "workos-fga",
3284
+ checks: proofChecks,
3285
+ command: cliAuth && !cliAuth.ok ? cliAuth.loginCommand ?? cliAuth.statusCommand : command,
3286
+ applied: Boolean(ok && !options.dryRun),
3287
+ data: fgaData({
3288
+ action,
3289
+ workspaceRoot: options.workspaceRoot,
3290
+ manifest,
3291
+ state,
3292
+ seedState,
3293
+ real,
3294
+ ...(cliAuth ? { cliAuth } : {}),
3295
+ ...(sdk ? { workosSdk: sdk.data } : {}),
3296
+ ...(stateFile ? { stateFile } : {}),
3297
+ ...writtenSetupGuideData,
3298
+ nextCommand: ok ? "forge deploy check --production --json" : `forge workos fga sync${real ? " --real" : ""} --file ${file} --json`,
3299
+ }),
3300
+ stdout: sdk?.stdout,
3301
+ stderr: sdk?.stderr,
3302
+ exitCode: ok ? 0 : 1,
3303
+ };
3304
+ }
3305
+
3306
+ const doctorChecks = collectWorkOSFgaDoctorChecks({
3307
+ workspaceRoot: options.workspaceRoot,
3308
+ file,
3309
+ manifest,
3310
+ state,
3311
+ seedState,
3312
+ real,
3313
+ });
3314
+ const readiness = workOSFgaReadiness({
3315
+ workspaceRoot: options.workspaceRoot,
3316
+ file,
3317
+ manifest,
3318
+ state,
3319
+ seedState,
3320
+ real,
3321
+ });
3322
+ const ok = doctorChecks.every((check) => check.ok);
3323
+ return {
3324
+ ok,
3325
+ kind: "workos-fga",
3326
+ checks: doctorChecks,
3327
+ command,
3328
+ applied: false,
3329
+ data: fgaData({
3330
+ action,
3331
+ workspaceRoot: options.workspaceRoot,
3332
+ manifest,
3333
+ state,
3334
+ seedState,
3335
+ readiness,
3336
+ real,
3337
+ ...writtenSetupGuideData,
3338
+ nextCommand: readiness.nextCommand,
3339
+ nextActions: readiness.nextActions,
3340
+ }),
3341
+ exitCode: ok ? 0 : 1,
3342
+ };
3343
+ }
3344
+
3345
+ export function runWorkOSCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
3346
+ if (options.subcommand === "fga") {
3347
+ return runWorkOSFgaCommand(options);
3348
+ }
3349
+ if (options.subcommand === "install") {
3350
+ return runWorkOSInstallCommand(options);
3351
+ }
3352
+ if (options.subcommand === "setup") {
3353
+ return runWorkOSSetupCommand(options);
3354
+ }
3355
+ if (options.subcommand === "prove") {
3356
+ return runWorkOSProveCommand(options);
3357
+ }
3358
+ if (options.subcommand === "env") {
3359
+ return runWorkOSEnvCommand(options);
3360
+ }
3361
+ return options.subcommand === "doctor"
3362
+ ? runWorkOSDoctorCommand(options)
3363
+ : runWorkOSSeedCommand(options);
3364
+ }
3365
+
3366
+ export function formatWorkOSJson(result: WorkOSCommandResult): string {
3367
+ return `${JSON.stringify(result, null, 2)}\n`;
3368
+ }
3369
+
3370
+ export function formatWorkOSHuman(result: WorkOSCommandResult): string {
3371
+ const lines = [
3372
+ result.ok ? "WorkOS: ok" : "WorkOS: needs attention",
3373
+ ...result.checks.map((check) => `${check.ok ? "ok" : "fail"} ${check.name}: ${check.detail}`),
3374
+ ];
3375
+ const dataObject = result.data && typeof result.data === "object"
3376
+ ? result.data as {
3377
+ cliAuth?: WorkOSCliAuthSummary;
3378
+ seedResult?: { cliAuth?: WorkOSCliAuthSummary };
3379
+ setup?: { cliAuth?: WorkOSCliAuthSummary; seedResult?: { cliAuth?: WorkOSCliAuthSummary } };
3380
+ }
3381
+ : {};
3382
+ const cliAuth = dataObject.cliAuth ?? dataObject.seedResult?.cliAuth ?? dataObject.setup?.cliAuth ?? dataObject.setup?.seedResult?.cliAuth;
3383
+ if (cliAuth) {
3384
+ if (cliAuth.ok && cliAuth.method === "cli") {
3385
+ lines.push(`WorkOS CLI authenticated${cliAuth.email ? ` as ${cliAuth.email}` : ""}; hosted setup can proceed without opening the dashboard.`);
3386
+ } else if (!cliAuth.ok && cliAuth.loginInstructions) {
3387
+ lines.push("WorkOS CLI login required before hosted setup can proceed.");
3388
+ if (cliAuth.loginShellCommand) {
3389
+ lines.push(`login: ${cliAuth.loginShellCommand}`);
3390
+ }
3391
+ if (cliAuth.loginInstructions.url) {
3392
+ lines.push(`open: ${cliAuth.loginInstructions.url}`);
3393
+ }
3394
+ if (cliAuth.loginInstructions.code) {
3395
+ lines.push(`code: ${cliAuth.loginInstructions.code}`);
3396
+ }
3397
+ if (cliAuth.rerunCommand) {
3398
+ lines.push(`rerun: ${cliAuth.rerunCommand}`);
3399
+ }
3400
+ } else if (cliAuth.method === "api-key") {
3401
+ lines.push("WorkOS hosted setup will use WORKOS_API_KEY; CLI browser login is optional.");
3402
+ }
3403
+ }
3404
+ if (result.command) {
3405
+ lines.push(`command: ${result.command.join(" ")}`);
3406
+ }
3407
+ if (result.kind === "workos-install" && !result.applied) {
3408
+ lines.push("AuthKit install not applied; pass --yes to execute the WorkOS CLI command");
3409
+ }
3410
+ if (result.kind === "workos-doctor" && !result.applied) {
3411
+ lines.push("external WorkOS doctor not run; pass --yes to execute the WorkOS CLI command");
3412
+ }
3413
+ if (result.kind === "workos-seed" && !result.applied) {
3414
+ const data = result.data && typeof result.data === "object"
3415
+ ? result.data as { dryRun?: boolean; seedAlreadyApplied?: boolean; seedStateFile?: string; nextCommand?: string }
3416
+ : {};
3417
+ if (data.seedAlreadyApplied) {
3418
+ lines.push(`seed already appears applied; WorkOS reported existing resources${data.seedStateFile ? ` and Forge wrote ${data.seedStateFile}` : ""}`);
3419
+ } else if (data.dryRun) {
3420
+ lines.push(`seed dry-run only; run ${data.nextCommand ?? "forge workos seed --file workos-seed.yml --json"} to execute the WorkOS CLI command`);
3421
+ } else {
3422
+ lines.push("seed not applied; inspect stdout/stderr from the WorkOS CLI command");
3423
+ }
3424
+ } else if (result.kind === "workos-seed" && result.applied) {
3425
+ const data = result.data && typeof result.data === "object"
3426
+ ? result.data as { seedStateFile?: string }
3427
+ : {};
3428
+ if (data.seedStateFile) {
3429
+ lines.push(`seed applied; Forge wrote ${data.seedStateFile}`);
3430
+ }
3431
+ }
3432
+ if (result.kind === "workos-setup" && !result.applied) {
3433
+ const data = result.data && typeof result.data === "object"
3434
+ ? result.data as { dryRun?: boolean; nextCommand?: string }
3435
+ : {};
3436
+ if (data.dryRun) {
3437
+ lines.push(`setup dry-run only; run ${data.nextCommand ?? "forge workos setup --real --file workos-seed.yml --json"} to apply hosted config`);
3438
+ } else {
3439
+ lines.push("setup not applied; inspect WorkOS checks and seed/config action output");
3440
+ }
3441
+ } else if (result.kind === "workos-setup" && result.applied) {
3442
+ const data = result.data && typeof result.data === "object"
3443
+ ? result.data as { seedState?: { path?: string; matchesSeedHash?: boolean | null } }
3444
+ : {};
3445
+ if (data.seedState?.path) {
3446
+ lines.push(
3447
+ data.seedState.matchesSeedHash === true
3448
+ ? `setup applied; ${data.seedState.path} matches the current seed`
3449
+ : `setup applied; inspect ${data.seedState.path} because it does not prove the current seed`,
3450
+ );
3451
+ }
3452
+ }
3453
+ if (result.kind === "workos-prove") {
3454
+ const data = result.data && typeof result.data === "object"
3455
+ ? result.data as { real?: boolean; nextCommand?: string }
3456
+ : {};
3457
+ if (result.ok && data.real) {
3458
+ lines.push("WorkOS real proof passed; hosted setup and seed evidence are recorded.");
3459
+ } else if (result.ok) {
3460
+ lines.push(`WorkOS proof dry-run passed; run ${data.nextCommand ?? "forge workos prove --real --file workos-seed.yml --json"} to apply hosted setup.`);
3461
+ } else {
3462
+ lines.push("WorkOS proof failed; inspect doctor, seed, and setup details.");
3463
+ }
3464
+ }
3465
+ if (result.kind === "workos-env") {
3466
+ const data = result.data && typeof result.data === "object"
3467
+ ? result.data as { write?: boolean; envFile?: string; changed?: string[]; requiredStillMissing?: string[]; nextActions?: string[] }
3468
+ : {};
3469
+ if (result.ok && data.write) {
3470
+ lines.push(`WorkOS env written${data.envFile ? ` to ${data.envFile}` : ""}.`);
3471
+ } else if (result.ok) {
3472
+ lines.push("WorkOS env plan is complete; pass --write to apply it.");
3473
+ } else {
3474
+ lines.push(`WorkOS env still needs: ${(data.requiredStillMissing ?? []).join(", ") || "required values"}.`);
3475
+ }
3476
+ for (const path of data.changed ?? []) {
3477
+ lines.push(`changed: ${path}`);
3478
+ }
3479
+ for (const action of data.nextActions ?? []) {
3480
+ lines.push(` - ${action}`);
3481
+ }
3482
+ }
3483
+ if (result.kind === "workos-fga") {
3484
+ const data = result.data && typeof result.data === "object"
3485
+ ? result.data as { action?: string; real?: boolean; nextCommand?: string; stateFile?: string; setupGuidePath?: string; setupGuideJsonPath?: string; nextActions?: string[] }
3486
+ : {};
3487
+ if (data.setupGuidePath) {
3488
+ lines.push(`FGA setup guide: ${data.setupGuidePath}`);
3489
+ }
3490
+ if (data.setupGuideJsonPath) {
3491
+ lines.push(`FGA setup JSON: ${data.setupGuideJsonPath}`);
3492
+ }
3493
+ if (result.ok && data.action === "plan") {
3494
+ lines.push(`WorkOS FGA plan passed; run ${data.nextCommand ?? "forge workos fga sync --json"}.`);
3495
+ } else if (result.ok && data.action === "sync") {
3496
+ lines.push(`WorkOS FGA sync recorded${data.stateFile ? ` in ${data.stateFile}` : ""}; run ${data.nextCommand ?? "forge workos fga prove --json"}.`);
3497
+ } else if (result.ok && data.action === "prove") {
3498
+ lines.push(`WorkOS FGA proof passed${data.real ? " for real-mode state" : " locally"}; production deploy gates can inspect the FGA state.`);
3499
+ } else if (result.ok && data.action === "doctor") {
3500
+ lines.push(`WorkOS FGA doctor passed${data.real ? " for real production gates" : " for local planning"}; run ${data.nextCommand ?? "forge workos fga sync --json"} if you need the next proof step.`);
3501
+ } else if (!result.ok) {
3502
+ lines.push("WorkOS FGA check failed; inspect manifest diagnostics, seed coverage, and FGA state.");
3503
+ for (const action of data.nextActions ?? []) {
3504
+ lines.push(` - ${action}`);
3505
+ }
3506
+ }
3507
+ }
3508
+ return `${lines.join("\n")}\n`;
3509
+ }