forgeos 0.1.0-alpha.60 → 0.1.0-alpha.61
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 +16 -0
- package/README.md +47 -0
- package/docs/changelog.md +57 -1
- 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 +108 -11
- package/src/forge/cli/commands.ts +10 -0
- package/src/forge/cli/deploy.ts +57 -6
- package/src/forge/cli/env-placeholders.ts +62 -0
- package/src/forge/cli/field-test.ts +5 -3
- package/src/forge/cli/golden-path.ts +487 -0
- package/src/forge/cli/main.ts +4 -0
- package/src/forge/cli/parse.ts +77 -2
- package/src/forge/cli/secrets.ts +6 -0
- package/src/forge/cli/workos.ts +204 -10
- 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.61 input=6dd290dbb57f6f75a802f741028ac6dbd672aa09d598ba2639ffcec7fa7feb7d content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
|
|
2
2
|
# AGENTS.md
|
|
3
3
|
|
|
4
4
|
<!-- forge-generated:start -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# forgeos
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.61
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Harden the WorkOS golden path and production deploy gate.
|
|
8
|
+
|
|
9
|
+
- Add the ForgeOS golden path status/plan flow for the official
|
|
10
|
+
`create -> auth -> field-test -> deploy` path.
|
|
11
|
+
- Add WorkOS real-auth proof helpers and env synchronization for app and web
|
|
12
|
+
workspaces, including backend-owned AuthKit session bridge guidance.
|
|
13
|
+
- Block production readiness when `deploy/.env.production` still contains
|
|
14
|
+
placeholder or dummy values, preventing local/demo WorkOS and database
|
|
15
|
+
settings from being mistaken for publishable production evidence.
|
|
16
|
+
- Expand field-test, deploy, env doctor, WorkOS, and golden-path regression
|
|
17
|
+
coverage, plus docs for the production-shaped WorkOS vendor-access flow.
|
|
18
|
+
|
|
3
19
|
## 0.1.0-alpha.60
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -75,6 +75,53 @@ npm run dev -- --open
|
|
|
75
75
|
Template apps ignore `src/forge/_generated/`, `forge.lock`, and operational `.forge/**` work dirs by default so a freshly created app does not flood git or the editor with generated files. Run `forge generate` after checkout or before verification to recreate the agent contract, client SDK, frontend graph, capability map, and runtime manifests.
|
|
76
76
|
Templates also include workspace editor excludes for generated/runtime directories so source files stay visually prominent.
|
|
77
77
|
|
|
78
|
+
## Production-Shaped Golden Path
|
|
79
|
+
|
|
80
|
+
For a real multi-tenant app with WorkOS/AuthKit, start with the golden-path
|
|
81
|
+
planner instead of piecing together commands from docs:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
forge golden-path plan --auth workos --target docker --real --production --json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The canonical path is:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
forge field-test create vendor-access --auth workos --template vendor-access --package-manager npm --install --git --json
|
|
91
|
+
cd vendor-access
|
|
92
|
+
npm run forge -- workos env --client-id client_... --write --json
|
|
93
|
+
npm run forge -- workos setup --real --file workos-seed.yml --json
|
|
94
|
+
npm run forge -- auth prove --provider workos --real --client-id client_... --file workos-seed.yml --json
|
|
95
|
+
npm run forge -- field-test run --realistic --json
|
|
96
|
+
npm run forge -- deploy readiness --production --json
|
|
97
|
+
npm run forge -- deploy package --target docker
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For an existing app, ask ForgeOS for the current blocking stage:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
forge golden-path status --real --production --json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
It reports whether the app can publish, which stage blocks publication, and the
|
|
107
|
+
next command to run. WorkOS FGA is optional; ordinary B2B apps can ship with
|
|
108
|
+
AuthKit, RBAC permission claims, Forge policies, and tenant isolation.
|
|
109
|
+
|
|
110
|
+
If the AuthKit client id is already known, pass it to the status command so the
|
|
111
|
+
next action is copy/paste exact instead of using `client_...` placeholders:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
forge golden-path status --real --production --client-id client_... --json
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
When the WorkOS CLI can confirm a client id exists but cannot expose the value,
|
|
118
|
+
`golden-path status` asks for `forge workos env --client-id client_... --write
|
|
119
|
+
--json`; ForgeOS then writes the OIDC/JWKS and web public env values without
|
|
120
|
+
dashboard clicks or shell-specific env syntax. The higher-level
|
|
121
|
+
`forge auth prove --provider workos --real --client-id client_... --file
|
|
122
|
+
workos-seed.yml --json` wrapper performs the same env preparation before it
|
|
123
|
+
proves hosted WorkOS setup.
|
|
124
|
+
|
|
78
125
|
For release or external smoke testing, choose the Forge package source explicitly:
|
|
79
126
|
|
|
80
127
|
```bash
|
package/docs/changelog.md
CHANGED
|
@@ -4,7 +4,63 @@ Release history for the `forgeos` npm package.
|
|
|
4
4
|
|
|
5
5
|
The canonical source file in the repository is `CHANGELOG.md`.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 0.1.0-alpha.61
|
|
8
|
+
|
|
9
|
+
- Added `forge golden-path plan/status` as the canonical
|
|
10
|
+
create -> WorkOS auth -> field-test -> Docker deploy production path for
|
|
11
|
+
agents and users.
|
|
12
|
+
- `forge golden-path plan/status` now accepts `--forge-spec`, so maintainers can
|
|
13
|
+
run the same P0 path against `npm:forgeos@alpha`, a packed tarball, or a local
|
|
14
|
+
checkout before publishing.
|
|
15
|
+
- `forge golden-path status --real --production --json` now wraps WorkOS
|
|
16
|
+
posture, field-test evidence, and deploy readiness into one answer with the
|
|
17
|
+
current stage, blockers, and next command.
|
|
18
|
+
- `forge golden-path plan/status` now accepts `--client-id client_...`, so
|
|
19
|
+
agents that already know the AuthKit client id get exact WorkOS env/proof
|
|
20
|
+
commands instead of placeholder next actions.
|
|
21
|
+
- `forge golden-path status` now includes the first blocking stage in
|
|
22
|
+
`summary.blockers` (for example `workos-real-seed` or `workos-doctor`) before
|
|
23
|
+
lower-level deploy readiness blockers, so agents do not have to infer the
|
|
24
|
+
real next action.
|
|
25
|
+
- Added `forge auth prove --provider workos --real --client-id client_...
|
|
26
|
+
--file workos-seed.yml --json` as the provider-aware auth proof wrapper
|
|
27
|
+
around hosted WorkOS setup, seed evidence, public AuthKit env preparation, and
|
|
28
|
+
CLI login instructions.
|
|
29
|
+
- `forge auth prove --provider workos --real --client-id ...` now reads the
|
|
30
|
+
`.env.local` values it just prepared, so a successful real WorkOS proof reports
|
|
31
|
+
`mode: "oidc"`/`productionReady: true` instead of falling back to local
|
|
32
|
+
`dev-headers` in the same process.
|
|
33
|
+
- Added `forge workos env --client-id client_... --write --json` to prepare `.env.local` and
|
|
34
|
+
`web/.env.local` for real WorkOS/AuthKit proof, including audience/JWKS values
|
|
35
|
+
when `WORKOS_CLIENT_ID` is supplied and an explicit next action when the CLI
|
|
36
|
+
reports a client id exists but cannot expose the value.
|
|
37
|
+
- `forge golden-path plan/status --real` now includes the WorkOS env preparation
|
|
38
|
+
gate before hosted setup/seed proof, so agents fix missing client/JWKS envs
|
|
39
|
+
before retrying AuthKit login or deploy readiness.
|
|
40
|
+
- In `--real` mode, `forge golden-path status` now treats missing or stale
|
|
41
|
+
`.workos-seed-state.json` evidence as an auth-stage blocker and points agents
|
|
42
|
+
to `forge auth prove --provider workos --real --file workos-seed.yml --json`
|
|
43
|
+
after env is ready, or the `--client-id client_...` form when agents need the
|
|
44
|
+
proof command to prepare WorkOS env in the same step.
|
|
45
|
+
- `forge golden-path status` now returns compact field-test/deploy summaries
|
|
46
|
+
instead of embedding full command stdout, keeping the JSON safe for agents to
|
|
47
|
+
parse after realistic field-test runs.
|
|
48
|
+
- `forge deploy plan` and generated Docker production README output now point
|
|
49
|
+
back to `forge golden-path status --real --production --json` as the first
|
|
50
|
+
app-to-production triage command.
|
|
51
|
+
- `forge deploy readiness/check` now prioritizes the deploy env action that
|
|
52
|
+
actually unblocks production, such as copying
|
|
53
|
+
`deploy/.env.production.example` to `deploy/.env.production` before rerunning
|
|
54
|
+
auth diagnostics.
|
|
55
|
+
- `forge env doctor --target production` and `forge deploy check --production`
|
|
56
|
+
now reject known placeholder/dummy production env values without printing the
|
|
57
|
+
secret value, preventing copied examples like `client_test_local` or
|
|
58
|
+
`sk_test_dummy_do_not_use` from being treated as deploy-ready evidence.
|
|
59
|
+
- Docs now surface the WorkOS real + Docker deploy golden path from the CLI,
|
|
60
|
+
golden-path, field-testing, production-readiness pages, and README.
|
|
61
|
+
- Added a redacted WorkOS real vendor-access field report documenting hosted
|
|
62
|
+
setup, idempotent seed, AuthKit session bridge, tenant normalization,
|
|
63
|
+
policy denial, field-test evidence, and production deploy blockers.
|
|
8
64
|
|
|
9
65
|
## 0.1.0-alpha.60
|
|
10
66
|
|
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.61","releaseId":"forgeos@0.1.0-alpha.61+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.61 input=6dd290dbb57f6f75a802f741028ac6dbd672aa09d598ba2639ffcec7fa7feb7d content=82fdee65e1219bbb3fa9572a4c904c7e1e4061b00430769528426ece3b3a4067
|
|
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.61",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.61+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
package/src/forge/cli/auth.ts
CHANGED
|
@@ -4,11 +4,12 @@ import {
|
|
|
4
4
|
FORGE_AUTH_INVALID_AUDIENCE,
|
|
5
5
|
FORGE_AUTH_JWKS_FAILED,
|
|
6
6
|
} from "../compiler/diagnostics/codes.ts";
|
|
7
|
-
import { loadAuthConfigFromEnv, type AuthClaimsMapping } from "../runtime/auth/config.ts";
|
|
7
|
+
import { AUTH_ENV, loadAuthConfigFromEnv, type AuthClaimsMapping } from "../runtime/auth/config.ts";
|
|
8
8
|
import { mapClaimsToAuthContext } from "../runtime/auth/claims.ts";
|
|
9
9
|
import { ForgeAuthError } from "../runtime/auth/errors.ts";
|
|
10
10
|
import { verifyJwtToken } from "../runtime/auth/verifier.ts";
|
|
11
11
|
import { loadSecretRegistry } from "../runtime/secrets/check.ts";
|
|
12
|
+
import { loadEnvFiles } from "../runtime/secrets/env-loader.ts";
|
|
12
13
|
import { existsSync } from "node:fs";
|
|
13
14
|
import { join } from "node:path";
|
|
14
15
|
import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
|
|
@@ -17,6 +18,9 @@ import {
|
|
|
17
18
|
collectPolicyPermissions,
|
|
18
19
|
missingValues,
|
|
19
20
|
parseSeedFile,
|
|
21
|
+
runWorkOSCommand,
|
|
22
|
+
type WorkOSCommandResult,
|
|
23
|
+
type WorkOSCommandRunner,
|
|
20
24
|
} from "./workos.ts";
|
|
21
25
|
|
|
22
26
|
export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove" | "status";
|
|
@@ -28,6 +32,11 @@ export interface AuthCommandOptions {
|
|
|
28
32
|
token?: string;
|
|
29
33
|
prod?: boolean;
|
|
30
34
|
scenario?: string;
|
|
35
|
+
provider?: "workos";
|
|
36
|
+
real?: boolean;
|
|
37
|
+
file?: string;
|
|
38
|
+
clientId?: string;
|
|
39
|
+
commandRunner?: WorkOSCommandRunner;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
export interface AuthCommandResult {
|
|
@@ -38,6 +47,42 @@ export interface AuthCommandResult {
|
|
|
38
47
|
exitCode: 0 | 1;
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
const AUTH_CLI_ENV_KEYS = Object.values(AUTH_ENV);
|
|
51
|
+
|
|
52
|
+
function withCliAuthEnv<T>(workspaceRoot: string, fn: () => T): T {
|
|
53
|
+
const { store } = loadEnvFiles({ workspaceRoot });
|
|
54
|
+
const previous = AUTH_CLI_ENV_KEYS.map((key) => ({
|
|
55
|
+
key,
|
|
56
|
+
had: Object.prototype.hasOwnProperty.call(process.env, key),
|
|
57
|
+
value: process.env[key],
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
for (const key of AUTH_CLI_ENV_KEYS) {
|
|
61
|
+
if (process.env[key] === undefined) {
|
|
62
|
+
const value = store.resolve(key);
|
|
63
|
+
if (value !== undefined) {
|
|
64
|
+
process.env[key] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return fn();
|
|
71
|
+
} finally {
|
|
72
|
+
for (const entry of previous) {
|
|
73
|
+
if (entry.had) {
|
|
74
|
+
process.env[entry.key] = entry.value;
|
|
75
|
+
} else {
|
|
76
|
+
delete process.env[entry.key];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loadAuthConfigForCli(workspaceRoot: string) {
|
|
83
|
+
return withCliAuthEnv(workspaceRoot, () => loadAuthConfigFromEnv(workspaceRoot));
|
|
84
|
+
}
|
|
85
|
+
|
|
41
86
|
function detectWorkOS(workspaceRoot: string, claims: AuthClaimsMapping) {
|
|
42
87
|
const secretRegistry = loadSecretRegistry(workspaceRoot);
|
|
43
88
|
const secretNames = new Set((secretRegistry?.secrets ?? []).map((secret) => secret.name));
|
|
@@ -94,7 +139,7 @@ function configErrors(config: ReturnType<typeof loadAuthConfigFromEnv>): { code:
|
|
|
94
139
|
}
|
|
95
140
|
|
|
96
141
|
function buildAuthPosture(workspaceRoot: string) {
|
|
97
|
-
const config =
|
|
142
|
+
const config = loadAuthConfigForCli(workspaceRoot);
|
|
98
143
|
const productionMode = config.mode === "jwt" || config.mode === "oidc";
|
|
99
144
|
const errors = configErrors(config);
|
|
100
145
|
const configReady = errors.length === 0;
|
|
@@ -272,8 +317,41 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
|
|
|
272
317
|
});
|
|
273
318
|
}
|
|
274
319
|
|
|
320
|
+
function runProviderProof(options: AuthCommandOptions): WorkOSCommandResult | null {
|
|
321
|
+
if (options.provider !== "workos") return null;
|
|
322
|
+
const file = options.file ?? "workos-seed.yml";
|
|
323
|
+
if (options.real && options.clientId) {
|
|
324
|
+
const env = runWorkOSCommand({
|
|
325
|
+
subcommand: "env",
|
|
326
|
+
workspaceRoot: options.workspaceRoot,
|
|
327
|
+
json: true,
|
|
328
|
+
file,
|
|
329
|
+
yes: false,
|
|
330
|
+
dryRun: false,
|
|
331
|
+
real: true,
|
|
332
|
+
write: true,
|
|
333
|
+
clientId: options.clientId,
|
|
334
|
+
commandRunner: options.commandRunner,
|
|
335
|
+
});
|
|
336
|
+
if (!env.ok) {
|
|
337
|
+
return env;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return runWorkOSCommand({
|
|
341
|
+
subcommand: "prove",
|
|
342
|
+
workspaceRoot: options.workspaceRoot,
|
|
343
|
+
json: true,
|
|
344
|
+
file,
|
|
345
|
+
yes: false,
|
|
346
|
+
dryRun: !options.real,
|
|
347
|
+
real: options.real ?? false,
|
|
348
|
+
clientId: options.clientId,
|
|
349
|
+
commandRunner: options.commandRunner,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
275
353
|
function validateConfig(workspaceRoot: string): AuthCommandResult {
|
|
276
|
-
const config =
|
|
354
|
+
const config = loadAuthConfigForCli(workspaceRoot);
|
|
277
355
|
const errors = configErrors(config);
|
|
278
356
|
const workos = detectWorkOS(workspaceRoot, config.claims);
|
|
279
357
|
|
|
@@ -298,7 +376,7 @@ function validateConfig(workspaceRoot: string): AuthCommandResult {
|
|
|
298
376
|
}
|
|
299
377
|
|
|
300
378
|
function publicConfig(workspaceRoot: string): AuthCommandResult {
|
|
301
|
-
const config =
|
|
379
|
+
const config = loadAuthConfigForCli(workspaceRoot);
|
|
302
380
|
const workos = detectWorkOS(workspaceRoot, config.claims);
|
|
303
381
|
return {
|
|
304
382
|
ok: true,
|
|
@@ -319,7 +397,7 @@ function publicConfig(workspaceRoot: string): AuthCommandResult {
|
|
|
319
397
|
}
|
|
320
398
|
|
|
321
399
|
function decodeToken(workspaceRoot: string, token: string | undefined): AuthCommandResult {
|
|
322
|
-
const config =
|
|
400
|
+
const config = loadAuthConfigForCli(workspaceRoot);
|
|
323
401
|
if (!token) {
|
|
324
402
|
return {
|
|
325
403
|
ok: false,
|
|
@@ -354,7 +432,7 @@ async function testToken(
|
|
|
354
432
|
workspaceRoot: string,
|
|
355
433
|
token: string | undefined,
|
|
356
434
|
): Promise<AuthCommandResult> {
|
|
357
|
-
const config =
|
|
435
|
+
const config = loadAuthConfigForCli(workspaceRoot);
|
|
358
436
|
if (!token) {
|
|
359
437
|
return {
|
|
360
438
|
ok: false,
|
|
@@ -430,8 +508,9 @@ export async function runAuthCommand(
|
|
|
430
508
|
};
|
|
431
509
|
}
|
|
432
510
|
if (options.subcommand === "prove") {
|
|
511
|
+
const providerProof = runProviderProof(options);
|
|
433
512
|
const checked = validateConfig(options.workspaceRoot);
|
|
434
|
-
const config =
|
|
513
|
+
const config = loadAuthConfigForCli(options.workspaceRoot);
|
|
435
514
|
const workos = detectWorkOS(options.workspaceRoot, config.claims);
|
|
436
515
|
const productionMode = config.mode === "jwt" || config.mode === "oidc";
|
|
437
516
|
const workosClaimsOk = workos.claimStatus.every((claim) => claim.ok);
|
|
@@ -444,7 +523,9 @@ export async function runAuthCommand(
|
|
|
444
523
|
: options.prod && tokenProof && !tokenProof.ok
|
|
445
524
|
? tokenProof.error
|
|
446
525
|
: undefined;
|
|
447
|
-
const proofOk = checked.ok &&
|
|
526
|
+
const proofOk = checked.ok &&
|
|
527
|
+
(!options.prod || (productionMode && tokenProof?.ok === true)) &&
|
|
528
|
+
(!providerProof || providerProof.ok);
|
|
448
529
|
const multiTenantProof = options.scenario === "multi-tenant"
|
|
449
530
|
? buildMultiTenantProof(options.workspaceRoot, workos, config.requiresTenant)
|
|
450
531
|
: null;
|
|
@@ -460,8 +541,11 @@ export async function runAuthCommand(
|
|
|
460
541
|
productionReady: productionMode && checked.ok,
|
|
461
542
|
prod: options.prod === true,
|
|
462
543
|
scenario: options.scenario ?? null,
|
|
544
|
+
provider: options.provider ?? null,
|
|
545
|
+
real: options.real === true,
|
|
463
546
|
authPosture: posture,
|
|
464
547
|
...(tokenProof ? { tokenProof: tokenProof.ok ? tokenProof.data : tokenProof.error } : {}),
|
|
548
|
+
...(providerProof ? { providerProof } : {}),
|
|
465
549
|
...(multiTenantProof ? { multiTenantProof } : {}),
|
|
466
550
|
invariants: [
|
|
467
551
|
{
|
|
@@ -495,9 +579,19 @@ export async function runAuthCommand(
|
|
|
495
579
|
},
|
|
496
580
|
],
|
|
497
581
|
workos,
|
|
582
|
+
nextActions: [
|
|
583
|
+
...(providerProof && !providerProof.ok
|
|
584
|
+
? providerProof.data && typeof providerProof.data === "object" && "nextCommand" in providerProof.data
|
|
585
|
+
? [String((providerProof.data as { nextCommand?: unknown }).nextCommand)]
|
|
586
|
+
: ["forge workos prove --real --file workos-seed.yml --json"]
|
|
587
|
+
: []),
|
|
588
|
+
...(options.provider === "workos" && providerProof?.ok
|
|
589
|
+
? ["forge auth prove --scenario multi-tenant --json", "forge deploy readiness --production --json"]
|
|
590
|
+
: []),
|
|
591
|
+
],
|
|
498
592
|
checkedAt: "deterministic",
|
|
499
593
|
},
|
|
500
|
-
error: prodError ?? checked.error,
|
|
594
|
+
error: prodError ?? (!providerProof || providerProof.ok ? checked.error : { code: "FORGE_AUTH_PROVIDER_PROOF_FAILED", message: "WorkOS provider proof failed" }),
|
|
501
595
|
exitCode: proofOk && scenarioOk ? 0 : 1,
|
|
502
596
|
};
|
|
503
597
|
}
|
|
@@ -511,7 +605,7 @@ export async function runAuthCommand(
|
|
|
511
605
|
return testToken(options.workspaceRoot, options.token);
|
|
512
606
|
}
|
|
513
607
|
|
|
514
|
-
const config =
|
|
608
|
+
const config = loadAuthConfigForCli(options.workspaceRoot);
|
|
515
609
|
return {
|
|
516
610
|
ok: true,
|
|
517
611
|
mode: config.mode,
|
|
@@ -540,9 +634,12 @@ function authPostureFromResult(result: AuthCommandResult): Record<string, unknow
|
|
|
540
634
|
export function formatAuthHuman(result: AuthCommandResult): string {
|
|
541
635
|
const posture = authPostureFromResult(result);
|
|
542
636
|
if (posture) {
|
|
637
|
+
const data = result.data && typeof result.data === "object" ? result.data as Record<string, unknown> : {};
|
|
543
638
|
const classification = typeof posture.classification === "string" ? posture.classification : result.mode;
|
|
544
639
|
const reason = typeof posture.reason === "string" ? posture.reason : result.error?.message;
|
|
545
|
-
const
|
|
640
|
+
const postureNextActions = Array.isArray(posture.nextActions) ? posture.nextActions.filter((item): item is string => typeof item === "string") : [];
|
|
641
|
+
const proofNextActions = Array.isArray(data.nextActions) ? data.nextActions.filter((item): item is string => typeof item === "string") : [];
|
|
642
|
+
const nextActions = Array.from(new Set([...proofNextActions, ...postureNextActions]));
|
|
546
643
|
const lines = [
|
|
547
644
|
`Auth ${result.mode}: ${result.ok ? "ok" : "failed"} (${classification})`,
|
|
548
645
|
`Production ready: ${posture.productionReady === true ? "yes" : "no"}`,
|
|
@@ -226,6 +226,11 @@ import {
|
|
|
226
226
|
formatFieldTestJson,
|
|
227
227
|
runFieldTestCommand,
|
|
228
228
|
} from "./field-test.ts";
|
|
229
|
+
import {
|
|
230
|
+
formatGoldenPathHuman,
|
|
231
|
+
formatGoldenPathJson,
|
|
232
|
+
runGoldenPathCommand,
|
|
233
|
+
} from "./golden-path.ts";
|
|
229
234
|
import {
|
|
230
235
|
formatSeedHuman,
|
|
231
236
|
formatSeedJson,
|
|
@@ -2144,6 +2149,11 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
2144
2149
|
process.stdout.write(command.json ? formatDeployJson(result) : formatDeployHuman(result));
|
|
2145
2150
|
return result.exitCode;
|
|
2146
2151
|
}
|
|
2152
|
+
case "golden-path": {
|
|
2153
|
+
const result = await runGoldenPathCommand(command);
|
|
2154
|
+
process.stdout.write(command.json ? formatGoldenPathJson(result) : formatGoldenPathHuman(result));
|
|
2155
|
+
return result.exitCode;
|
|
2156
|
+
}
|
|
2147
2157
|
case "field-test": {
|
|
2148
2158
|
const result = await runFieldTestCommand(command);
|
|
2149
2159
|
process.stdout.write(command.json ? formatFieldTestJson(result) : formatFieldTestHuman(result));
|
package/src/forge/cli/deploy.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { join } from "node:path";
|
|
|
12
12
|
import { runAuthCommand } from "./auth.ts";
|
|
13
13
|
import { runAuthMdCommand } from "./authmd.ts";
|
|
14
14
|
import { runWorkOSCommand } from "./workos.ts";
|
|
15
|
+
import { productionPlaceholderEnvProblems } from "./env-placeholders.ts";
|
|
15
16
|
|
|
16
17
|
export type DeploySubcommand = "plan" | "init" | "check" | "readiness" | "render" | "package" | "verify";
|
|
17
18
|
export type DeployTarget = "docker" | "forge-cloud";
|
|
@@ -125,7 +126,7 @@ function deployStageForCheck(name: string): string {
|
|
|
125
126
|
if (name.startsWith("auth-md") || name === "oauth-protected-resource") return "auth metadata";
|
|
126
127
|
if (name.startsWith("field-test")) return "field test";
|
|
127
128
|
if (name === "live-production") return "live/runtime";
|
|
128
|
-
if (name === "deploy-env-sources") return "environment";
|
|
129
|
+
if (name === "deploy-env-sources" || name === "production-env-values") return "environment";
|
|
129
130
|
return "production";
|
|
130
131
|
}
|
|
131
132
|
|
|
@@ -588,6 +589,13 @@ function processDeployEnv(): Record<string, string> {
|
|
|
588
589
|
);
|
|
589
590
|
}
|
|
590
591
|
|
|
592
|
+
function productionEnvValues(workspaceRoot: string): Record<string, string> {
|
|
593
|
+
return {
|
|
594
|
+
...readDeployEnvFile(workspaceRoot),
|
|
595
|
+
...processDeployEnv(),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
591
599
|
function deployEnvSources(workspaceRoot: string): Record<string, unknown> {
|
|
592
600
|
const processEnv = processDeployEnv();
|
|
593
601
|
const deployEnv = readDeployEnvFile(workspaceRoot);
|
|
@@ -601,10 +609,7 @@ function deployEnvSources(workspaceRoot: string): Record<string, unknown> {
|
|
|
601
609
|
keys: envKeys(values),
|
|
602
610
|
};
|
|
603
611
|
});
|
|
604
|
-
const productionEvidence =
|
|
605
|
-
...deployEnv,
|
|
606
|
-
...processEnv,
|
|
607
|
-
};
|
|
612
|
+
const productionEvidence = productionEnvValues(workspaceRoot);
|
|
608
613
|
return {
|
|
609
614
|
readOrder: ["process.env", DEPLOY_ENV_FILE],
|
|
610
615
|
note: `${DEPLOY_ENV_FILE} and current process env are production deploy evidence; .env/.env.local are reported only as local guidance and are not used to pass production gates.`,
|
|
@@ -690,6 +695,16 @@ function renderProductionReadme(): string {
|
|
|
690
695
|
|
|
691
696
|
This directory is generated by \`forge deploy init --target docker\` or \`forge deploy package --target docker\`.
|
|
692
697
|
|
|
698
|
+
Start with the golden-path status gate when you are unsure what comes next:
|
|
699
|
+
|
|
700
|
+
\`\`\`bash
|
|
701
|
+
forge golden-path status --real --production --json
|
|
702
|
+
\`\`\`
|
|
703
|
+
|
|
704
|
+
It reads WorkOS hosted setup evidence, field-test evidence, and deploy readiness
|
|
705
|
+
without hiding the lower-level commands. If it cannot publish, it returns the
|
|
706
|
+
current blocking stage and the next command to run.
|
|
707
|
+
|
|
693
708
|
## 1. Prepare production env
|
|
694
709
|
|
|
695
710
|
\`\`\`bash
|
|
@@ -760,6 +775,7 @@ forge deploy verify --production --url https://app.example.com --json
|
|
|
760
775
|
function buildPlan(options: DeployCommandOptions): DeployCommandResult {
|
|
761
776
|
const commands = options.target === "forge-cloud"
|
|
762
777
|
? [
|
|
778
|
+
"forge golden-path status --real --production --json",
|
|
763
779
|
"forge deploy check --production --json",
|
|
764
780
|
"forge auth check --production --json",
|
|
765
781
|
"forge authmd check --json",
|
|
@@ -768,6 +784,7 @@ function buildPlan(options: DeployCommandOptions): DeployCommandResult {
|
|
|
768
784
|
"forge deploy verify --production --url https://<your-forge-cloud-app> --json",
|
|
769
785
|
]
|
|
770
786
|
: [
|
|
787
|
+
"forge golden-path status --real --production --json",
|
|
771
788
|
"forge deploy init --target docker",
|
|
772
789
|
"cp deploy/.env.production.example deploy/.env.production",
|
|
773
790
|
"forge authmd generate --json",
|
|
@@ -810,6 +827,7 @@ function buildPlan(options: DeployCommandOptions): DeployCommandResult {
|
|
|
810
827
|
notes: [
|
|
811
828
|
"dev-headers auth is local-only and must not be enabled for public runtime.",
|
|
812
829
|
"Use forge workos doctor/seed when WorkOS is configured.",
|
|
830
|
+
"Use forge golden-path status --real --production --json when the next blocking stage is unclear.",
|
|
813
831
|
"WorkOS FGA is optional; ordinary WorkOS apps can be production-ready with AuthKit/RBAC permission claims, Forge policies, and tenant isolation.",
|
|
814
832
|
"Use forge test authz or HTTP probes to prove cross-tenant denial.",
|
|
815
833
|
],
|
|
@@ -858,6 +876,22 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
|
|
|
858
876
|
message: `${DEPLOY_ENV_FILE} and process.env are production deploy evidence; .env/.env.local are local guidance only`,
|
|
859
877
|
details: deployEnvSources(options.workspaceRoot),
|
|
860
878
|
});
|
|
879
|
+
const placeholderEnvProblems = options.production
|
|
880
|
+
? productionPlaceholderEnvProblems(productionEnvValues(options.workspaceRoot), DEPLOY_ENV_KEYS as unknown as string[])
|
|
881
|
+
: [];
|
|
882
|
+
checks.push({
|
|
883
|
+
name: "production-env-values",
|
|
884
|
+
ok: placeholderEnvProblems.length === 0,
|
|
885
|
+
severity: "error",
|
|
886
|
+
message: placeholderEnvProblems.length === 0
|
|
887
|
+
? "production env values do not contain known placeholders"
|
|
888
|
+
: `production env contains placeholder value(s): ${placeholderEnvProblems.map((problem) => problem.key).join(", ")}`,
|
|
889
|
+
command: "edit deploy/.env.production with real production values",
|
|
890
|
+
details: {
|
|
891
|
+
placeholders: placeholderEnvProblems,
|
|
892
|
+
valuesRedacted: true,
|
|
893
|
+
},
|
|
894
|
+
});
|
|
861
895
|
|
|
862
896
|
const auth = await withDeployEnv(options.workspaceRoot, async () => loadAuthConfigFromEnv(options.workspaceRoot));
|
|
863
897
|
const productionAuth = auth.mode === "jwt" || auth.mode === "oidc";
|
|
@@ -1076,9 +1110,10 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
|
|
|
1076
1110
|
});
|
|
1077
1111
|
|
|
1078
1112
|
const errorFree = checks.every((check) => check.ok || check.severity === "warning");
|
|
1079
|
-
const
|
|
1113
|
+
const rawFailureActions = unique(checks.flatMap((check) =>
|
|
1080
1114
|
!check.ok && check.command ? expandDeployFailureCommand(check.command) : []
|
|
1081
1115
|
));
|
|
1116
|
+
const failureActions = prioritizeDeployFailureActions(options, checks, rawFailureActions);
|
|
1082
1117
|
const nextActions = errorFree
|
|
1083
1118
|
? ["forge deploy verify --production --url https://app.example.com --json"]
|
|
1084
1119
|
: failureActions;
|
|
@@ -1110,6 +1145,22 @@ function expandDeployFailureCommand(command: string): string[] {
|
|
|
1110
1145
|
return [command];
|
|
1111
1146
|
}
|
|
1112
1147
|
|
|
1148
|
+
function prioritizeDeployFailureActions(
|
|
1149
|
+
options: DeployCommandOptions,
|
|
1150
|
+
checks: DeployCheck[],
|
|
1151
|
+
actions: string[],
|
|
1152
|
+
): string[] {
|
|
1153
|
+
const database = checks.find((check) => check.name === "database-url");
|
|
1154
|
+
if (!database || database.ok || !database.command) {
|
|
1155
|
+
return actions;
|
|
1156
|
+
}
|
|
1157
|
+
const deployEnvMissing = !nodeFileSystem.exists(join(options.workspaceRoot, DEPLOY_ENV_FILE));
|
|
1158
|
+
if (deployEnvMissing && database.command === "cp deploy/.env.production.example deploy/.env.production") {
|
|
1159
|
+
return unique([database.command, ...actions.filter((action) => action !== database.command)]);
|
|
1160
|
+
}
|
|
1161
|
+
return actions;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1113
1164
|
function renderDocker(options: DeployCommandOptions): DeployCommandResult {
|
|
1114
1165
|
const dir = join(options.workspaceRoot, "deploy");
|
|
1115
1166
|
nodeFileSystem.mkdirp(dir);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface PlaceholderEnvProblem {
|
|
2
|
+
key: string;
|
|
3
|
+
reason: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const GENERIC_PLACEHOLDER_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [
|
|
7
|
+
{ pattern: /\bexample\.(com|test|org|net)\b/, reason: "example domain" },
|
|
8
|
+
{ pattern: /\bdummy\b/, reason: "dummy marker" },
|
|
9
|
+
{ pattern: /\bplaceholder\b/, reason: "placeholder marker" },
|
|
10
|
+
{ pattern: /\bchange[-_]?me\b/, reason: "change-me marker" },
|
|
11
|
+
{ pattern: /\bdo[-_]?not[-_]?use\b/, reason: "do-not-use marker" },
|
|
12
|
+
{ pattern: /^<.*>$/, reason: "angle-bracket placeholder" },
|
|
13
|
+
{ pattern: /^your[-_]/, reason: "your-value placeholder" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const KEY_PLACEHOLDER_PATTERNS: Record<string, Array<{ pattern: RegExp; reason: string }>> = {
|
|
17
|
+
DATABASE_URL: [
|
|
18
|
+
{ pattern: /^postgres:\/\/forge:forge@postgres:5432\/forge_app$/i, reason: "generated Docker example database URL" },
|
|
19
|
+
{ pattern: /^postgres:\/\/forge:forge@db:5432\/forge$/i, reason: "local dummy database URL" },
|
|
20
|
+
{ pattern: /^postgres:\/\/local-only\b/i, reason: "local-only database URL" },
|
|
21
|
+
],
|
|
22
|
+
WORKOS_API_KEY: [
|
|
23
|
+
{ pattern: /^sk_test_dummy/i, reason: "dummy WorkOS API key" },
|
|
24
|
+
{ pattern: /^sk_test_?\.\.\.$/i, reason: "placeholder WorkOS API key" },
|
|
25
|
+
],
|
|
26
|
+
WORKOS_CLIENT_ID: [
|
|
27
|
+
{ pattern: /^client_?\.\.\.$/i, reason: "placeholder WorkOS client id" },
|
|
28
|
+
{ pattern: /^client_test_local$/i, reason: "local fake WorkOS client id" },
|
|
29
|
+
],
|
|
30
|
+
WORKOS_COOKIE_PASSWORD: [
|
|
31
|
+
{ pattern: /dummy/i, reason: "dummy cookie password" },
|
|
32
|
+
{ pattern: /change[-_]?me/i, reason: "change-me cookie password" },
|
|
33
|
+
],
|
|
34
|
+
FORGE_AUTH_AUDIENCE: [
|
|
35
|
+
{ pattern: /^client_test_local$/i, reason: "local fake auth audience" },
|
|
36
|
+
],
|
|
37
|
+
FORGE_AUTH_JWKS_URI: [
|
|
38
|
+
{ pattern: /\/client_test_local$/i, reason: "local fake JWKS client id" },
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function placeholderEnvProblem(key: string, value: string | undefined): PlaceholderEnvProblem | null {
|
|
43
|
+
const trimmed = value?.trim() ?? "";
|
|
44
|
+
if (!trimmed) return null;
|
|
45
|
+
const normalized = trimmed.toLowerCase();
|
|
46
|
+
for (const { pattern, reason } of KEY_PLACEHOLDER_PATTERNS[key] ?? []) {
|
|
47
|
+
if (pattern.test(trimmed)) return { key, reason };
|
|
48
|
+
}
|
|
49
|
+
for (const { pattern, reason } of GENERIC_PLACEHOLDER_PATTERNS) {
|
|
50
|
+
if (pattern.test(normalized)) return { key, reason };
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function productionPlaceholderEnvProblems(
|
|
56
|
+
values: Record<string, string>,
|
|
57
|
+
keys = Object.keys(values),
|
|
58
|
+
): PlaceholderEnvProblem[] {
|
|
59
|
+
return keys
|
|
60
|
+
.map((key) => placeholderEnvProblem(key, values[key]))
|
|
61
|
+
.filter((problem): problem is PlaceholderEnvProblem => Boolean(problem));
|
|
62
|
+
}
|
|
@@ -317,6 +317,7 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
|
|
|
317
317
|
};
|
|
318
318
|
}
|
|
319
319
|
if (options.dryRun) {
|
|
320
|
+
const forgeSpecFlag = options.forgeSpec ? ` --forge-spec ${options.forgeSpec}` : "";
|
|
320
321
|
return normalizeForgeCliCommandsInValue(options.workspaceRoot, {
|
|
321
322
|
schemaVersion: "0.1.0",
|
|
322
323
|
ok: true,
|
|
@@ -328,11 +329,12 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
|
|
|
328
329
|
template: options.template,
|
|
329
330
|
packageManager: options.packageManager,
|
|
330
331
|
auth: options.auth,
|
|
332
|
+
forgeSpec: options.forgeSpec,
|
|
331
333
|
install: true,
|
|
332
334
|
git: true,
|
|
333
|
-
command: `forge new ${options.name} --template ${options.template} --package-manager ${options.packageManager} --field-test --install`,
|
|
335
|
+
command: `forge new ${options.name} --template ${options.template} --package-manager ${options.packageManager}${forgeSpecFlag} --field-test --install`,
|
|
334
336
|
goldenPath: [
|
|
335
|
-
`forge field-test create ${options.name} --auth ${options.auth ?? "none"} --template ${options.template} --package-manager ${options.packageManager} --install --git --json`,
|
|
337
|
+
`forge field-test create ${options.name} --auth ${options.auth ?? "none"} --template ${options.template} --package-manager ${options.packageManager}${forgeSpecFlag} --install --git --json`,
|
|
336
338
|
`cd ${options.name}`,
|
|
337
339
|
"forge field-test run --realistic --json",
|
|
338
340
|
"forge field-test report --json",
|
|
@@ -341,7 +343,7 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
|
|
|
341
343
|
],
|
|
342
344
|
},
|
|
343
345
|
nextActions: [
|
|
344
|
-
`forge field-test create ${options.name} --auth ${options.auth ?? "none"} --template ${options.template} --package-manager ${options.packageManager} --install --git --json`,
|
|
346
|
+
`forge field-test create ${options.name} --auth ${options.auth ?? "none"} --template ${options.template} --package-manager ${options.packageManager}${forgeSpecFlag} --install --git --json`,
|
|
345
347
|
],
|
|
346
348
|
exitCode: 0,
|
|
347
349
|
});
|