publishing: unfenced vault accessor for the amplify path

createPublishingCredentialAccessor is execution-fenced (Temporal contexts
only) — the API-process amplify route threw EXECUTION_CREDENTIAL_FENCE.
Use the established PgConnectionStore+vault accessor instead (same trust
level as the archive amplify route's bot-token path).
This commit is contained in:
glass beadian 2026-10-13 18:32:38 +00:00
parent 6145281b3f
commit 96e5a63dd8
11 changed files with 1501 additions and 8 deletions

View file

@ -0,0 +1,72 @@
import { Body, Controller, Get, Header, Headers, HttpCode, Inject, Post, Query, Redirect, Req, UnauthorizedException, UseGuards } from "@nestjs/common";
import type { Request } from "express";
import { RbacGuard, type AuthenticatedRequest } from "../rbac/rbac.guard.js";
import { requireSession } from "../http/validation.js";
import { OidcService, type TokenResponse, type UserInfo } from "./oidc.service.js";
import type { PublicJwk } from "./oidc.keys.js";
/**
* The Hub's OpenID Connect provider (issuer https://api.occupymedia.cc/oidc).
*
* GET /oidc/.well-known/openid-configuration discovery
* GET /oidc/jwks public ES256 keys
* GET /oidc/authorize validate, then 302 → Hub SPA consent page
* POST /oidc/authorize/consent (Bearer session) issue the single-use code
* POST /oidc/token code + PKCE verifier → tokens
* GET /oidc/userinfo Bearer access_token → claims
*
* Only /authorize/consent is session-guarded: every active member may sign in
* to files, so it needs no permission. See OidcService for the cookie-less
* authorize flow (a deliberate deviation from the design doc).
*/
@Controller("oidc")
export class OidcController {
constructor(@Inject(OidcService) private readonly oidc: OidcService) {}
@Get(".well-known/openid-configuration")
@Header("Cache-Control", "public, max-age=300")
discovery(): Record<string, unknown> {
return this.oidc.discovery();
}
@Get("jwks")
@Header("Cache-Control", "public, max-age=300")
jwks(): Promise<{ keys: PublicJwk[] }> {
return this.oidc.jwks();
}
@Get("authorize")
@Redirect()
@Header("Cache-Control", "no-store")
authorize(@Query() query: unknown): { url: string; statusCode: number } {
return { url: this.oidc.authorizeRedirect(query), statusCode: 302 };
}
@Post("authorize/consent")
@HttpCode(200)
@UseGuards(RbacGuard)
@Header("Cache-Control", "no-store")
consent(@Req() request: Request & AuthenticatedRequest, @Body() body: unknown): Promise<{ redirectTo: string }> {
const session = requireSession(request);
return this.oidc.consent(session.workspaceId, session.userId, body);
}
@Post("token")
@HttpCode(200)
@Header("Cache-Control", "no-store")
@Header("Pragma", "no-cache")
token(@Body() body: unknown): Promise<TokenResponse> {
return this.oidc.token(body);
}
@Get("userinfo")
@Header("Cache-Control", "no-store")
async userinfo(@Req() request: Request, @Headers("authorization") authorization: string | undefined): Promise<UserInfo> {
try {
return await this.oidc.userinfo(authorization);
} catch (err) {
if (err instanceof UnauthorizedException) request.res?.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"');
throw err;
}
}
}