Add WorkOS web auth callback#32
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
WalkthroughThis PR implements POST /v1/auth/web/callback: verifies WorkOS JWTs via remote JWKS, extracts sid/jti into a WebCallbackIdentity (token_id or session_id), derives a stable idempotency key, rejects if neither id is present, and invokes db.resolveWebMember inside runIdempotent to perform idempotent provisioning or "seen" updates. DB lookups were narrowed to read-only getWebMemberByWorkOsUserId and tests/docs were updated accordingly. Sequence Diagram(s)sequenceDiagram
participant Browser
participant ApiServer
participant AuthService
participant RemoteJWKS
participant DB
Browser->>ApiServer: POST /v1/auth/web/callback (access_token)
ApiServer->>AuthService: verifyWebToken(token)
AuthService->>RemoteJWKS: obtain JWK set
RemoteJWKS-->>AuthService: keys
AuthService-->>ApiServer: WebCallbackIdentity (workos_user_id, email, token_id?|session_id?)
ApiServer->>ApiServer: webCallbackIdempotencyKey(identity)
ApiServer->>DB: runIdempotent(idempotencyKey, resolveWebMember(...))
DB-->>ApiServer: webAuthResponse
ApiServer-->>Browser: 200 / 401 not_authenticated if missing id
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-32.isaac-a46.workers.dev |
|
@CodeRabbit review please |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/db/src/local-repository.ts (1)
172-227:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse the new idempotency key in
resolveWebMemberto make callback replays deterministic.Line 172 introduces
idempotencyKey, but the method never uses it. Replays can still mutatelast_seen_at/Proposed fix
- async resolveWebMember(input: { workosUserId: string; email: string; idempotencyKey?: string; now?: string }) { + async resolveWebMember(input: { workosUserId: string; email: string; idempotencyKey: string; now?: string }) { const now = input.now ?? new Date().toISOString(); const existing = [...this.workspaceMembers.values()].find((member) => member.workos_user_id === input.workosUserId); if (existing) { - existing.email = input.email; - existing.last_seen_at = now; - return this.webAuthResponse(existing, null); + return this.runIdempotent( + `web.member.seen:${existing.workspace_id}:${input.idempotencyKey}`, + () => { + existing.email = input.email; + existing.last_seen_at = now; + return this.webAuthResponse(existing, null); + }, + ); } - const workspace: Workspace = { - id: crypto.randomUUID(), - name: `${input.email.split("@")[0] ?? "user"}'s Workspace`, - contact_email: input.email, - created_at: now, - updated_at: now, - }; - this.workspaces.set(workspace.id, workspace); - - const member: WorkspaceMember = { - id: createId("mem"), - workspace_id: workspace.id, - workos_user_id: input.workosUserId, - email: input.email, - scopes: [...DEFAULT_MEMBER_SCOPES], - created_at: now, - last_seen_at: now, - }; - this.workspaceMembers.set(member.id, member); - - const generated = await generateApiKey(this.options.apiKeyEnv ?? "preview", this.options.apiKeyPepper); - const apiKey: ApiKey = { - id: createId("key"), - workspace_id: workspace.id, - public_id: generated.publicId, - name: "Default", - secret_hmac: generated.secretHmac, - pepper_kid: 1, - scopes: ["publish", "read"], - revoked_at: null, - last_used_at: null, - created_at: now, - }; - this.apiKeys.set(apiKey.id, apiKey); - this.addEvent("system", "web-auth", "workspace.created", "workspace", workspace.id, workspace.id, {}, now); - this.addEvent( - "system", - "web-auth", - "api_key.created", - "api_key", - apiKey.id, - workspace.id, - { name: apiKey.name, public_id: apiKey.public_id }, - now, - ); - return this.webAuthResponse(member, { api_key: toApiKeySummary(apiKey), secret: generated.secret }); + return this.runIdempotent(`web.member.provision:workos-user:${input.workosUserId}`, async () => { + const workspace: Workspace = { + id: crypto.randomUUID(), + name: `${input.email.split("@")[0] ?? "user"}'s Workspace`, + contact_email: input.email, + created_at: now, + updated_at: now, + }; + this.workspaces.set(workspace.id, workspace); + + const member: WorkspaceMember = { + id: createId("mem"), + workspace_id: workspace.id, + workos_user_id: input.workosUserId, + email: input.email, + scopes: [...DEFAULT_MEMBER_SCOPES], + created_at: now, + last_seen_at: now, + }; + this.workspaceMembers.set(member.id, member); + + const generated = await generateApiKey(this.options.apiKeyEnv ?? "preview", this.options.apiKeyPepper); + const apiKey: ApiKey = { + id: createId("key"), + workspace_id: workspace.id, + public_id: generated.publicId, + name: "Default", + secret_hmac: generated.secretHmac, + pepper_kid: 1, + scopes: ["publish", "read"], + revoked_at: null, + last_used_at: null, + created_at: now, + }; + this.apiKeys.set(apiKey.id, apiKey); + this.addEvent("system", "web-auth", "workspace.created", "workspace", workspace.id, workspace.id, {}, now); + this.addEvent( + "system", + "web-auth", + "api_key.created", + "api_key", + apiKey.id, + workspace.id, + { name: apiKey.name, public_id: apiKey.public_id }, + now, + ); + return this.webAuthResponse(member, { api_key: toApiKeySummary(apiKey), secret: generated.secret }); + }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/db/src/local-repository.ts` around lines 172 - 227, resolveWebMember accepts idempotencyKey but never uses it, so replayed callbacks can mutate member fields and produce different payloads; add deterministic idempotency handling by introducing a local map (e.g. this.webAuthIdempotency or this.idempotencyMap) keyed by idempotencyKey that stores the canonical response (member id, api key id/public_id, and secret or the full webAuthResponse payload). In resolveWebMember, if input.idempotencyKey is provided then: 1) check the map and if an entry exists return that stored webAuthResponse without updating last_seen_at/email; 2) if no entry exists proceed to create/find member/workspace/apiKey as before but do not rely on transient timestamps being mutated on replays, then store the exact webAuthResponse you will return into the idempotency map under that key before returning. Ensure you reference resolveWebMember, this.workspaceMembers, this.apiKeys, addEvent, and webAuthResponse when implementing the storage and lookup so subsequent calls with the same idempotencyKey return the identical payload.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/index.ts`:
- Around line 328-330: The callback path currently calls
webCallbackIdempotencyKey(identity) but AuthService.verifyWebToken() returns
WorkOsIdentity with optional token_id/session_id, allowing custom verifiers to
compile but always cause 401; fix by tightening the verifier contract or
enforcing the requirement at verify boundary: introduce a narrower type (e.g.,
WebCallbackIdentity) that requires token_id or session_id and update
AuthService.verifyWebToken to return that type (or make a new method
verifyWebCallbackToken), or add an immediate runtime check after verify that
throws or returns an authentication error with a clear message if both token_id
and session_id are missing; update all AuthService implementations to satisfy
the new return type and adjust the callback usage of webCallbackIdempotencyKey
to expect WebCallbackIdentity.
- Around line 332-339: You detached the instance method db.resolveWebMember into
a local variable which loses its receiver (this) used inside resolveWebMember
(it calls this.runIdempotent, this.workspaceMembers, this.webAuthResponse);
instead call the method on the db instance directly (e.g., return
runIdempotent(context, () => db.resolveWebMember({...})) or explicitly bind the
receiver (db.resolveWebMember.bind(db)) so resolveWebMember retains access to
this and its instance fields/methods.
In `@apps/api/src/workos.ts`:
- Around line 122-124: The code currently sets cacheMaxAge: Infinity when
creating the JWKS resolver via createRemoteJWKSet and also memoizes resolvers in
remoteJwksCache (remoteWorkOsJwks), which prevents timely key rotation; change
cacheMaxAge from Infinity to a finite, reasonable TTL (for example 1 hour or a
configurable value) and ensure remoteWorkOsJwks / remoteJwksCache uses that TTL
so the resolver will refetch keys after expiry; update createRemoteJWKSet
invocation and any configuration around remoteWorkOsJwks/remoteJwksCache to
accept and respect a finite cacheMaxAge instead of Infinity.
---
Outside diff comments:
In `@packages/db/src/local-repository.ts`:
- Around line 172-227: resolveWebMember accepts idempotencyKey but never uses
it, so replayed callbacks can mutate member fields and produce different
payloads; add deterministic idempotency handling by introducing a local map
(e.g. this.webAuthIdempotency or this.idempotencyMap) keyed by idempotencyKey
that stores the canonical response (member id, api key id/public_id, and secret
or the full webAuthResponse payload). In resolveWebMember, if
input.idempotencyKey is provided then: 1) check the map and if an entry exists
return that stored webAuthResponse without updating last_seen_at/email; 2) if no
entry exists proceed to create/find member/workspace/apiKey as before but do not
rely on transient timestamps being mutated on replays, then store the exact
webAuthResponse you will return into the idempotency map under that key before
returning. Ensure you reference resolveWebMember, this.workspaceMembers,
this.apiKeys, addEvent, and webAuthResponse when implementing the storage and
lookup so subsequent calls with the same idempotencyKey return the identical
payload.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 83632757-6899-47ed-8fe5-42eb69975d79
📒 Files selected for processing (10)
apps/api/src/index.test.tsapps/api/src/index.tsapps/api/src/workos.test.tsapps/api/src/workos.tsdocs/ops/project-status.mddocs/ops/web-app-todo.mdpackages/contracts/src/routes.tspackages/db/src/index.test.tspackages/db/src/local-repository.tspackages/db/src/postgres/repository.ts
32786ab to
9bc4d38
Compare
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-32.isaac-a46.workers.dev |
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/db/src/local-repository.ts (1)
209-263:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRollback the staged workspace/member if default-key generation fails.
WorkspaceandWorkspaceMemberare persisted before the firstawait. IfgenerateApiKey(...)throws, the cache entry is removed but the half-provisioned member remains, so every retry falls into the existing-member path and never gets adefault_api_key.💡 Localized fix
this.workspaceMembers.set(member.id, member); - const generated = await generateApiKey(this.options.apiKeyEnv ?? "preview", this.options.apiKeyPepper); + let generated: Awaited<ReturnType<typeof generateApiKey>>; + try { + generated = await generateApiKey(this.options.apiKeyEnv ?? "preview", this.options.apiKeyPepper); + } catch (error) { + this.workspaceMembers.delete(member.id); + this.workspaces.delete(workspace.id); + throw error; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/db/src/local-repository.ts` around lines 209 - 263, In resolveWebMemberOnce, workspace and member are inserted into this.workspaces and this.workspaceMembers before awaiting generateApiKey, so if generateApiKey throws we leave a half-provisioned member; either move creation of workspace/member until after generateApiKey succeeds, or wrap the generateApiKey call in try/catch and on error remove the staged entries (this.workspaces.delete(workspace.id) and this.workspaceMembers.delete(member.id)) before rethrowing; ensure any events and this.apiKeys insertion happen only after generateApiKey returns and reference symbols: resolveWebMemberOnce, generateApiKey, this.workspaces, this.workspaceMembers, this.apiKeys, addEvent, toApiKeySummary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/index.test.ts`:
- Around line 199-239: The test doesn't assert that the session-only callback
maps session_id -> the idempotency key sent to resolveWebMember; update the db
mock's resolveWebMember (the async resolveWebMember(this: { marker: string },
input: { email: string }) stub) to accept and return the idempotencyKey from its
input, then add an assertion on the response JSON that idempotencyKey equals the
expected fallback key (e.g. "workos-sid:sess_1") so handleRequest (driven by
AUTH.verifyWebToken returning session_id) is verified to pass the correct
idempotencyKey to resolveWebMember.
In `@apps/api/src/index.ts`:
- Around line 707-715: The current hasWebCallbackId incorrectly treats empty
strings as present, causing webCallbackIdempotencyKey to sometimes return
"workos-session:undefined"; update hasWebCallbackId to only accept non-empty
strings for token_id and session_id (e.g., typeof === "string" &&
identity.token_id.length>0) and make webCallbackIdempotencyKey defensively
handle missing values (throw or return a deterministic error/invalid key) so
empty identifiers cannot collapse unrelated callbacks—apply these checks around
WorkOsIdentity/WebCallbackIdentity and ensure AuthService.verifyWebToken callers
will fail closed rather than produce a shared idempotency key.
In `@docs/ops/web-app-todo.md`:
- Around line 42-43: Update the POST /v1/auth/web/callback implementation
(agents/workos-callback-api / apps/api code that uses jose.createRemoteJWKSet
and runCommand) to extract session_id from JWT sid and token_id from JWT jti,
derive and use idempotency keys such as workos-jti:{jti} or workos-session:{sid}
when performing upserts, and ensure first-time provisioning is gated by a
deterministic key like workos-user:{workos_user_id} to avoid duplicate Personal
Workspace creation; wire these keys into the existing upsert/provision logic
(workspace_members upsert, Personal Workspace + Workspace Member + default API
Key via runCommand) so the callback is idempotent and concurrency-safe.
In `@packages/db/src/postgres/repository.ts`:
- Around line 233-328: The handler must replay a prior callback by
input.idempotencyKey before branching on existing member; add an initial
top-level call to runAdminCommand (actor { type: "system", id: "web-auth" },
action like "web.member.replay") using input.idempotencyKey and null workspace
to check for and return any previously stored result, and only if that returns
nothing continue the current logic that checks
workspaceMemberQueries.findByWorkOsUserId and then uses the per-user
provisioning key (`workos-user:${input.workosUserId}`) for the provisioning
branch; update resolveWebMember to perform this idempotency-key replay-first
step (referencing runAdminCommand, resolveWebMember, input.idempotencyKey,
workspaceMemberQueries.findByWorkOsUserId, and the existing
"web.member.seen"/"web.member.provision" branches).
---
Outside diff comments:
In `@packages/db/src/local-repository.ts`:
- Around line 209-263: In resolveWebMemberOnce, workspace and member are
inserted into this.workspaces and this.workspaceMembers before awaiting
generateApiKey, so if generateApiKey throws we leave a half-provisioned member;
either move creation of workspace/member until after generateApiKey succeeds, or
wrap the generateApiKey call in try/catch and on error remove the staged entries
(this.workspaces.delete(workspace.id) and
this.workspaceMembers.delete(member.id)) before rethrowing; ensure any events
and this.apiKeys insertion happen only after generateApiKey returns and
reference symbols: resolveWebMemberOnce, generateApiKey, this.workspaces,
this.workspaceMembers, this.apiKeys, addEvent, toApiKeySummary.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 7408f5f5-d5dd-4ab5-9bb7-8d0fed1cbc10
📒 Files selected for processing (10)
apps/api/src/index.test.tsapps/api/src/index.tsapps/api/src/workos.test.tsapps/api/src/workos.tsdocs/ops/project-status.mddocs/ops/web-app-todo.mdpackages/contracts/src/routes.tspackages/db/src/index.test.tspackages/db/src/local-repository.tspackages/db/src/postgres/repository.ts
| it("calls resolveWebMember with the database receiver intact", async () => { | ||
| const db = { | ||
| marker: "receiver-kept", | ||
| async getWhoami() { | ||
| return {}; | ||
| }, | ||
| async getAgentView() { | ||
| return null; | ||
| }, | ||
| async getPublicAgentView() { | ||
| return null; | ||
| }, | ||
| async resolveWebMember(this: { marker: string }, input: { email: string }) { | ||
| return { receiver: this.marker, email: input.email }; | ||
| }, | ||
| async runCleanup() { | ||
| return {}; | ||
| }, | ||
| }; | ||
| const env: Env = { | ||
| AUTH: { | ||
| async verifyApiKey() { | ||
| return null; | ||
| }, | ||
| async verifyWebToken() { | ||
| return { workos_user_id: "user_1", email: "user@example.com", session_id: "sess_1" }; | ||
| }, | ||
| }, | ||
| DB: db as unknown as Env["DB"], | ||
| }; | ||
|
|
||
| const response = await handleRequest( | ||
| new Request("https://api.test/v1/auth/web/callback", { | ||
| method: "POST", | ||
| headers: { authorization: "Bearer workos-ok" }, | ||
| }), | ||
| env, | ||
| ); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| await expect(response.json()).resolves.toMatchObject({ receiver: "receiver-kept", email: "user@example.com" }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Assert the session_id fallback key in this test.
This is the only session-only callback path, but it never checks what idempotencyKey reaches resolveWebMember. A regression in the session_id → workos-sid:* mapping would still pass here.
🧪 Tighten the assertion
- async resolveWebMember(this: { marker: string }, input: { email: string }) {
+ async resolveWebMember(this: { marker: string }, input: { email: string; idempotencyKey: string }) {
+ expect(input.idempotencyKey).toBe("workos-sid:sess_1");
return { receiver: this.marker, email: input.email };
},🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/index.test.ts` around lines 199 - 239, The test doesn't assert
that the session-only callback maps session_id -> the idempotency key sent to
resolveWebMember; update the db mock's resolveWebMember (the async
resolveWebMember(this: { marker: string }, input: { email: string }) stub) to
accept and return the idempotencyKey from its input, then add an assertion on
the response JSON that idempotencyKey equals the expected fallback key (e.g.
"workos-sid:sess_1") so handleRequest (driven by AUTH.verifyWebToken returning
session_id) is verified to pass the correct idempotencyKey to resolveWebMember.
| function hasWebCallbackId(identity: WorkOsIdentity): identity is WebCallbackIdentity { | ||
| return typeof identity.token_id === "string" || typeof identity.session_id === "string"; | ||
| } | ||
|
|
||
| function webCallbackIdempotencyKey(identity: WebCallbackIdentity): string { | ||
| if (identity.token_id) { | ||
| return `workos-jti:${identity.token_id}`; | ||
| } | ||
| return `workos-session:${identity.session_id}`; |
There was a problem hiding this comment.
Reject blank WorkOS callback identifiers.
hasWebCallbackId treats "" as present, but webCallbackIdempotencyKey treats it as absent and can fall through to workos-session:undefined. A custom AuthService.verifyWebToken implementation can then collapse unrelated callbacks onto the same idempotency key instead of failing closed.
Suggested fix
function hasWebCallbackId(identity: WorkOsIdentity): identity is WebCallbackIdentity {
- return typeof identity.token_id === "string" || typeof identity.session_id === "string";
+ return (
+ (typeof identity.token_id === "string" && identity.token_id.length > 0) ||
+ (typeof identity.session_id === "string" && identity.session_id.length > 0)
+ );
}
function webCallbackIdempotencyKey(identity: WebCallbackIdentity): string {
- if (identity.token_id) {
+ if (typeof identity.token_id === "string" && identity.token_id.length > 0) {
return `workos-jti:${identity.token_id}`;
}
- return `workos-session:${identity.session_id}`;
+ if (typeof identity.session_id === "string" && identity.session_id.length > 0) {
+ return `workos-session:${identity.session_id}`;
+ }
+ throw new Error("missing WorkOS token_id or session_id");
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function hasWebCallbackId(identity: WorkOsIdentity): identity is WebCallbackIdentity { | |
| return typeof identity.token_id === "string" || typeof identity.session_id === "string"; | |
| } | |
| function webCallbackIdempotencyKey(identity: WebCallbackIdentity): string { | |
| if (identity.token_id) { | |
| return `workos-jti:${identity.token_id}`; | |
| } | |
| return `workos-session:${identity.session_id}`; | |
| function hasWebCallbackId(identity: WorkOsIdentity): identity is WebCallbackIdentity { | |
| return ( | |
| (typeof identity.token_id === "string" && identity.token_id.length > 0) || | |
| (typeof identity.session_id === "string" && identity.session_id.length > 0) | |
| ); | |
| } | |
| function webCallbackIdempotencyKey(identity: WebCallbackIdentity): string { | |
| if (typeof identity.token_id === "string" && identity.token_id.length > 0) { | |
| return `workos-jti:${identity.token_id}`; | |
| } | |
| if (typeof identity.session_id === "string" && identity.session_id.length > 0) { | |
| return `workos-session:${identity.session_id}`; | |
| } | |
| throw new Error("missing WorkOS token_id or session_id"); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/index.ts` around lines 707 - 715, The current hasWebCallbackId
incorrectly treats empty strings as present, causing webCallbackIdempotencyKey
to sometimes return "workos-session:undefined"; update hasWebCallbackId to only
accept non-empty strings for token_id and session_id (e.g., typeof === "string"
&& identity.token_id.length>0) and make webCallbackIdempotencyKey defensively
handle missing values (throw or return a deterministic error/invalid key) so
empty identifiers cannot collapse unrelated callbacks—apply these checks around
WorkOsIdentity/WebCallbackIdentity and ensure AuthService.verifyWebToken callers
will fail closed rather than produce a shared idempotency key.
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-32.isaac-a46.workers.dev |
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
|
agent-paste PR preview resources were cleaned up. The pr-preview-${context.issue.number} environment is left in place; remove it from the GitHub UI if desired. |
* feat(api): add WorkOS web auth callback * fix(api): address WorkOS callback review * fix(api): address WorkOS callback review follow-ups * fix(api): address WorkOS callback review follow-ups
* Add WorkOS web auth callback (#32) * feat(api): add WorkOS web auth callback * fix(api): address WorkOS callback review * fix(api): address WorkOS callback review follow-ups * fix(api): address WorkOS callback review follow-ups * feat(api): add dashboard workspace artifact reads
Summary
Implements
POST /v1/auth/web/callbackfor the WorkOS/AuthKit web sign-in flow.Changes
client_idclaim, and resolves canonical WorkOS user email server-side.jose.createRemoteJWKSetwith process-lifetime JWKS cache and extractssid/jtiinto the API identity model for deterministic callback idempotency.Risk
High: this touches auth/session admission and first-login workspace provisioning. The implementation is fail-closed for missing deterministic token/session IDs and keeps WorkOS user lookup server-side.
Test plan
pnpm --filter @agent-paste/api testpnpm --filter @agent-paste/api typecheckpnpm --filter @agent-paste/contracts testpnpm --filter @agent-paste/contracts typecheckpnpm --filter @agent-paste/contracts openapi:checkpnpm --filter @agent-paste/db testpnpm --filter @agent-paste/db typecheckpnpm --filter @agent-paste/db db:checkpnpm verifyturbo run testfor the configured package setCodeRabbit notes
coderabbit review --agent --base agents/web-member-schemafound 5 issues; addressed unused lookup email input, deterministic callback idempotency, provisioning idempotency documentation/commentary, and docs forsid/jtiextraction.coderabbit review --agent --base agents/web-member-schemacompleted with 0 findings.Summary by CodeRabbit
New Features
Documentation
Tests