diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b813c9e3..bfc7088d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,9 @@ jobs: - name: Test core contracts run: npm run test:core + - name: Test OAuth and Console compatibility + run: npm run test:oauth-console + - name: Test CLI compatibility run: npm run test:cli diff --git a/docs/oauth-console-validation.md b/docs/oauth-console-validation.md new file mode 100644 index 00000000..7c4baefa --- /dev/null +++ b/docs/oauth-console-validation.md @@ -0,0 +1,50 @@ +# OAuth and Console validation + +This page records what Miftah's release gate proves for native remote OAuth and the optional local Console, what it deliberately does not prove, and which external evidence is still missing. It is an evidence record, not a claim that every OAuth provider is supported. + +## Automated compatibility gate + +Every pull request and push to `development` or `main` runs `npm run test:oauth-console` on Ubuntu, macOS, and Windows with Node.js 20, 22, and 24. The same matrix also runs Miftah's core and packaged-CLI contracts. A release candidate cannot rely only on the Linux full-test job. + +The dedicated suite uses deterministic local fixtures and does not contact live OAuth providers. It exercises: + +- protected-resource and authorization-server discovery, exact resource and issuer validation, PKCE, callback handoff, exchange, refresh, and bearer injection; +- connection/profile/upstream isolation, credential envelopes, expiry, identity mismatch, redaction, and audit integrity; +- configuration migration, connection planning and writes, CLI-only operation, Console lifecycle commands, and provider-adapter contracts; +- literal-loopback binding, single-use state, bounded cancellation and timeout cleanup, exact Console listener and Origin checks, CSRF, and browser launch boundaries. + +The secure-store tests validate the vault adapter boundary, credential-envelope isolation, module construction, and unavailable-vault diagnostics. The suite does not write test credentials into the operator's real OS vault. It also does not prove that a live provider granted the requested scopes or that the selected account is the intended account; those require provider and user evidence. + +## Recovery scenarios + +### Refresh and reauthorization + +An expiring credential is refreshed only after discovery is revalidated. Refresh failure never falls through to another profile or connection. `miftah auth reauth` keeps the existing credential until its replacement succeeds, and failed interactive authorization returns a typed diagnostic without printing provider output. + +### Disconnect and cleanup + +`miftah auth disconnect` deletes only the exact local vault binding and marks the connection disconnected. It does not claim provider-side revocation. Callback listeners and Console-owned resources close during success, failure, or shutdown. + +### Cancellation and callback timeout + +The loopback callback accepts one exact state, issuer, host, and path. Cancellation and timeout close the listener and discard transient authorization material without changing another connection or exposing callback parameters. + +### Configuration backup and recovery + +Connection writes are plan-first, reject symlink or concurrent-replacement races, and create a unique recovery backup before atomic installation. Stop active clients before restoring a backup, validate the restored file, and then restart clients. + +For provider-adapter-backed or upstream-owned OAuth, follow the provider's own reauthentication and cache-recovery instructions. Miftah does not import, repair, or replay opaque provider token caches. + +## Release status and limitations + +OAuth and Console are being validated for a pre-v1 feature release. Passing this automated gate is required release evidence, but it is not v1.0 readiness and does not close the demand or external-validation gates by itself. The current support classes and manual fallbacks remain defined in the [OAuth support matrix](oauth-support.md#support-matrix). + +## External design-partner evidence + +Snapshot date: 2026-07-22. + +- Interested external users: 5 +- Recorded completed external workflows: 0 +- Recorded returning external users: 0 + +The external design-partner gate remains open. Closing it requires five external users to complete real multi-account workflows and at least three to return after setup. Record only deidentified workflow, setup friction, return behavior, and rejected assumptions; never record credentials, OAuth codes, tokens, provider payloads, or personal account identifiers. diff --git a/docs/oauth-support.md b/docs/oauth-support.md index 6b79c9fd..70cd3e60 100644 --- a/docs/oauth-support.md +++ b/docs/oauth-support.md @@ -6,7 +6,7 @@ Miftah performs protected-resource and authorization-server discovery, browser a Miftah does not support OAuth for every MCP server or provider. It does not guess private endpoints, scrape provider caches, automate local STDIO providers' custom login flows, accept passwords or browser cookies as OAuth state, or treat a valid token as proof that the correct account was selected. Operator lifecycle commands manage only Miftah's exact local binding and vault credential; `auth disconnect` does not claim provider-side token revocation. -The [OAuth and local Console design delta](oauth-console-threat-model.md) records the enforced OAuth security controls, residual risks, and the separately launched local Console control-plane boundary. Run `miftah dashboard` for the optional browser-local UI, or `miftah console --config ` for the API-only compatibility surface. +The [OAuth and local Console design delta](oauth-console-threat-model.md) records the enforced OAuth security controls, residual risks, and the separately launched local Console control-plane boundary. The [OAuth and Console validation](oauth-console-validation.md) page records the cross-platform automated gate, recovery coverage, limitations, and external-user evidence separately. Run `miftah dashboard` for the optional browser-local UI, or `miftah console --config ` for the API-only compatibility surface. ## Support matrix diff --git a/package.json b/package.json index 1af71bbd..d1107e8b 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "dev": "tsup --watch", "test": "vitest run", "test:core": "vitest run tests/config.test.ts tests/config-loader.test.ts tests/config-diagnostics.test.ts tests/config-migration.test.ts tests/config-public-contract.test.ts tests/config-runtime-parity.test.ts tests/config-schema-contract.test.ts tests/executable-resolver.test.ts tests/http-server.test.ts tests/presets.test.ts tests/profile-manager.test.ts tests/provider-routing-matchers.test.ts tests/provider-routing-matchers-docs-contract.test.ts tests/routing-context.test.ts tests/routing-policy.test.ts tests/secret-provider-availability.test.ts tests/secret-providers.test.ts tests/secrets.test.ts tests/tooling-config.test.ts tests/windows-config-acl.test.ts tests/windows-config-migration-acl-failure.test.ts tests/windows-config-migration-acl.test.ts tests/windows-secret-process-resolution.test.ts", + "test:oauth-console": "vitest run tests/oauth tests/remote-oauth tests/console tests/identity tests/provider-adapter-contract.test.ts tests/preset-catalog.test.ts tests/init-command.test.ts tests/cli-exit-codes.test.ts tests/cli-parse.test.ts tests/config-migration.test.ts tests/audit-integrity.test.ts", "test:package": "vitest run tests/package-contract.test.ts", "test:coverage": "vitest run --coverage", "smoke:cli": "node dist/cli/main.js schema", diff --git a/tests/console-application-service.test.ts b/tests/console-application-service.test.ts index 52a90c91..f7ed3a9c 100644 --- a/tests/console-application-service.test.ts +++ b/tests/console-application-service.test.ts @@ -101,12 +101,12 @@ describe("Console application service", () => { expect(JSON.stringify(config)).not.toMatch(/token|secret|password/iu); const snippets = await service.clientSnippets("claude-desktop"); - expect(snippets).toEqual([ - expect.objectContaining({ - client: "claude-desktop", - json: expect.stringContaining(configPath) - }) - ]); + expect(snippets).toHaveLength(1); + expect(snippets[0]).toMatchObject({ client: "claude-desktop" }); + const snippetConfig = JSON.parse(snippets[0]?.json ?? "") as { + mcpServers: Record; + }; + expect(snippetConfig.mcpServers["posthog-work"]?.args).toContain(configPath); expect(JSON.stringify(snippets)).not.toContain("auth.example.test"); await expect(service.onboardNativeOAuth({ diff --git a/tests/console-server.test.ts b/tests/console-server.test.ts index 5687bb8b..6906c383 100644 --- a/tests/console-server.test.ts +++ b/tests/console-server.test.ts @@ -237,9 +237,14 @@ describe("local Console control server", () => { headers: { origin: server.url.origin, cookie: session.cookie } }); expect(snippets.status).toBe(200); - const snippetBody = await snippets.json(); + const snippetBody = await snippets.json() as { + data: Array<{ client: string; json: string }>; + }; expect(snippetBody).toMatchObject({ data: [{ client: "claude-desktop" }] }); - expect(JSON.stringify(snippetBody)).toContain(configPath); + const snippetConfig = JSON.parse(snippetBody.data[0]?.json ?? "") as { + mcpServers: Record; + }; + expect(snippetConfig.mcpServers["posthog-work"]?.args).toContain(configPath); expect(JSON.stringify(snippetBody)).not.toContain("auth.example.test"); } finally { await server.close(); diff --git a/tests/oauth-console-validation-contract.test.ts b/tests/oauth-console-validation-contract.test.ts new file mode 100644 index 00000000..299fcf86 --- /dev/null +++ b/tests/oauth-console-validation-contract.test.ts @@ -0,0 +1,68 @@ +import { readFile } from "node:fs/promises"; +import { describe, expect, it } from "vitest"; + +async function repositoryFile(path: string): Promise { + return readFile(new URL(`../${path}`, import.meta.url), "utf8"); +} + +function workflowJob(workflow: string, jobName: string): string | undefined { + const lines = workflow.split(/\r?\n/u); + const start = lines.findIndex((line) => line === ` ${jobName}:`); + if (start === -1) return undefined; + const nextJob = lines.findIndex((line, index) => index > start && /^ {2}[a-z0-9_-]+:$/u.test(line)); + return lines.slice(start, nextJob === -1 ? undefined : nextJob).join("\n"); +} + +describe("OAuth and Console validation contract", () => { + it("keeps the deterministic OAuth and Console suite in every supported OS and Node job", async () => { + const [packageJson, workflow] = await Promise.all([ + repositoryFile("package.json"), + repositoryFile(".github/workflows/ci.yml") + ]); + const packageManifest = JSON.parse(packageJson) as { + scripts?: Record; + }; + const compatibilityJob = workflowJob(workflow, "compatibility"); + + expect(packageManifest.scripts?.["test:oauth-console"]).toBe( + "vitest run tests/oauth tests/remote-oauth tests/console tests/identity " + + "tests/provider-adapter-contract.test.ts tests/preset-catalog.test.ts " + + "tests/init-command.test.ts tests/cli-exit-codes.test.ts tests/cli-parse.test.ts " + + "tests/config-migration.test.ts tests/audit-integrity.test.ts" + ); + expect(compatibilityJob).toBeDefined(); + expect(compatibilityJob).toContain("Test OAuth and Console compatibility"); + expect(compatibilityJob).toContain("run: npm run test:oauth-console"); + expect(compatibilityJob).toContain("os: [ubuntu-latest, macos-latest, windows-latest]"); + expect(compatibilityJob).toContain('node: ["20", "22", "24"]'); + }); + + it("publishes truthful automated, recovery, and external-user release evidence", async () => { + const [oauthSupport, validation] = await Promise.all([ + repositoryFile("docs/oauth-support.md"), + repositoryFile("docs/oauth-console-validation.md") + ]); + + expect(oauthSupport).toContain("[OAuth and Console validation](oauth-console-validation.md)"); + + for (const requiredEvidence of [ + "# OAuth and Console validation", + "Ubuntu, macOS, and Windows", + "Node.js 20, 22, and 24", + "deterministic local fixtures", + "does not contact live OAuth providers", + "does not write test credentials into the operator's real OS vault", + "Refresh and reauthorization", + "Disconnect and cleanup", + "Cancellation and callback timeout", + "Configuration backup and recovery", + "pre-v1 feature release", + "Interested external users: 5", + "Recorded completed external workflows: 0", + "Recorded returning external users: 0", + "The external design-partner gate remains open" + ]) { + expect(validation).toContain(requiredEvidence); + } + }); +});