forgeos 0.1.0-alpha.55 → 0.1.0-alpha.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +38 -2
- package/docs/changelog.md +32 -0
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/cli/auth.ts +10 -2
- package/src/forge/cli/commands.ts +2 -0
- package/src/forge/cli/deploy.ts +169 -22
- package/src/forge/cli/dev.ts +5 -2
- package/src/forge/cli/main.ts +6 -1
- package/src/forge/cli/parse.ts +15 -5
- package/src/forge/cli/secrets.ts +131 -1
- package/src/forge/cli/workos.ts +69 -11
- package/src/forge/compiler/integration/add.ts +64 -23
- package/src/forge/compiler/integration/templates/workos.ts +65 -30
- package/src/forge/compiler/orchestrator/run.ts +4 -2
- package/src/forge/compiler/recipes/profiles.ts +81 -0
- package/src/forge/dev/server.ts +163 -11
- package/src/forge/ui/index.ts +3 -21
- package/src/forge/version.ts +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.57 input=7128597ce3f6cd158bf4361cebb1eef8678c19ff9b2cb60c454a0876356d37c6 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
|
|
2
2
|
# AGENTS.md
|
|
3
3
|
|
|
4
4
|
<!-- forge-generated:start -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# forgeos
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.57
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fix the WorkOS AuthKit local and tunneled development flow.
|
|
8
|
+
|
|
9
|
+
- Add `response_type=code` to Forge-owned WorkOS authorization URLs and avoid sending a default `organization_id` during login so AuthKit can present normal sign-in and organization selection.
|
|
10
|
+
- Resolve WorkOS organization and membership claims during callback when AuthKit returns only the user/token, then expose Forge-normalized session claims through `/session`.
|
|
11
|
+
- Add `forge dev --public-api-url` for browser-facing API URLs behind tunnels and update the WorkOS React bridge to use backend-owned `/login`, `/session`, and `/logout` routes through same-origin/proxy-aware URL resolution.
|
|
12
|
+
- Sync public WorkOS browser env into `web/.env.local` during `forge add auth workos` and `forge workos setup --real`, and update WorkOS doctor/UI audit checks for the backend-owned AuthKit session flow.
|
|
13
|
+
|
|
14
|
+
## 0.1.0-alpha.56
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Add the production deploy golden path as an explicit CLI workflow.
|
|
19
|
+
|
|
20
|
+
- `forge deploy init --target docker` now acts as the canonical Docker
|
|
21
|
+
production bootstrap command for runtime files, `deploy/.env.production`
|
|
22
|
+
guidance, and production handoff docs.
|
|
23
|
+
- `forge deploy readiness --production --json` now summarizes whether an app
|
|
24
|
+
can publish, grouped blockers/warnings, readiness score, and the next
|
|
25
|
+
command an agent should run.
|
|
26
|
+
- `forge env doctor --target local|staging|production --json` now reports the
|
|
27
|
+
effective auth mode, database posture, provider detection, missing env
|
|
28
|
+
names, and safe source metadata without printing secret values.
|
|
29
|
+
- `forge deploy check --production --json` now includes blocking/warning
|
|
30
|
+
summaries and the readiness object used by the deploy gate.
|
|
31
|
+
- `forge deploy verify --production --url ... --json` now validates required
|
|
32
|
+
public auth metadata and probes optional runtime endpoints such as
|
|
33
|
+
`/ready`, `/live/status`, `/outbox/status`, and `/webhooks/workos`.
|
|
34
|
+
- Production docs now present the canonical flow:
|
|
35
|
+
`deploy init -> env doctor -> auth/auth.md/WorkOS proof -> field-test ->
|
|
36
|
+
readiness -> check -> package -> verify`, while keeping WorkOS FGA
|
|
37
|
+
explicitly optional unless the app opts into FGA artifacts.
|
|
38
|
+
|
|
3
39
|
## 0.1.0-alpha.55
|
|
4
40
|
|
|
5
41
|
### Patch Changes
|
|
@@ -90,7 +126,7 @@
|
|
|
90
126
|
- Tighten the no-dashboard WorkOS real setup/proof handoff.
|
|
91
127
|
|
|
92
128
|
- WorkOS hosted setup now records explicit `WORKOS_MODE=agent npx --yes
|
|
93
|
-
|
|
129
|
+
workos@latest auth login --json` shell guidance when CLI login is required.
|
|
94
130
|
- Failed `forge workos setup --real` login attempts now tell the operator to
|
|
95
131
|
rerun the same setup command after completing OAuth/device-code login,
|
|
96
132
|
instead of pointing generically at `prove --real`.
|
|
@@ -251,7 +287,7 @@
|
|
|
251
287
|
`forge dev --seed --all-tenants`, and seed readiness evidence in dev and
|
|
252
288
|
field-test summaries.
|
|
253
289
|
- Update templates, docs, and tests around the new `create app -> add auth ->
|
|
254
|
-
|
|
290
|
+
field-test -> deploy` path.
|
|
255
291
|
|
|
256
292
|
## 0.1.0-alpha.41
|
|
257
293
|
|
package/docs/changelog.md
CHANGED
|
@@ -6,6 +6,38 @@ The canonical source file in the repository is `CHANGELOG.md`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.1.0-alpha.57
|
|
10
|
+
|
|
11
|
+
- Fixed the WorkOS/AuthKit login path used by local and tunneled development:
|
|
12
|
+
Forge-owned authorization URLs now include `response_type=code` and no longer
|
|
13
|
+
send a default `organization_id` unless explicitly requested.
|
|
14
|
+
- The WorkOS callback now resolves organization and membership claims when
|
|
15
|
+
AuthKit returns only the user/token, then exposes Forge-normalized session
|
|
16
|
+
claims through `/session`.
|
|
17
|
+
- Added `forge dev --public-api-url` so browser-facing API URLs can be injected
|
|
18
|
+
cleanly for SSH tunnels, Docker Desktop, and remote dev hosts.
|
|
19
|
+
- The generated WorkOS React bridge now uses backend-owned `/login`, `/session`,
|
|
20
|
+
and `/logout` routes through same-origin/proxy-aware URL resolution.
|
|
21
|
+
- `forge add auth workos`, `forge workos setup --real`, `workos doctor`, and
|
|
22
|
+
`inspect ui --ergonomics` now understand the backend-owned AuthKit session
|
|
23
|
+
flow and sync public browser env into `web/.env.local`.
|
|
24
|
+
|
|
25
|
+
## 0.1.0-alpha.56
|
|
26
|
+
|
|
27
|
+
- Added `forge deploy init --target docker` as the canonical production Docker
|
|
28
|
+
bootstrap command.
|
|
29
|
+
- Added `forge deploy readiness --production --json` to answer whether an app
|
|
30
|
+
can publish, what blocks production, and which command should run next.
|
|
31
|
+
- Added `forge env doctor --target local|staging|production --json` for safe,
|
|
32
|
+
target-specific env source reporting without exposing secret values.
|
|
33
|
+
- `forge deploy check --production --json` now includes grouped
|
|
34
|
+
blockers/warnings and a readiness summary for agent handoffs.
|
|
35
|
+
- `forge deploy verify --production --url ... --json` now validates required
|
|
36
|
+
public auth metadata and optional operational endpoints.
|
|
37
|
+
- Production docs now describe the canonical flow from deploy init through
|
|
38
|
+
field-test, readiness, package, and verify, with WorkOS FGA kept optional
|
|
39
|
+
unless the app opts into FGA artifacts.
|
|
40
|
+
|
|
9
41
|
## 0.1.0-alpha.55
|
|
10
42
|
|
|
11
43
|
- `forge add auth workos` now defaults to AuthKit/RBAC, permission claims,
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.
|
|
1
|
+
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.57","releaseId":"forgeos@0.1.0-alpha.57+unknown","schemaVersion":"0.1.0"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.57 input=7128597ce3f6cd158bf4361cebb1eef8678c19ff9b2cb60c454a0876356d37c6 content=40c7132e14bfeffd8018f04de93db9ad294cfca4e190b21106d86bb80bc3bc03
|
|
2
2
|
export const releaseManifest = {
|
|
3
3
|
"defaultProvider": "local",
|
|
4
4
|
"diagnostics": [],
|
|
@@ -19,7 +19,7 @@ export const releaseManifest = {
|
|
|
19
19
|
"custom"
|
|
20
20
|
],
|
|
21
21
|
"packageName": "forgeos",
|
|
22
|
-
"packageVersion": "0.1.0-alpha.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
22
|
+
"packageVersion": "0.1.0-alpha.57",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.57+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
package/src/forge/cli/auth.ts
CHANGED
|
@@ -180,11 +180,16 @@ function permissionSubsetForRestrictedTenant(permissions: string[]): string[] {
|
|
|
180
180
|
function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof detectWorkOS>, requiresTenant: boolean) {
|
|
181
181
|
const rootSeedPresent = existsSync(join(workspaceRoot, "workos-seed.yml"));
|
|
182
182
|
const generatedSeedPresent = existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/workos-seed.yml"));
|
|
183
|
+
const fgaArtifactsPresent =
|
|
184
|
+
existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/fga.ts")) ||
|
|
185
|
+
existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/resource-map.ts")) ||
|
|
186
|
+
existsSync(join(workspaceRoot, ".workos-fga-state.json"));
|
|
183
187
|
const authMdPresent = existsSync(join(workspaceRoot, "public/auth.md"));
|
|
184
188
|
const metadataPresent = existsSync(join(workspaceRoot, "public/.well-known/oauth-protected-resource"));
|
|
185
189
|
const activePermissions = collectPolicyPermissions(workspaceRoot);
|
|
186
190
|
const expectedResourceTypes = collectExpectedResourceTypes(workspaceRoot);
|
|
187
191
|
const seed = parseSeedFile(workspaceRoot);
|
|
192
|
+
const fgaEnabled = fgaArtifactsPresent || seed.resourceTypes.length > 0;
|
|
188
193
|
const missingSeedPermissions = missingValues(activePermissions, seed.permissions);
|
|
189
194
|
const missingSeedResources = missingValues(expectedResourceTypes, seed.resourceTypes);
|
|
190
195
|
const acmePermissions = activePermissions;
|
|
@@ -223,8 +228,10 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
|
|
|
223
228
|
},
|
|
224
229
|
{
|
|
225
230
|
id: "seed-resource-coverage",
|
|
226
|
-
ok: missingSeedResources.length === 0,
|
|
227
|
-
evidence:
|
|
231
|
+
ok: !fgaEnabled || missingSeedResources.length === 0,
|
|
232
|
+
evidence: !fgaEnabled
|
|
233
|
+
? "FGA is not enabled; seed resource type coverage is not required for AuthKit/RBAC tenant isolation."
|
|
234
|
+
: missingSeedResources.length === 0
|
|
228
235
|
? `seed covers expected app resource type(s): ${expectedResourceTypes.join(", ") || "none required"}`
|
|
229
236
|
: `seed missing expected app resource type(s): ${missingSeedResources.join(", ")}`,
|
|
230
237
|
},
|
|
@@ -244,6 +251,7 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
|
|
|
244
251
|
appContract: {
|
|
245
252
|
activePermissions,
|
|
246
253
|
expectedResourceTypes,
|
|
254
|
+
fgaEnabled,
|
|
247
255
|
seedPath: seed.path,
|
|
248
256
|
seedPermissions: seed.permissions,
|
|
249
257
|
seedResourceTypes: seed.resourceTypes,
|
|
@@ -2709,6 +2709,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
2709
2709
|
webOnly: command.webOnly,
|
|
2710
2710
|
open: command.open,
|
|
2711
2711
|
webPort: command.webPort,
|
|
2712
|
+
publicApiUrl: command.publicApiUrl,
|
|
2712
2713
|
telemetry: command.telemetry,
|
|
2713
2714
|
envFile: command.envFile,
|
|
2714
2715
|
skipStartupConsole: command.skipStartupConsole,
|
|
@@ -2863,6 +2864,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
2863
2864
|
workspaceRoot: command.workspaceRoot,
|
|
2864
2865
|
json: command.json,
|
|
2865
2866
|
redacted: command.redacted,
|
|
2867
|
+
target: command.target,
|
|
2866
2868
|
});
|
|
2867
2869
|
|
|
2868
2870
|
if (command.json) {
|
package/src/forge/cli/deploy.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { runAuthCommand } from "./auth.ts";
|
|
|
13
13
|
import { runAuthMdCommand } from "./authmd.ts";
|
|
14
14
|
import { runWorkOSCommand } from "./workos.ts";
|
|
15
15
|
|
|
16
|
-
export type DeploySubcommand = "plan" | "check" | "render" | "package" | "verify";
|
|
16
|
+
export type DeploySubcommand = "plan" | "init" | "check" | "readiness" | "render" | "package" | "verify";
|
|
17
17
|
export type DeployTarget = "docker" | "forge-cloud";
|
|
18
18
|
|
|
19
19
|
export interface DeployCommandOptions {
|
|
@@ -42,6 +42,8 @@ export interface DeployCommandResult {
|
|
|
42
42
|
target: DeployTarget;
|
|
43
43
|
production: boolean;
|
|
44
44
|
checks: DeployCheck[];
|
|
45
|
+
blocking?: string[];
|
|
46
|
+
warnings?: string[];
|
|
45
47
|
files?: string[];
|
|
46
48
|
plan?: {
|
|
47
49
|
summary: string;
|
|
@@ -50,6 +52,7 @@ export interface DeployCommandResult {
|
|
|
50
52
|
notes: string[];
|
|
51
53
|
};
|
|
52
54
|
probes?: Array<{ method: string; url: string; ok: boolean; status?: number; contentType?: string; error?: string; jsonValid?: boolean }>;
|
|
55
|
+
readiness?: DeployReadinessSummary;
|
|
53
56
|
nextActions: string[];
|
|
54
57
|
exitCode: 0 | 1;
|
|
55
58
|
}
|
|
@@ -57,6 +60,27 @@ export interface DeployCommandResult {
|
|
|
57
60
|
interface DeployProbeExpectation {
|
|
58
61
|
contentTypeIncludes?: string;
|
|
59
62
|
json?: boolean;
|
|
63
|
+
required?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface DeployReadinessSummary {
|
|
67
|
+
score: number;
|
|
68
|
+
status: "ready" | "blocked" | "warning";
|
|
69
|
+
blocking: string[];
|
|
70
|
+
warnings: string[];
|
|
71
|
+
stages: Array<{
|
|
72
|
+
name: string;
|
|
73
|
+
ok: boolean;
|
|
74
|
+
score: number;
|
|
75
|
+
checks: string[];
|
|
76
|
+
blocking: string[];
|
|
77
|
+
warnings: string[];
|
|
78
|
+
}>;
|
|
79
|
+
answers: {
|
|
80
|
+
canPublish: boolean;
|
|
81
|
+
ifNotWhatIsMissing: string[];
|
|
82
|
+
nextCommand: string | null;
|
|
83
|
+
};
|
|
60
84
|
}
|
|
61
85
|
|
|
62
86
|
function readGeneratedJson<T>(workspaceRoot: string, relative: string): T | null {
|
|
@@ -93,6 +117,74 @@ function unique(values: string[]): string[] {
|
|
|
93
117
|
return [...new Set(values)];
|
|
94
118
|
}
|
|
95
119
|
|
|
120
|
+
function deployStageForCheck(name: string): string {
|
|
121
|
+
if (["generated", "package-lockfile", "frontend-build-script"].includes(name)) return "build";
|
|
122
|
+
if (["database-url"].includes(name)) return "database";
|
|
123
|
+
if (name.startsWith("auth-") || ["production-auth-mode", "auth-issuer", "auth-audience", "auth-jwks", "tenant-claim"].includes(name)) return "auth";
|
|
124
|
+
if (name.startsWith("workos")) return "workos";
|
|
125
|
+
if (name.startsWith("auth-md") || name === "oauth-protected-resource") return "auth metadata";
|
|
126
|
+
if (name.startsWith("field-test")) return "field test";
|
|
127
|
+
if (name === "live-production") return "live/runtime";
|
|
128
|
+
if (name === "deploy-env-sources") return "environment";
|
|
129
|
+
return "production";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function summarizeDeployReadiness(checks: DeployCheck[], nextActions: string[]): DeployReadinessSummary {
|
|
133
|
+
const blocking = checks
|
|
134
|
+
.filter((check) => !check.ok && check.severity === "error")
|
|
135
|
+
.map((check) => `${check.name}: ${check.message}`);
|
|
136
|
+
const warnings = checks
|
|
137
|
+
.filter((check) => !check.ok && check.severity === "warning")
|
|
138
|
+
.map((check) => `${check.name}: ${check.message}`);
|
|
139
|
+
const stageNames = [
|
|
140
|
+
"build",
|
|
141
|
+
"environment",
|
|
142
|
+
"database",
|
|
143
|
+
"auth",
|
|
144
|
+
"workos",
|
|
145
|
+
"auth metadata",
|
|
146
|
+
"field test",
|
|
147
|
+
"live/runtime",
|
|
148
|
+
"production",
|
|
149
|
+
];
|
|
150
|
+
const stages = stageNames
|
|
151
|
+
.map((stage) => {
|
|
152
|
+
const stageChecks = checks.filter((check) => deployStageForCheck(check.name) === stage);
|
|
153
|
+
const stageBlocking = stageChecks
|
|
154
|
+
.filter((check) => !check.ok && check.severity === "error")
|
|
155
|
+
.map((check) => `${check.name}: ${check.message}`);
|
|
156
|
+
const stageWarnings = stageChecks
|
|
157
|
+
.filter((check) => !check.ok && check.severity === "warning")
|
|
158
|
+
.map((check) => `${check.name}: ${check.message}`);
|
|
159
|
+
const passed = stageChecks.filter((check) => check.ok || check.severity === "warning").length;
|
|
160
|
+
const score = stageChecks.length === 0 ? 100 : Math.round((passed / stageChecks.length) * 100);
|
|
161
|
+
return {
|
|
162
|
+
name: stage,
|
|
163
|
+
ok: stageBlocking.length === 0,
|
|
164
|
+
score,
|
|
165
|
+
checks: stageChecks.map((check) => check.name),
|
|
166
|
+
blocking: stageBlocking,
|
|
167
|
+
warnings: stageWarnings,
|
|
168
|
+
};
|
|
169
|
+
})
|
|
170
|
+
.filter((stage) => stage.checks.length > 0);
|
|
171
|
+
const passed = checks.filter((check) => check.ok || check.severity === "warning").length;
|
|
172
|
+
const score = checks.length === 0 ? 100 : Math.round((passed / checks.length) * 100);
|
|
173
|
+
const canPublish = blocking.length === 0;
|
|
174
|
+
return {
|
|
175
|
+
score,
|
|
176
|
+
status: blocking.length > 0 ? "blocked" : warnings.length > 0 ? "warning" : "ready",
|
|
177
|
+
blocking,
|
|
178
|
+
warnings,
|
|
179
|
+
stages,
|
|
180
|
+
answers: {
|
|
181
|
+
canPublish,
|
|
182
|
+
ifNotWhatIsMissing: blocking,
|
|
183
|
+
nextCommand: canPublish ? "forge deploy verify --production --url https://app.example.com --json" : nextActions[0] ?? null,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
96
188
|
function hasWorkOSIntegration(workspaceRoot: string): boolean {
|
|
97
189
|
const secrets = new Set(requiredSecretNames(workspaceRoot));
|
|
98
190
|
return secrets.has("WORKOS_API_KEY") ||
|
|
@@ -596,7 +688,7 @@ function databaseReadyCommand(options: DeployCommandOptions): string {
|
|
|
596
688
|
function renderProductionReadme(): string {
|
|
597
689
|
return `# ForgeOS Production Deploy
|
|
598
690
|
|
|
599
|
-
This directory is generated by \`forge deploy package --target docker\`.
|
|
691
|
+
This directory is generated by \`forge deploy init --target docker\` or \`forge deploy package --target docker\`.
|
|
600
692
|
|
|
601
693
|
## 1. Prepare production env
|
|
602
694
|
|
|
@@ -619,13 +711,17 @@ For WorkOS-backed apps, local dry-runs are not enough for production deploy
|
|
|
619
711
|
readiness. Run:
|
|
620
712
|
|
|
621
713
|
\`\`\`bash
|
|
714
|
+
forge workos setup --real --file workos-seed.yml --json
|
|
622
715
|
forge workos prove --real --file workos-seed.yml --json
|
|
623
716
|
\`\`\`
|
|
624
717
|
|
|
625
|
-
This applies or confirms hosted
|
|
626
|
-
writes \`.workos-seed-state.json\`. \`forge deploy
|
|
627
|
-
that state to match the current \`workos-seed.yml\`
|
|
628
|
-
posture as production evidence.
|
|
718
|
+
This applies or confirms hosted redirect/CORS/webhook config and seed data
|
|
719
|
+
through the WorkOS CLI and writes \`.workos-seed-state.json\`. \`forge deploy
|
|
720
|
+
check --production\` requires that state to match the current \`workos-seed.yml\`
|
|
721
|
+
before treating WorkOS posture as production evidence. WorkOS FGA is optional:
|
|
722
|
+
most apps can ship with AuthKit, RBAC permission claims, Forge policies, and
|
|
723
|
+
tenant isolation; use \`forge add auth workos --with-fga\` only when the app needs
|
|
724
|
+
resource-level authorization outside normal role/permission checks.
|
|
629
725
|
|
|
630
726
|
## 2. Prove the app before traffic
|
|
631
727
|
|
|
@@ -633,9 +729,12 @@ posture as production evidence.
|
|
|
633
729
|
forge generate --check --json
|
|
634
730
|
forge check --json
|
|
635
731
|
forge authmd generate --json
|
|
732
|
+
forge env doctor --target production --json
|
|
636
733
|
forge auth prove --scenario multi-tenant --json
|
|
734
|
+
forge workos setup --real --file workos-seed.yml --json
|
|
637
735
|
forge workos prove --real --file workos-seed.yml --json
|
|
638
736
|
forge field-test run --realistic --json
|
|
737
|
+
forge deploy readiness --production --json
|
|
639
738
|
forge deploy check --production --json
|
|
640
739
|
\`\`\`
|
|
641
740
|
|
|
@@ -654,7 +753,7 @@ forge auth prove --prod --token <jwt> --json
|
|
|
654
753
|
forge deploy verify --production --url https://app.example.com --json
|
|
655
754
|
\`\`\`
|
|
656
755
|
|
|
657
|
-
\`forge auth prove --prod\` verifies a real JWT/OIDC token against the configured issuer/audience/JWKS. \`forge deploy verify --production\` probes \`GET /health\`, \`HEAD /auth.md\`, \`GET /auth.md\`, \`HEAD /.well-known/oauth-protected-resource\`, and \`GET /.well-known/oauth-protected-resource\`; it also validates auth metadata content-types and requires the OAuth protected-resource metadata body to be valid JSON.
|
|
756
|
+
\`forge auth prove --prod\` verifies a real JWT/OIDC token against the configured issuer/audience/JWKS. \`forge deploy verify --production\` probes required endpoints \`GET /health\`, \`HEAD /auth.md\`, \`GET /auth.md\`, \`HEAD /.well-known/oauth-protected-resource\`, and \`GET /.well-known/oauth-protected-resource\`; it also validates auth metadata content-types and requires the OAuth protected-resource metadata body to be valid JSON. Optional probes check \`GET /ready\`, \`GET /live/status\`, \`GET /outbox/status\`, and \`HEAD /webhooks/workos\` when the app exposes those operational endpoints.
|
|
658
757
|
`;
|
|
659
758
|
}
|
|
660
759
|
|
|
@@ -669,10 +768,17 @@ function buildPlan(options: DeployCommandOptions): DeployCommandResult {
|
|
|
669
768
|
"forge deploy verify --production --url https://<your-forge-cloud-app> --json",
|
|
670
769
|
]
|
|
671
770
|
: [
|
|
672
|
-
"forge deploy
|
|
771
|
+
"forge deploy init --target docker",
|
|
673
772
|
"cp deploy/.env.production.example deploy/.env.production",
|
|
674
|
-
"forge
|
|
773
|
+
"forge authmd generate --json",
|
|
774
|
+
"forge env doctor --target production --json",
|
|
775
|
+
"forge auth prove --scenario multi-tenant --json",
|
|
776
|
+
"forge workos setup --real --file workos-seed.yml --json",
|
|
777
|
+
"forge workos prove --real --file workos-seed.yml --json",
|
|
675
778
|
FIELD_TEST_PRODUCTION_COMMAND,
|
|
779
|
+
"forge deploy readiness --production --json",
|
|
780
|
+
"forge deploy check --production --json",
|
|
781
|
+
"forge deploy package --target docker",
|
|
676
782
|
"docker compose -f deploy/docker-compose.yml up --build",
|
|
677
783
|
"forge deploy verify --production --url https://app.example.com --json",
|
|
678
784
|
];
|
|
@@ -699,10 +805,12 @@ function buildPlan(options: DeployCommandOptions): DeployCommandResult {
|
|
|
699
805
|
"field-test report exists with runtime/auth/UI probes",
|
|
700
806
|
"runtime /health responds",
|
|
701
807
|
"tenant and policy proof is run before public traffic",
|
|
808
|
+
"readiness score has no blocking items",
|
|
702
809
|
],
|
|
703
810
|
notes: [
|
|
704
811
|
"dev-headers auth is local-only and must not be enabled for public runtime.",
|
|
705
812
|
"Use forge workos doctor/seed when WorkOS is configured.",
|
|
813
|
+
"WorkOS FGA is optional; ordinary WorkOS apps can be production-ready with AuthKit/RBAC permission claims, Forge policies, and tenant isolation.",
|
|
706
814
|
"Use forge test authz or HTTP probes to prove cross-tenant denial.",
|
|
707
815
|
],
|
|
708
816
|
},
|
|
@@ -971,6 +1079,10 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
|
|
|
971
1079
|
const failureActions = unique(checks.flatMap((check) =>
|
|
972
1080
|
!check.ok && check.command ? expandDeployFailureCommand(check.command) : []
|
|
973
1081
|
));
|
|
1082
|
+
const nextActions = errorFree
|
|
1083
|
+
? ["forge deploy verify --production --url https://app.example.com --json"]
|
|
1084
|
+
: failureActions;
|
|
1085
|
+
const readiness = summarizeDeployReadiness(checks, nextActions);
|
|
974
1086
|
return normalizeForgeCliCommandsInValue(options.workspaceRoot, {
|
|
975
1087
|
schemaVersion: "0.1.0",
|
|
976
1088
|
ok: errorFree,
|
|
@@ -979,9 +1091,10 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
|
|
|
979
1091
|
target: options.target,
|
|
980
1092
|
production: options.production,
|
|
981
1093
|
checks,
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1094
|
+
blocking: readiness.blocking,
|
|
1095
|
+
warnings: readiness.warnings,
|
|
1096
|
+
readiness,
|
|
1097
|
+
nextActions,
|
|
985
1098
|
exitCode: errorFree ? 0 : 1,
|
|
986
1099
|
});
|
|
987
1100
|
}
|
|
@@ -1020,6 +1133,7 @@ function renderDocker(options: DeployCommandOptions): DeployCommandResult {
|
|
|
1020
1133
|
files: files.map(([file]) => file),
|
|
1021
1134
|
nextActions: [
|
|
1022
1135
|
"cp deploy/.env.production.example deploy/.env.production",
|
|
1136
|
+
"forge deploy readiness --production --json",
|
|
1023
1137
|
"forge deploy check --production --json",
|
|
1024
1138
|
"docker compose -f deploy/docker-compose.yml up --build",
|
|
1025
1139
|
],
|
|
@@ -1087,22 +1201,32 @@ async function verifyUrl(options: DeployCommandOptions): Promise<DeployCommandRe
|
|
|
1087
1201
|
exitCode: 1,
|
|
1088
1202
|
};
|
|
1089
1203
|
}
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1204
|
+
const probeSpecs: Array<{ method: "GET" | "HEAD"; path: string; expectation?: DeployProbeExpectation }> = [
|
|
1205
|
+
{ method: "GET", path: "/health", expectation: { required: true } },
|
|
1206
|
+
{ method: "HEAD", path: "/auth.md", expectation: { contentTypeIncludes: "text/markdown", required: true } },
|
|
1207
|
+
{ method: "GET", path: "/auth.md", expectation: { contentTypeIncludes: "text/markdown", required: true } },
|
|
1208
|
+
{ method: "HEAD", path: "/.well-known/oauth-protected-resource", expectation: { contentTypeIncludes: "application/json", required: true } },
|
|
1209
|
+
{ method: "GET", path: "/.well-known/oauth-protected-resource", expectation: { contentTypeIncludes: "application/json", json: true, required: true } },
|
|
1210
|
+
{ method: "GET", path: "/ready", expectation: { contentTypeIncludes: "application/json" } },
|
|
1211
|
+
{ method: "GET", path: "/live/status", expectation: { contentTypeIncludes: "application/json" } },
|
|
1212
|
+
{ method: "GET", path: "/outbox/status", expectation: { contentTypeIncludes: "application/json" } },
|
|
1213
|
+
{ method: "HEAD", path: "/webhooks/workos" },
|
|
1214
|
+
];
|
|
1215
|
+
const probes = await Promise.all(probeSpecs.map((item) =>
|
|
1216
|
+
probe(item.method, `${base}${item.path}`, item.expectation ?? {})
|
|
1217
|
+
));
|
|
1097
1218
|
const checks = probes.map((item): DeployCheck => ({
|
|
1098
1219
|
name: `${item.method} ${item.url.replace(base, "") || "/"}`,
|
|
1099
1220
|
ok: item.ok,
|
|
1100
|
-
severity:
|
|
1221
|
+
severity: probeSpecs.find((spec) => `${base}${spec.path}` === item.url && spec.method === item.method)?.expectation?.required || item.url.endsWith("/health")
|
|
1222
|
+
? "error"
|
|
1223
|
+
: "warning",
|
|
1101
1224
|
message: item.ok
|
|
1102
1225
|
? `HTTP ${item.status}${item.contentType ? ` ${item.contentType}` : ""}`
|
|
1103
1226
|
: item.error ?? `HTTP ${item.status ?? "failed"}`,
|
|
1104
1227
|
}));
|
|
1105
1228
|
const ok = checks.every((check) => check.ok || check.severity === "warning");
|
|
1229
|
+
const readiness = summarizeDeployReadiness(checks, ok ? [] : ["check runtime logs", "forge deploy check --production --json"]);
|
|
1106
1230
|
return {
|
|
1107
1231
|
schemaVersion: "0.1.0",
|
|
1108
1232
|
ok,
|
|
@@ -1111,16 +1235,33 @@ async function verifyUrl(options: DeployCommandOptions): Promise<DeployCommandRe
|
|
|
1111
1235
|
target: options.target,
|
|
1112
1236
|
production: options.production,
|
|
1113
1237
|
checks,
|
|
1238
|
+
blocking: readiness.blocking,
|
|
1239
|
+
warnings: readiness.warnings,
|
|
1114
1240
|
probes,
|
|
1115
|
-
|
|
1241
|
+
readiness,
|
|
1242
|
+
nextActions: ok
|
|
1243
|
+
? [
|
|
1244
|
+
"forge auth prove --prod --token <jwt> --json",
|
|
1245
|
+
"forge auth prove --scenario multi-tenant --json",
|
|
1246
|
+
"forge handoff --json",
|
|
1247
|
+
]
|
|
1248
|
+
: ["check runtime logs", "forge deploy check --production --json"],
|
|
1116
1249
|
exitCode: ok ? 0 : 1,
|
|
1117
1250
|
};
|
|
1118
1251
|
}
|
|
1119
1252
|
|
|
1120
1253
|
export async function runDeployCommand(options: DeployCommandOptions): Promise<DeployCommandResult> {
|
|
1121
1254
|
if (options.subcommand === "plan") return buildPlan(options);
|
|
1255
|
+
if (options.subcommand === "readiness") {
|
|
1256
|
+
const result = await buildChecks({ ...options, subcommand: "check", production: true });
|
|
1257
|
+
return {
|
|
1258
|
+
...result,
|
|
1259
|
+
action: "readiness",
|
|
1260
|
+
nextActions: result.readiness?.answers.nextCommand ? [result.readiness.answers.nextCommand] : result.nextActions,
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1122
1263
|
if (options.subcommand === "check") return buildChecks(options);
|
|
1123
|
-
if (options.subcommand === "render" || options.subcommand === "package") return renderDocker(options);
|
|
1264
|
+
if (options.subcommand === "init" || options.subcommand === "render" || options.subcommand === "package") return renderDocker(options);
|
|
1124
1265
|
return verifyUrl(options);
|
|
1125
1266
|
}
|
|
1126
1267
|
|
|
@@ -1132,7 +1273,13 @@ export function formatDeployHuman(result: DeployCommandResult): string {
|
|
|
1132
1273
|
const lines = [
|
|
1133
1274
|
`deploy ${result.action} ${result.ok ? "ok" : "failed"}`,
|
|
1134
1275
|
`target: ${result.target}`,
|
|
1276
|
+
...(result.readiness ? [
|
|
1277
|
+
`readiness: ${result.readiness.status} (${result.readiness.score}/100)`,
|
|
1278
|
+
`can publish: ${result.readiness.answers.canPublish ? "yes" : "no"}`,
|
|
1279
|
+
] : []),
|
|
1135
1280
|
...result.checks.map((check) => `${check.ok ? "ok" : check.severity === "warning" ? "warn" : "fail"} ${check.name}: ${check.message}`),
|
|
1281
|
+
...(result.blocking?.length ? ["", "Blocking:", ...result.blocking.map((item) => ` ${item}`)] : []),
|
|
1282
|
+
...(result.warnings?.length ? ["", "Warnings:", ...result.warnings.map((item) => ` ${item}`)] : []),
|
|
1136
1283
|
...(result.files?.length ? ["", "Files:", ...result.files.map((file) => ` ${file}`)] : []),
|
|
1137
1284
|
...(result.plan ? ["", result.plan.summary, "", "Commands:", ...result.plan.commands.map((command) => ` ${command}`)] : []),
|
|
1138
1285
|
...(result.nextActions.length ? ["", "Next:", ...result.nextActions.map((action) => ` ${action}`)] : []),
|
package/src/forge/cli/dev.ts
CHANGED
|
@@ -53,6 +53,7 @@ export interface DevCommandOptions {
|
|
|
53
53
|
webOnly?: boolean;
|
|
54
54
|
open?: boolean;
|
|
55
55
|
webPort?: number;
|
|
56
|
+
publicApiUrl?: string;
|
|
56
57
|
telemetry: string[];
|
|
57
58
|
envFile?: string;
|
|
58
59
|
mode?: "dev" | "serve";
|
|
@@ -305,6 +306,7 @@ function buildDetachedDevArgs(options: DevCommandOptions): string[] {
|
|
|
305
306
|
if (options.webOnly) args.push("--web-only");
|
|
306
307
|
if (options.open) args.push("--open");
|
|
307
308
|
if (options.webPort !== undefined) args.push("--web-port", String(options.webPort));
|
|
309
|
+
if (options.publicApiUrl) args.push("--public-api-url", options.publicApiUrl);
|
|
308
310
|
if (options.telemetry.length > 0) args.push("--telemetry", options.telemetry.join(","));
|
|
309
311
|
if (options.envFile) args.push("--env-file", options.envFile);
|
|
310
312
|
if (options.seed) args.push("--seed");
|
|
@@ -1343,13 +1345,14 @@ export async function runDevCommand(
|
|
|
1343
1345
|
|
|
1344
1346
|
if (options.webOnly) {
|
|
1345
1347
|
const apiUrl = `http://${host}:${port}`;
|
|
1348
|
+
const publicApiUrl = options.publicApiUrl ?? apiUrl;
|
|
1346
1349
|
const webHandle = startWebDevServer({
|
|
1347
1350
|
workspaceRoot,
|
|
1348
1351
|
host,
|
|
1349
1352
|
port: webPort,
|
|
1350
1353
|
requestedPort: webPortSelection.requestedPort,
|
|
1351
1354
|
autoPortSelected: webPortSelection.autoPortSelected,
|
|
1352
|
-
apiUrl,
|
|
1355
|
+
apiUrl: publicApiUrl,
|
|
1353
1356
|
json: options.json,
|
|
1354
1357
|
});
|
|
1355
1358
|
if (!webHandle) {
|
|
@@ -1451,7 +1454,7 @@ export async function runDevCommand(
|
|
|
1451
1454
|
port: webPort,
|
|
1452
1455
|
requestedPort: webPortSelection.requestedPort,
|
|
1453
1456
|
autoPortSelected: webPortSelection.autoPortSelected,
|
|
1454
|
-
apiUrl: handle.url,
|
|
1457
|
+
apiUrl: options.publicApiUrl ?? handle.url,
|
|
1455
1458
|
json: options.json,
|
|
1456
1459
|
});
|
|
1457
1460
|
|
package/src/forge/cli/main.ts
CHANGED
|
@@ -20,6 +20,9 @@ function formatHelp(): string {
|
|
|
20
20
|
" forge field-test create vendor-access --auth workos --install --git --json Create a real field-test app",
|
|
21
21
|
" forge field-test run --realistic --templates vendor-access --package-managers npm --json",
|
|
22
22
|
" forge field-test report --json Summarize the machine-readable field-test report",
|
|
23
|
+
" forge deploy init --target docker --json Generate Docker production files, env template, and deploy README",
|
|
24
|
+
" forge deploy readiness --production --json Answer whether the app can publish, what blocks it, and the next command",
|
|
25
|
+
" forge env doctor --target production --json Show production env sources, missing keys, auth mode, database, and provider",
|
|
23
26
|
" forge seed status --json Discover app seed commands from the generated runtime graph",
|
|
24
27
|
" forge seed dev --json Run the app seed command against the local Forge dev runtime",
|
|
25
28
|
" forge seed dev --all-tenants --json Seed every discovered local tenant/persona profile",
|
|
@@ -51,7 +54,7 @@ function formatHelp(): string {
|
|
|
51
54
|
" forge deploy plan --target docker --json Explain production deploy gates and commands",
|
|
52
55
|
" forge deploy check --production --json Gate auth, DB, metadata, generated artifacts, and liveQuery readiness",
|
|
53
56
|
" forge deploy package --target docker Write Docker production deploy files under deploy/",
|
|
54
|
-
" forge deploy verify --production --url https://app.example.com --json Probe
|
|
57
|
+
" forge deploy verify --production --url https://app.example.com --json Probe health, public auth metadata, and optional runtime endpoints",
|
|
55
58
|
" forge release check --allow-missing-local-release --json Gate release readiness without failing on unprepared local artifacts",
|
|
56
59
|
" forge self-host check --prepared-only --json Report compose readiness without creating deploy files",
|
|
57
60
|
" forge delta status --verbose --json Include Delta schema, lock, and aggregate count details",
|
|
@@ -107,6 +110,7 @@ function formatDevHelp(): string {
|
|
|
107
110
|
" --db memory|pglite|postgres|none Choose the development database adapter",
|
|
108
111
|
" --port <port> API runtime port; use 0 for an ephemeral port",
|
|
109
112
|
" --web-port <port> Web dev server port; use 0 for an ephemeral port",
|
|
113
|
+
" --public-api-url <url> Browser-facing API URL injected into web env, useful for tunnels",
|
|
110
114
|
" --host <host> Bind host, default 127.0.0.1",
|
|
111
115
|
" --no-web Start API/runtime only",
|
|
112
116
|
" --api-only Start API/runtime only",
|
|
@@ -121,6 +125,7 @@ function formatDevHelp(): string {
|
|
|
121
125
|
"",
|
|
122
126
|
"Examples:",
|
|
123
127
|
" forge dev --db memory --port 3777 --web-port 5174",
|
|
128
|
+
" forge dev --public-api-url http://localhost:3765",
|
|
124
129
|
" forge dev --db memory --port 0 --web-port 0",
|
|
125
130
|
" forge dev --seed --db pglite",
|
|
126
131
|
" forge dev --seed --all-tenants --db pglite",
|