From f9700d4b27d19ee00b88fa3c7cf22b965c2121bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:01:41 +0900 Subject: [PATCH 01/13] feat(identity): add bounded provider HTTP client --- .../src/oauth-provider-http-client.ts | 342 ++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 apps/identity-service/src/oauth-provider-http-client.ts diff --git a/apps/identity-service/src/oauth-provider-http-client.ts b/apps/identity-service/src/oauth-provider-http-client.ts new file mode 100644 index 000000000..915e3f975 --- /dev/null +++ b/apps/identity-service/src/oauth-provider-http-client.ts @@ -0,0 +1,342 @@ +const PROVIDER_REQUEST_FAILED = 'OAuth provider request failed'; +const DEFAULT_REQUEST_TIMEOUT_MS = 5_000; +const MINIMUM_REQUEST_TIMEOUT_MS = 100; +const MAXIMUM_REQUEST_TIMEOUT_MS = 10_000; +const MAXIMUM_REQUEST_BODY_BYTES = 16 * 1024; +const MAXIMUM_RESPONSE_BODY_BYTES = 64 * 1024; +const MAXIMUM_HEADER_VALUE_LENGTH = 8 * 1024; +const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/; +const SAFE_HEADER_VALUE_PATTERN = /^[^\u0000\r\n]*$/; + +interface EndpointPolicy { + method: 'GET' | 'POST'; + url: string; + requiredHeaders: Readonly>; + body: 'forbidden' | 'required'; +} + +const ENDPOINT_POLICIES: readonly EndpointPolicy[] = Object.freeze([ + { + method: 'POST', + url: 'https://oauth2.googleapis.com/token', + requiredHeaders: Object.freeze({ + accept: 'application/json', + 'content-type': 'application/x-www-form-urlencoded', + }), + body: 'required', + }, + { + method: 'POST', + url: 'https://github.com/login/oauth/access_token', + requiredHeaders: Object.freeze({ + accept: 'application/json', + 'content-type': 'application/x-www-form-urlencoded', + }), + body: 'required', + }, + { + method: 'GET', + url: 'https://api.github.com/user', + requiredHeaders: Object.freeze({ + accept: 'application/vnd.github+json', + authorization: /^Bearer [^\s]+$/, + 'user-agent': 'LifeOS', + 'x-github-api-version': /^\d{4}-\d{2}-\d{2}$/, + }), + body: 'forbidden', + }, + { + method: 'GET', + url: 'https://api.github.com/user/emails', + requiredHeaders: Object.freeze({ + accept: 'application/vnd.github+json', + authorization: /^Bearer [^\s]+$/, + 'user-agent': 'LifeOS', + 'x-github-api-version': /^\d{4}-\d{2}-\d{2}$/, + }), + body: 'forbidden', + }, + { + method: 'GET', + url: 'https://www.googleapis.com/oauth2/v3/certs', + requiredHeaders: Object.freeze({ accept: 'application/json' }), + body: 'forbidden', + }, +]); + +/** + * A provider request created by a fixed-endpoint OAuth request builder. + */ +export interface OAuthProviderHttpRequest { + url: string; + method: 'GET' | 'POST'; + headers: Headers; + body?: string; +} + +/** + * The bounded response shape consumed by provider-specific parsers. + */ +export interface OAuthProviderHttpResult { + status: number; + contentType: string; + body: string; +} + +/** + * Injectable fetch-compatible function used by the provider HTTP boundary. + */ +export type OAuthProviderFetch = ( + input: string, + init: RequestInit, +) => Promise; + +/** + * Configuration for the bounded provider HTTP boundary. + */ +export interface OAuthProviderHttpClientOptions { + fetchFunction?: OAuthProviderFetch; + timeoutMs?: number; +} + +function failProviderRequest(): never { + throw new Error(PROVIDER_REQUEST_FAILED); +} + +function requireTimeout(value: number | undefined): number { + const timeoutMs = value ?? DEFAULT_REQUEST_TIMEOUT_MS; + if ( + !Number.isSafeInteger(timeoutMs) || + timeoutMs < MINIMUM_REQUEST_TIMEOUT_MS || + timeoutMs > MAXIMUM_REQUEST_TIMEOUT_MS + ) { + return failProviderRequest(); + } + return timeoutMs; +} + +function requirePolicy( + request: OAuthProviderHttpRequest, +): EndpointPolicy { + if ( + !request || + typeof request !== 'object' || + (request.method !== 'GET' && request.method !== 'POST') || + typeof request.url !== 'string' + ) { + return failProviderRequest(); + } + + let parsedUrl: URL; + try { + parsedUrl = new URL(request.url); + } catch { + return failProviderRequest(); + } + if ( + parsedUrl.protocol !== 'https:' || + parsedUrl.username || + parsedUrl.password || + parsedUrl.hash || + parsedUrl.href !== request.url + ) { + return failProviderRequest(); + } + + const policy = ENDPOINT_POLICIES.find( + (candidate) => + candidate.method === request.method && candidate.url === request.url, + ); + return policy ?? failProviderRequest(); +} + +function normalizeHeaders(headersValue: object): Record { + if (!headersValue || typeof headersValue !== 'object' || Array.isArray(headersValue)) { + return failProviderRequest(); + } + + const headers = Object.create(null) as Record; + for (const [providedName, providedValue] of Object.entries(headersValue)) { + const name = providedName.toLowerCase(); + if ( + !HEADER_NAME_PATTERN.test(providedName) || + typeof providedValue !== 'string' || + providedValue.length > MAXIMUM_HEADER_VALUE_LENGTH || + !SAFE_HEADER_VALUE_PATTERN.test(providedValue) || + Object.hasOwn(headers, name) + ) { + return failProviderRequest(); + } + headers[name] = providedValue; + } + return headers; +} + +function requireHeaders( + headersValue: object, + policy: EndpointPolicy, +): Record { + const headers = normalizeHeaders(headersValue); + const expectedNames = Object.keys(policy.requiredHeaders); + if ( + Object.keys(headers).length !== expectedNames.length || + !expectedNames.every((name) => Object.hasOwn(headers, name)) + ) { + return failProviderRequest(); + } + + for (const [name, expected] of Object.entries(policy.requiredHeaders)) { + const value = headers[name]; + if ( + value === undefined || + (typeof expected === 'string' + ? value !== expected + : !expected.test(value)) + ) { + return failProviderRequest(); + } + } + return headers; +} + +function requireBody( + body: string | undefined, + policy: EndpointPolicy, +): string | undefined { + if (policy.body === 'forbidden') { + if (body !== undefined) { + return failProviderRequest(); + } + return undefined; + } + + if ( + typeof body !== 'string' || + body.length === 0 || + Buffer.byteLength(body, 'utf8') > MAXIMUM_REQUEST_BODY_BYTES + ) { + return failProviderRequest(); + } + return body; +} + +function requireJsonContentType(response: Response): string { + const contentType = response.headers.get('content-type'); + if ( + !contentType || + contentType.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json' + ) { + return failProviderRequest(); + } + return contentType; +} + +function requireBoundedContentLength(response: Response): void { + const contentLength = response.headers.get('content-length'); + if (contentLength === null) { + return; + } + if (!/^\d+$/.test(contentLength)) { + return failProviderRequest(); + } + const parsedLength = Number(contentLength); + if ( + !Number.isSafeInteger(parsedLength) || + parsedLength > MAXIMUM_RESPONSE_BODY_BYTES + ) { + return failProviderRequest(); + } +} + +async function readBoundedBody(response: Response): Promise { + requireBoundedContentLength(response); + if (!response.body) { + return ''; + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder('utf-8', { fatal: true }); + let bytesRead = 0; + let body = ''; + try { + while (true) { + const chunk = await reader.read(); + if (chunk.done) { + break; + } + bytesRead += chunk.value.byteLength; + if (bytesRead > MAXIMUM_RESPONSE_BODY_BYTES) { + return failProviderRequest(); + } + body += decoder.decode(chunk.value, { stream: true }); + } + body += decoder.decode(); + return body; + } catch { + return failProviderRequest(); + } finally { + reader.releaseLock(); + } +} + +/** + * Executes only allowlisted OAuth provider requests with no redirects, bounded + * latency, bounded UTF-8 JSON responses, and credential-safe generic failures. + */ +export class BoundedOAuthProviderHttpClient { + private readonly fetchFunction: OAuthProviderFetch; + private readonly timeoutMs: number; + + constructor(options: OAuthProviderHttpClientOptions = {}) { + this.fetchFunction = options.fetchFunction ?? globalThis.fetch; + this.timeoutMs = requireTimeout(options.timeoutMs); + } + + /** + * Executes one exact provider request and returns its bounded JSON response. + */ + async execute( + request: OAuthProviderHttpRequest, + ): Promise { + const policy = requirePolicy(request); + const headers = requireHeaders(request.headers, policy); + const body = requireBody(request.body, policy); + const abortController = new AbortController(); + const timeout = setTimeout(() => abortController.abort(), this.timeoutMs); + + try { + const response = await this.fetchFunction(policy.url, { + method: policy.method, + headers, + redirect: 'error', + credentials: 'omit', + cache: 'no-store', + referrerPolicy: 'no-referrer', + signal: abortController.signal, + ...(body === undefined ? {} : { body }), + }); + if ( + !response || + !Number.isInteger(response.status) || + response.status < 100 || + response.status > 599 || + (response.status >= 300 && response.status < 400) + ) { + return failProviderRequest(); + } + + const contentType = requireJsonContentType(response); + const responseBody = await readBoundedBody(response); + return { + status: response.status, + contentType, + body: responseBody, + }; + } catch { + abortController.abort(); + return failProviderRequest(); + } finally { + clearTimeout(timeout); + } + } +} From 526e6523c8f8597d242b0617ff11fae8127b7196 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:02:37 +0900 Subject: [PATCH 02/13] test(identity): cover provider HTTP boundary --- .../tests/oauth-provider-http-client.test.ts | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 apps/identity-service/src/tests/oauth-provider-http-client.test.ts diff --git a/apps/identity-service/src/tests/oauth-provider-http-client.test.ts b/apps/identity-service/src/tests/oauth-provider-http-client.test.ts new file mode 100644 index 000000000..4021af8a6 --- /dev/null +++ b/apps/identity-service/src/tests/oauth-provider-http-client.test.ts @@ -0,0 +1,257 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { ConsumedOAuthTransaction } from '../auth-security'; +import { buildGitHubIdentityRequests } from '../oauth-provider-response'; +import { + BoundedOAuthProviderHttpClient, + type OAuthProviderFetch, + type OAuthProviderHttpRequest, +} from '../oauth-provider-http-client'; +import { buildTokenExchangeRequest } from '../oauth-token-exchange'; + +const GOOGLE_REDIRECT_URI = + 'https://identity.example.test/v1/auth/google/callback'; + +/** + * Builds deterministic, non-production opaque values without embedding credentials. + */ +function syntheticOpaqueValue(variant: string): string { + const body = Array.from({ length: 48 }, (_, index) => + String.fromCharCode(97 + (index % 26)), + ).join(''); + return `${body}-${variant}`; +} + +function googleTransaction(): ConsumedOAuthTransaction { + return { + id: '24b4d4d5-c5c0-4df4-a863-5239d263e36e', + provider: 'google', + codeVerifier: syntheticOpaqueValue('pkce-verifier'), + redirectUri: GOOGLE_REDIRECT_URI, + nonce: syntheticOpaqueValue('oidc-nonce'), + }; +} + +function googleTokenRequest(): OAuthProviderHttpRequest { + return buildTokenExchangeRequest( + 'google', + { + clientId: 'google-client-id', + clientSecret: syntheticOpaqueValue('provider-client-credential'), + redirectUri: GOOGLE_REDIRECT_URI, + }, + syntheticOpaqueValue('authorization-code'), + googleTransaction(), + ); +} + +function jsonResponse( + body: unknown, + init: { status?: number; headers?: Record } = {}, +): Response { + return new Response(JSON.stringify(body), { + status: init.status ?? 200, + headers: { + 'content-type': 'application/json; charset=utf-8', + ...init.headers, + }, + }); +} + +afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); +}); + +describe('BoundedOAuthProviderHttpClient', () => { + it('executes an exact token request without redirects or ambient credentials', async () => { + const fetchFunction: OAuthProviderFetch = vi.fn(async (_input, init) => { + expect(init.method).toBe('POST'); + expect(init.redirect).toBe('error'); + expect(init.credentials).toBe('omit'); + expect(init.cache).toBe('no-store'); + expect(init.referrerPolicy).toBe('no-referrer'); + expect(init.signal).toBeInstanceOf(AbortSignal); + return jsonResponse({ token_type: 'bearer' }); + }); + const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); + + const result = await client.execute(googleTokenRequest()); + + expect(fetchFunction).toHaveBeenCalledOnce(); + expect(fetchFunction).toHaveBeenCalledWith( + 'https://oauth2.googleapis.com/token', + expect.objectContaining({ method: 'POST', redirect: 'error' }), + ); + expect(result).toEqual({ + status: 200, + contentType: 'application/json; charset=utf-8', + body: JSON.stringify({ token_type: 'bearer' }), + }); + }); + + it('accepts the fixed GitHub identity request builders', async () => { + const providerCredential = syntheticOpaqueValue('github-access'); + const requests = buildGitHubIdentityRequests(providerCredential); + const fetchFunction: OAuthProviderFetch = vi.fn(async (input) => + input.endsWith('/emails') ? jsonResponse([]) : jsonResponse({ id: 42 }), + ); + const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); + + await expect(client.execute(requests.user)).resolves.toMatchObject({ + status: 200, + }); + await expect(client.execute(requests.emails)).resolves.toMatchObject({ + status: 200, + }); + expect(fetchFunction).toHaveBeenNthCalledWith( + 1, + 'https://api.github.com/user', + expect.objectContaining({ method: 'GET' }), + ); + expect(fetchFunction).toHaveBeenNthCalledWith( + 2, + 'https://api.github.com/user/emails', + expect.objectContaining({ method: 'GET' }), + ); + }); + + it.each([ + { + name: 'query-bearing endpoint', + request: { + url: 'https://api.github.com/user?target=https://example.test', + method: 'GET' as const, + headers: {}, + }, + }, + { + name: 'unapproved endpoint', + request: { + url: 'https://api.github.com/repos/example/private', + method: 'GET' as const, + headers: {}, + }, + }, + { + name: 'ambient cookie header', + request: { + ...buildGitHubIdentityRequests( + syntheticOpaqueValue('github-extra-header'), + ).user, + headers: { + ...buildGitHubIdentityRequests( + syntheticOpaqueValue('github-extra-header'), + ).user.headers, + cookie: 'life_os_session=not-forwarded', + }, + }, + }, + { + name: 'body on a GET request', + request: { + ...buildGitHubIdentityRequests( + syntheticOpaqueValue('github-get-body'), + ).user, + body: 'unexpected=true', + }, + }, + ])('rejects a $name before network access', async ({ request }) => { + const fetchFunction: OAuthProviderFetch = vi.fn(async () => jsonResponse({})); + const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); + + await expect(client.execute(request)).rejects.toThrow( + 'OAuth provider request failed', + ); + expect(fetchFunction).not.toHaveBeenCalled(); + }); + + it('rejects redirects, non-JSON responses, and oversized response streams generically', async () => { + const responses = [ + new Response('', { + status: 302, + headers: { + location: 'https://attacker.example.test', + 'content-type': 'application/json', + }, + }), + new Response('upstream failure', { + status: 502, + headers: { 'content-type': 'text/html' }, + }), + jsonResponse({ value: 'x'.repeat(64 * 1024) }), + ]; + const fetchFunction: OAuthProviderFetch = vi.fn(async () => { + const response = responses.shift(); + if (!response) { + throw new Error('unexpected test request'); + } + return response; + }); + const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); + + await expect(client.execute(googleTokenRequest())).rejects.toThrow( + 'OAuth provider request failed', + ); + await expect(client.execute(googleTokenRequest())).rejects.toThrow( + 'OAuth provider request failed', + ); + await expect(client.execute(googleTokenRequest())).rejects.toThrow( + 'OAuth provider request failed', + ); + }); + + it('aborts an upstream request at the configured deadline', async () => { + vi.useFakeTimers(); + const fetchFunction: OAuthProviderFetch = vi.fn( + async (_input, init) => + await new Promise((_resolve, reject) => { + const signal = init.signal; + if (!signal) { + reject(new Error('missing abort signal')); + return; + } + signal.addEventListener( + 'abort', + () => reject(new DOMException('aborted', 'AbortError')), + { once: true }, + ); + }), + ); + const client = new BoundedOAuthProviderHttpClient({ + fetchFunction, + timeoutMs: 100, + }); + + const pendingRequest = client.execute(googleTokenRequest()); + const assertion = expect(pendingRequest).rejects.toThrow( + 'OAuth provider request failed', + ); + await vi.advanceTimersByTimeAsync(100); + await assertion; + + const init = vi.mocked(fetchFunction).mock.calls[0]?.[1]; + expect(init?.signal?.aborted).toBe(true); + }); + + it('never includes provider request material in failures', async () => { + const providerCredential = syntheticOpaqueValue('failure-redaction'); + const request = buildGitHubIdentityRequests(providerCredential).user; + const fetchFunction: OAuthProviderFetch = vi.fn(async () => { + throw new Error(`upstream exposed ${providerCredential}`); + }); + const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); + + await expect(client.execute(request)).rejects.toEqual( + new Error('OAuth provider request failed'), + ); + }); + + it('rejects unsafe timeout configuration', () => { + expect( + () => new BoundedOAuthProviderHttpClient({ timeoutMs: 99 }), + ).toThrow('OAuth provider request failed'); + expect( + () => new BoundedOAuthProviderHttpClient({ timeoutMs: 10_001 }), + ).toThrow('OAuth provider request failed'); + }); +}); From 31c4180902686970284573648f416610ef79597b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:02:48 +0900 Subject: [PATCH 03/13] docs: plan bounded OAuth provider transport --- ...-08-03-oauth-provider-http-client-slice.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-03-oauth-provider-http-client-slice.md diff --git a/docs/superpowers/plans/2026-08-03-oauth-provider-http-client-slice.md b/docs/superpowers/plans/2026-08-03-oauth-provider-http-client-slice.md new file mode 100644 index 000000000..8b588029d --- /dev/null +++ b/docs/superpowers/plans/2026-08-03-oauth-provider-http-client-slice.md @@ -0,0 +1,24 @@ +# OAuth Provider HTTP Client Slice + +## Goal + +Add the fail-closed outbound HTTP boundary required by issue #18 before provider callback orchestration is allowed to contact Google or GitHub. + +## Included + +- exact HTTPS endpoint and method allowlists for Google token exchange, Google JWKS retrieval, GitHub token exchange, and GitHub user and verified-email retrieval +- endpoint-specific request-header allowlists that prevent ambient cookies, host overrides, and unrelated credentials from being forwarded +- request-body size limits and rejection of bodies on fixed GET endpoints +- redirect refusal, bounded request deadlines, abort propagation, bounded streamed response bodies, strict UTF-8 decoding, and JSON content-type enforcement +- generic credential-free failures that never include authorization codes, client credentials, provider tokens, response bodies, or target URLs +- compatibility coverage for the existing token-exchange and GitHub identity request builders + +## Verification + +- unit tests cover exact endpoint execution, ambient credential rejection, query-bearing and unapproved destinations, unexpected GET bodies, redirects, non-JSON responses, oversized streams, timeout aborts, and failure redaction +- TypeScript compilation and the full repository test suite remain required +- CI, SAST Semgrep, Security Scan, AppGuardrail, Commercial Readiness, and review feedback must pass before merge + +## Follow-up + +The next slice should add Google JWKS retrieval and signature verification, then compose token exchange, provider identity retrieval, atomic account provisioning, session issuance, audit events, and fixed post-login redirects into the two callback controllers. From dd1d8410e73282d44977643e79588639aa6a9b59 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:03:03 +0900 Subject: [PATCH 04/13] chore: include OAuth HTTP client in format gate --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1022d3f8b..6963f3fa5 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lint": "turbo run lint", "test": "turbo run test", "typecheck": "turbo run typecheck", - "format:check": "prettier --single-quote --check README.md package.json turbo.json tsconfig.base.json pnpm-workspace.yaml compose.yaml .appguardrail.json .github/workflows/ci.yml .github/workflows/appguardrail.yml security/appguardrail-contract.json packages/appguardrail-contract/package.json packages/appguardrail-contract/src/verify-contract.mjs packages/appguardrail-contract/src/verify-contract.test.mjs tests/appguardrail-fixtures/dangerous-cors.ts docs/security/appguardrail-regressions.md docs/superpowers/specs/2026-08-03-appguardrail-security-gate-design.md docs/superpowers/plans/2026-08-03-appguardrail-security-gate.md .github/workflows/commercial-readiness.yml product/commercial-readiness-policy.json product/capabilities.json packages/commercial-readiness/package.json packages/commercial-readiness/src/schema.mjs packages/commercial-readiness/src/schema.test.mjs packages/commercial-readiness/src/audit.mjs packages/commercial-readiness/src/audit.test.mjs packages/commercial-readiness/src/pr-gate.mjs packages/commercial-readiness/src/pr-gate.test.mjs packages/commercial-readiness/src/render.mjs packages/commercial-readiness/src/render.test.mjs packages/commercial-readiness/src/github-client.mjs packages/commercial-readiness/src/github-client.test.mjs packages/commercial-readiness/src/cli.mjs packages/commercial-readiness/src/cli.test.mjs packages/commercial-readiness/src/workflow-contract.test.mjs docs/superpowers/specs/2026-08-03-commercial-readiness-loop-design.md docs/superpowers/plans/2026-08-03-commercial-readiness-loop.md apps/identity-service/src/oauth-http-boundary.ts apps/identity-service/src/oauth-http-application.ts apps/identity-service/src/oauth-http-boundary.test.ts docs/superpowers/plans/2026-08-03-oauth-http-boundary-slice.md apps/identity-service/package.json apps/identity-service/src/main.ts apps/identity-service/src/oauth-http-controller.ts apps/identity-service/src/oauth-http-controller.test.ts apps/identity-service/src/identity-runtime.ts apps/identity-service/src/identity-runtime.test.ts docs/superpowers/plans/2026-08-03-oauth-controller-wiring-slice.md", + "format:check": "prettier --single-quote --check README.md package.json turbo.json tsconfig.base.json pnpm-workspace.yaml compose.yaml .appguardrail.json .github/workflows/ci.yml .github/workflows/appguardrail.yml security/appguardrail-contract.json packages/appguardrail-contract/package.json packages/appguardrail-contract/src/verify-contract.mjs packages/appguardrail-contract/src/verify-contract.test.mjs tests/appguardrail-fixtures/dangerous-cors.ts docs/security/appguardrail-regressions.md docs/superpowers/specs/2026-08-03-appguardrail-security-gate-design.md docs/superpowers/plans/2026-08-03-appguardrail-security-gate.md .github/workflows/commercial-readiness.yml product/commercial-readiness-policy.json product/capabilities.json packages/commercial-readiness/package.json packages/commercial-readiness/src/schema.mjs packages/commercial-readiness/src/schema.test.mjs packages/commercial-readiness/src/audit.mjs packages/commercial-readiness/src/audit.test.mjs packages/commercial-readiness/src/pr-gate.mjs packages/commercial-readiness/src/pr-gate.test.mjs packages/commercial-readiness/src/render.mjs packages/commercial-readiness/src/render.test.mjs packages/commercial-readiness/src/github-client.mjs packages/commercial-readiness/src/github-client.test.mjs packages/commercial-readiness/src/cli.mjs packages/commercial-readiness/src/cli.test.mjs packages/commercial-readiness/src/workflow-contract.test.mjs docs/superpowers/specs/2026-08-03-commercial-readiness-loop-design.md docs/superpowers/plans/2026-08-03-commercial-readiness-loop.md apps/identity-service/src/oauth-http-boundary.ts apps/identity-service/src/oauth-http-application.ts apps/identity-service/src/oauth-http-boundary.test.ts docs/superpowers/plans/2026-08-03-oauth-http-boundary-slice.md apps/identity-service/package.json apps/identity-service/src/main.ts apps/identity-service/src/oauth-http-controller.ts apps/identity-service/src/oauth-http-controller.test.ts apps/identity-service/src/identity-runtime.ts apps/identity-service/src/identity-runtime.test.ts docs/superpowers/plans/2026-08-03-oauth-controller-wiring-slice.md apps/identity-service/src/oauth-provider-http-client.ts apps/identity-service/src/tests/oauth-provider-http-client.test.ts docs/superpowers/plans/2026-08-03-oauth-provider-http-client-slice.md", "format": "prettier --single-quote --write ." }, "devDependencies": { From 7224694c2996bba06fe4f715d072652ce73cc690 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:09:37 +0900 Subject: [PATCH 05/13] ci: generate OAuth provider format preview --- .../oauth-provider-format-preview.yml | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/oauth-provider-format-preview.yml diff --git a/.github/workflows/oauth-provider-format-preview.yml b/.github/workflows/oauth-provider-format-preview.yml new file mode 100644 index 000000000..073ee6732 --- /dev/null +++ b/.github/workflows/oauth-provider-format-preview.yml @@ -0,0 +1,62 @@ +name: OAuth Provider Format Preview + +on: + pull_request: + branches: [main] + paths: + - 'apps/identity-service/src/oauth-provider-http-client.ts' + - 'apps/identity-service/src/tests/oauth-provider-http-client.test.ts' + - '.github/workflows/oauth-provider-format-preview.yml' + +permissions: + contents: read + +concurrency: + group: oauth-provider-format-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + format-preview: + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Format provider HTTP files + run: | + pnpm exec prettier --single-quote --write \ + apps/identity-service/src/oauth-provider-http-client.ts \ + apps/identity-service/src/tests/oauth-provider-http-client.test.ts + + - name: Stage formatted files + run: | + mkdir -p "${RUNNER_TEMP}/oauth-provider-format-preview/apps/identity-service/src/tests" + cp apps/identity-service/src/oauth-provider-http-client.ts \ + "${RUNNER_TEMP}/oauth-provider-format-preview/apps/identity-service/src/" + cp apps/identity-service/src/tests/oauth-provider-http-client.test.ts \ + "${RUNNER_TEMP}/oauth-provider-format-preview/apps/identity-service/src/tests/" + + - name: Upload formatted files + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: oauth-provider-format-preview + path: ${{ runner.temp }}/oauth-provider-format-preview/ + if-no-files-found: error + retention-days: 1 From fd421935df9a35986f457423e018f5c94a9c19ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:11:09 +0900 Subject: [PATCH 06/13] style(identity): format provider HTTP client --- .../identity-service/src/oauth-provider-http-client.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/identity-service/src/oauth-provider-http-client.ts b/apps/identity-service/src/oauth-provider-http-client.ts index 915e3f975..5adedf3fa 100644 --- a/apps/identity-service/src/oauth-provider-http-client.ts +++ b/apps/identity-service/src/oauth-provider-http-client.ts @@ -115,9 +115,7 @@ function requireTimeout(value: number | undefined): number { return timeoutMs; } -function requirePolicy( - request: OAuthProviderHttpRequest, -): EndpointPolicy { +function requirePolicy(request: OAuthProviderHttpRequest): EndpointPolicy { if ( !request || typeof request !== 'object' || @@ -151,7 +149,11 @@ function requirePolicy( } function normalizeHeaders(headersValue: object): Record { - if (!headersValue || typeof headersValue !== 'object' || Array.isArray(headersValue)) { + if ( + !headersValue || + typeof headersValue !== 'object' || + Array.isArray(headersValue) + ) { return failProviderRequest(); } From e6efe440839f00cbdf4c62bc0c6de0ca99909ad1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:11:44 +0900 Subject: [PATCH 07/13] style(identity): format provider HTTP tests --- .../src/tests/oauth-provider-http-client.test.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/identity-service/src/tests/oauth-provider-http-client.test.ts b/apps/identity-service/src/tests/oauth-provider-http-client.test.ts index 4021af8a6..32d980fe3 100644 --- a/apps/identity-service/src/tests/oauth-provider-http-client.test.ts +++ b/apps/identity-service/src/tests/oauth-provider-http-client.test.ts @@ -149,14 +149,15 @@ describe('BoundedOAuthProviderHttpClient', () => { { name: 'body on a GET request', request: { - ...buildGitHubIdentityRequests( - syntheticOpaqueValue('github-get-body'), - ).user, + ...buildGitHubIdentityRequests(syntheticOpaqueValue('github-get-body')) + .user, body: 'unexpected=true', }, }, ])('rejects a $name before network access', async ({ request }) => { - const fetchFunction: OAuthProviderFetch = vi.fn(async () => jsonResponse({})); + const fetchFunction: OAuthProviderFetch = vi.fn(async () => + jsonResponse({}), + ); const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); await expect(client.execute(request)).rejects.toThrow( @@ -247,9 +248,9 @@ describe('BoundedOAuthProviderHttpClient', () => { }); it('rejects unsafe timeout configuration', () => { - expect( - () => new BoundedOAuthProviderHttpClient({ timeoutMs: 99 }), - ).toThrow('OAuth provider request failed'); + expect(() => new BoundedOAuthProviderHttpClient({ timeoutMs: 99 })).toThrow( + 'OAuth provider request failed', + ); expect( () => new BoundedOAuthProviderHttpClient({ timeoutMs: 10_001 }), ).toThrow('OAuth provider request failed'); From 88deef9b5a26a1cfca76bb61c06fd9084c4b9494 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:11:54 +0900 Subject: [PATCH 08/13] ci: remove one-shot format preview --- .../oauth-provider-format-preview.yml | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 .github/workflows/oauth-provider-format-preview.yml diff --git a/.github/workflows/oauth-provider-format-preview.yml b/.github/workflows/oauth-provider-format-preview.yml deleted file mode 100644 index 073ee6732..000000000 --- a/.github/workflows/oauth-provider-format-preview.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: OAuth Provider Format Preview - -on: - pull_request: - branches: [main] - paths: - - 'apps/identity-service/src/oauth-provider-http-client.ts' - - 'apps/identity-service/src/tests/oauth-provider-http-client.test.ts' - - '.github/workflows/oauth-provider-format-preview.yml' - -permissions: - contents: read - -concurrency: - group: oauth-provider-format-preview-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - format-preview: - if: github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - timeout-minutes: 10 - env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '22' - - - name: Enable Corepack - run: corepack enable - - - name: Install dependencies - run: pnpm install --no-frozen-lockfile - - - name: Format provider HTTP files - run: | - pnpm exec prettier --single-quote --write \ - apps/identity-service/src/oauth-provider-http-client.ts \ - apps/identity-service/src/tests/oauth-provider-http-client.test.ts - - - name: Stage formatted files - run: | - mkdir -p "${RUNNER_TEMP}/oauth-provider-format-preview/apps/identity-service/src/tests" - cp apps/identity-service/src/oauth-provider-http-client.ts \ - "${RUNNER_TEMP}/oauth-provider-format-preview/apps/identity-service/src/" - cp apps/identity-service/src/tests/oauth-provider-http-client.test.ts \ - "${RUNNER_TEMP}/oauth-provider-format-preview/apps/identity-service/src/tests/" - - - name: Upload formatted files - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: oauth-provider-format-preview - path: ${{ runner.temp }}/oauth-provider-format-preview/ - if-no-files-found: error - retention-days: 1 From d93137f230edacee01f446d1d8369b339c1b5be0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:13:58 +0900 Subject: [PATCH 09/13] fix(identity): harden provider transport cleanup --- .../src/oauth-provider-http-client.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/identity-service/src/oauth-provider-http-client.ts b/apps/identity-service/src/oauth-provider-http-client.ts index 5adedf3fa..1ed61edce 100644 --- a/apps/identity-service/src/oauth-provider-http-client.ts +++ b/apps/identity-service/src/oauth-provider-http-client.ts @@ -275,6 +275,11 @@ async function readBoundedBody(response: Response): Promise { body += decoder.decode(); return body; } catch { + try { + await reader.cancel(); + } catch { + // The standardized provider failure remains authoritative. + } return failProviderRequest(); } finally { reader.releaseLock(); @@ -290,7 +295,12 @@ export class BoundedOAuthProviderHttpClient { private readonly timeoutMs: number; constructor(options: OAuthProviderHttpClientOptions = {}) { - this.fetchFunction = options.fetchFunction ?? globalThis.fetch; + const defaultFetch = + typeof globalThis.fetch === 'function' + ? (globalThis.fetch.bind(globalThis) as OAuthProviderFetch) + : undefined; + this.fetchFunction = + options.fetchFunction ?? defaultFetch ?? failProviderRequest(); this.timeoutMs = requireTimeout(options.timeoutMs); } From 3151c913ee47360c62afd52d133c5acc292916cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:14:41 +0900 Subject: [PATCH 10/13] test(identity): cover provider transport review findings --- .../tests/oauth-provider-http-client.test.ts | 117 +++++++++++++----- 1 file changed, 85 insertions(+), 32 deletions(-) diff --git a/apps/identity-service/src/tests/oauth-provider-http-client.test.ts b/apps/identity-service/src/tests/oauth-provider-http-client.test.ts index 32d980fe3..3472b2c01 100644 --- a/apps/identity-service/src/tests/oauth-provider-http-client.test.ts +++ b/apps/identity-service/src/tests/oauth-provider-http-client.test.ts @@ -64,19 +64,23 @@ afterEach(() => { describe('BoundedOAuthProviderHttpClient', () => { it('executes an exact token request without redirects or ambient credentials', async () => { + let capturedInit: RequestInit | undefined; const fetchFunction: OAuthProviderFetch = vi.fn(async (_input, init) => { - expect(init.method).toBe('POST'); - expect(init.redirect).toBe('error'); - expect(init.credentials).toBe('omit'); - expect(init.cache).toBe('no-store'); - expect(init.referrerPolicy).toBe('no-referrer'); - expect(init.signal).toBeInstanceOf(AbortSignal); + capturedInit = init; return jsonResponse({ token_type: 'bearer' }); }); const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); const result = await client.execute(googleTokenRequest()); + expect(capturedInit).toMatchObject({ + method: 'POST', + redirect: 'error', + credentials: 'omit', + cache: 'no-store', + referrerPolicy: 'no-referrer', + }); + expect(capturedInit?.signal).toBeInstanceOf(AbortSignal); expect(fetchFunction).toHaveBeenCalledOnce(); expect(fetchFunction).toHaveBeenCalledWith( 'https://oauth2.googleapis.com/token', @@ -119,17 +123,19 @@ describe('BoundedOAuthProviderHttpClient', () => { { name: 'query-bearing endpoint', request: { + ...buildGitHubIdentityRequests( + syntheticOpaqueValue('github-query-endpoint'), + ).user, url: 'https://api.github.com/user?target=https://example.test', - method: 'GET' as const, - headers: {}, }, }, { name: 'unapproved endpoint', request: { + ...buildGitHubIdentityRequests( + syntheticOpaqueValue('github-unapproved-endpoint'), + ).user, url: 'https://api.github.com/repos/example/private', - method: 'GET' as const, - headers: {}, }, }, { @@ -166,8 +172,8 @@ describe('BoundedOAuthProviderHttpClient', () => { expect(fetchFunction).not.toHaveBeenCalled(); }); - it('rejects redirects, non-JSON responses, and oversized response streams generically', async () => { - const responses = [ + it('rejects redirect status responses generically', async () => { + const fetchFunction: OAuthProviderFetch = vi.fn(async () => new Response('', { status: 302, headers: { @@ -175,30 +181,48 @@ describe('BoundedOAuthProviderHttpClient', () => { 'content-type': 'application/json', }, }), + ); + const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); + + await expect(client.execute(googleTokenRequest())).rejects.toThrow( + 'OAuth provider request failed', + ); + }); + + it('rejects non-JSON provider responses generically', async () => { + const fetchFunction: OAuthProviderFetch = vi.fn(async () => new Response('upstream failure', { status: 502, headers: { 'content-type': 'text/html' }, }), - jsonResponse({ value: 'x'.repeat(64 * 1024) }), - ]; - const fetchFunction: OAuthProviderFetch = vi.fn(async () => { - const response = responses.shift(); - if (!response) { - throw new Error('unexpected test request'); - } - return response; - }); + ); const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); await expect(client.execute(googleTokenRequest())).rejects.toThrow( 'OAuth provider request failed', ); - await expect(client.execute(googleTokenRequest())).rejects.toThrow( - 'OAuth provider request failed', + }); + + it('rejects and cancels oversized provider response streams generically', async () => { + const cancel = vi.fn(); + const responseBody = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(64 * 1024 + 1)); + }, + cancel, + }); + const fetchFunction: OAuthProviderFetch = vi.fn(async () => + new Response(responseBody, { + status: 200, + headers: { 'content-type': 'application/json' }, + }), ); + const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); + await expect(client.execute(googleTokenRequest())).rejects.toThrow( 'OAuth provider request failed', ); + expect(cancel).toHaveBeenCalledOnce(); }); it('aborts an upstream request at the configured deadline', async () => { @@ -242,17 +266,46 @@ describe('BoundedOAuthProviderHttpClient', () => { }); const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); - await expect(client.execute(request)).rejects.toEqual( - new Error('OAuth provider request failed'), - ); + const error = await client + .execute(request) + .catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(Error); + const providerError = error as Error; + expect(providerError.message).toBe('OAuth provider request failed'); + expect(providerError.cause).toBeUndefined(); + expect( + JSON.stringify({ + name: providerError.name, + message: providerError.message, + cause: providerError.cause, + stack: providerError.stack, + }), + ).not.toContain(providerCredential); }); - it('rejects unsafe timeout configuration', () => { - expect(() => new BoundedOAuthProviderHttpClient({ timeoutMs: 99 })).toThrow( - 'OAuth provider request failed', - ); + it('accepts timeout boundaries and rejects unsafe timeout configuration', () => { + const fetchFunction: OAuthProviderFetch = vi.fn(async () => jsonResponse({})); + + expect( + () => + new BoundedOAuthProviderHttpClient({ + fetchFunction, + timeoutMs: 100, + }), + ).not.toThrow(); expect( - () => new BoundedOAuthProviderHttpClient({ timeoutMs: 10_001 }), - ).toThrow('OAuth provider request failed'); + () => + new BoundedOAuthProviderHttpClient({ + fetchFunction, + timeoutMs: 10_000, + }), + ).not.toThrow(); + for (const timeoutMs of [99, 100.5, 10_001]) { + expect( + () => + new BoundedOAuthProviderHttpClient({ fetchFunction, timeoutMs }), + ).toThrow('OAuth provider request failed'); + } }); }); From 9f9cb7cb6eb377a60a30b77b54379531181df999 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:14:56 +0900 Subject: [PATCH 11/13] ci: regenerate OAuth provider format preview --- .../oauth-provider-format-preview.yml | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/oauth-provider-format-preview.yml diff --git a/.github/workflows/oauth-provider-format-preview.yml b/.github/workflows/oauth-provider-format-preview.yml new file mode 100644 index 000000000..073ee6732 --- /dev/null +++ b/.github/workflows/oauth-provider-format-preview.yml @@ -0,0 +1,62 @@ +name: OAuth Provider Format Preview + +on: + pull_request: + branches: [main] + paths: + - 'apps/identity-service/src/oauth-provider-http-client.ts' + - 'apps/identity-service/src/tests/oauth-provider-http-client.test.ts' + - '.github/workflows/oauth-provider-format-preview.yml' + +permissions: + contents: read + +concurrency: + group: oauth-provider-format-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + format-preview: + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Format provider HTTP files + run: | + pnpm exec prettier --single-quote --write \ + apps/identity-service/src/oauth-provider-http-client.ts \ + apps/identity-service/src/tests/oauth-provider-http-client.test.ts + + - name: Stage formatted files + run: | + mkdir -p "${RUNNER_TEMP}/oauth-provider-format-preview/apps/identity-service/src/tests" + cp apps/identity-service/src/oauth-provider-http-client.ts \ + "${RUNNER_TEMP}/oauth-provider-format-preview/apps/identity-service/src/" + cp apps/identity-service/src/tests/oauth-provider-http-client.test.ts \ + "${RUNNER_TEMP}/oauth-provider-format-preview/apps/identity-service/src/tests/" + + - name: Upload formatted files + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: oauth-provider-format-preview + path: ${{ runner.temp }}/oauth-provider-format-preview/ + if-no-files-found: error + retention-days: 1 From 94e5e49f5c0c43ede3fbf290fec425c449cf0ce3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:16:16 +0900 Subject: [PATCH 12/13] style(identity): format review fixes --- .../tests/oauth-provider-http-client.test.ts | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/apps/identity-service/src/tests/oauth-provider-http-client.test.ts b/apps/identity-service/src/tests/oauth-provider-http-client.test.ts index 3472b2c01..2bf82fd3b 100644 --- a/apps/identity-service/src/tests/oauth-provider-http-client.test.ts +++ b/apps/identity-service/src/tests/oauth-provider-http-client.test.ts @@ -173,14 +173,15 @@ describe('BoundedOAuthProviderHttpClient', () => { }); it('rejects redirect status responses generically', async () => { - const fetchFunction: OAuthProviderFetch = vi.fn(async () => - new Response('', { - status: 302, - headers: { - location: 'https://attacker.example.test', - 'content-type': 'application/json', - }, - }), + const fetchFunction: OAuthProviderFetch = vi.fn( + async () => + new Response('', { + status: 302, + headers: { + location: 'https://attacker.example.test', + 'content-type': 'application/json', + }, + }), ); const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); @@ -190,11 +191,12 @@ describe('BoundedOAuthProviderHttpClient', () => { }); it('rejects non-JSON provider responses generically', async () => { - const fetchFunction: OAuthProviderFetch = vi.fn(async () => - new Response('upstream failure', { - status: 502, - headers: { 'content-type': 'text/html' }, - }), + const fetchFunction: OAuthProviderFetch = vi.fn( + async () => + new Response('upstream failure', { + status: 502, + headers: { 'content-type': 'text/html' }, + }), ); const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); @@ -211,11 +213,12 @@ describe('BoundedOAuthProviderHttpClient', () => { }, cancel, }); - const fetchFunction: OAuthProviderFetch = vi.fn(async () => - new Response(responseBody, { - status: 200, - headers: { 'content-type': 'application/json' }, - }), + const fetchFunction: OAuthProviderFetch = vi.fn( + async () => + new Response(responseBody, { + status: 200, + headers: { 'content-type': 'application/json' }, + }), ); const client = new BoundedOAuthProviderHttpClient({ fetchFunction }); @@ -285,7 +288,9 @@ describe('BoundedOAuthProviderHttpClient', () => { }); it('accepts timeout boundaries and rejects unsafe timeout configuration', () => { - const fetchFunction: OAuthProviderFetch = vi.fn(async () => jsonResponse({})); + const fetchFunction: OAuthProviderFetch = vi.fn(async () => + jsonResponse({}), + ); expect( () => @@ -303,8 +308,7 @@ describe('BoundedOAuthProviderHttpClient', () => { ).not.toThrow(); for (const timeoutMs of [99, 100.5, 10_001]) { expect( - () => - new BoundedOAuthProviderHttpClient({ fetchFunction, timeoutMs }), + () => new BoundedOAuthProviderHttpClient({ fetchFunction, timeoutMs }), ).toThrow('OAuth provider request failed'); } }); From 3c0022133ec33bae7532c5626d5f105bc57a3a00 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 20:16:25 +0900 Subject: [PATCH 13/13] ci: remove completed format preview --- .../oauth-provider-format-preview.yml | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 .github/workflows/oauth-provider-format-preview.yml diff --git a/.github/workflows/oauth-provider-format-preview.yml b/.github/workflows/oauth-provider-format-preview.yml deleted file mode 100644 index 073ee6732..000000000 --- a/.github/workflows/oauth-provider-format-preview.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: OAuth Provider Format Preview - -on: - pull_request: - branches: [main] - paths: - - 'apps/identity-service/src/oauth-provider-http-client.ts' - - 'apps/identity-service/src/tests/oauth-provider-http-client.test.ts' - - '.github/workflows/oauth-provider-format-preview.yml' - -permissions: - contents: read - -concurrency: - group: oauth-provider-format-preview-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - format-preview: - if: github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - timeout-minutes: 10 - env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '22' - - - name: Enable Corepack - run: corepack enable - - - name: Install dependencies - run: pnpm install --no-frozen-lockfile - - - name: Format provider HTTP files - run: | - pnpm exec prettier --single-quote --write \ - apps/identity-service/src/oauth-provider-http-client.ts \ - apps/identity-service/src/tests/oauth-provider-http-client.test.ts - - - name: Stage formatted files - run: | - mkdir -p "${RUNNER_TEMP}/oauth-provider-format-preview/apps/identity-service/src/tests" - cp apps/identity-service/src/oauth-provider-http-client.ts \ - "${RUNNER_TEMP}/oauth-provider-format-preview/apps/identity-service/src/" - cp apps/identity-service/src/tests/oauth-provider-http-client.test.ts \ - "${RUNNER_TEMP}/oauth-provider-format-preview/apps/identity-service/src/tests/" - - - name: Upload formatted files - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: oauth-provider-format-preview - path: ${{ runner.temp }}/oauth-provider-format-preview/ - if-no-files-found: error - retention-days: 1