diff --git a/docs/architecture.md b/docs/architecture.md index d9d2056..5f290af 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -470,7 +470,6 @@ Everything else is internal by the design already written down: |---|---| | `/.well-known/heimdall-configuration` | GC apps, mesh-reachable | | `/v1/apps` | GC apps, mesh-reachable | -| `/v1/apps/{appSlug}/claims/issue` | GC app backends, mesh-reachable | | `/healthz` | local probes | The private command plane is not part of this question. It is diff --git a/docs/service-contract.md b/docs/service-contract.md index 7602ccc..2cffa2a 100644 --- a/docs/service-contract.md +++ b/docs/service-contract.md @@ -65,7 +65,6 @@ Current shape includes: - `configurationUri` - `oauthStartEndpoint` - `oauthCallbackEndpoint` -- `claimIssueEndpoint` - `supportedProviders` - `apps` @@ -497,63 +496,6 @@ Current helper contract: This is a reference seam, not a magical shared middleware product yet. -### `POST /v1/apps/{appSlug}/claims/issue` - -Purpose: - -- issue a signed Heimdall access claim for one app after identity/grant facts - have already been resolved - -This is currently the concrete claim contract and integration seam for early app -binding work. - -Request body: - -```json -{ - "accountId": "acct_repixelizer_001", - "displayName": "Meta", - "facts": ["entitlement.app_access", "grant.operator"], - "linkedIdentities": [ - { - "provider": "discord", - "providerUserId": "123456789", - "username": "meta" - } - ], - "accessRevision": 1, - "ttlSeconds": 3600 -} -``` - -Response: - -```json -{ - "session": { - "accountId": "acct_repixelizer_001", - "sessionId": "uuid", - "appSlug": "repixelizer", - "accessRevision": 1, - "expiresAt": "2026-05-26T12:00:00.000Z" - }, - "accessToken": "", - "refreshToken": "", - "refresh": { - "expiresAt": "2026-05-26T12:00:00.000Z" - }, - "claimSet": {}, - "verification": { - "issuer": "https://heimdall.gamecult.org", - "jwksUri": "https://heimdall.gamecult.org/.well-known/jwks.json", - "alg": "EdDSA", - "kid": "ed25519-..." - }, - "sharedCapabilities": ["app_access", "queue_submit", "admin_access"], - "hybridCapabilities": [] -} -``` - ## Access claim shape Current claim payload fields: diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 835c7be..9a4fdab 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -72,7 +72,6 @@ Do not trust this file for the exact live HEAD. Always check git. - `/v1/oauth/{provider}/start` - `/v1/oauth/{provider}/callback` - `/v1/apps/{appSlug}/auth-completions/redeem` - - `/v1/apps/{appSlug}/claims/issue` - `/v1/apps/{appSlug}/sessions/refresh` - `/v1/apps/{appSlug}/managed-credentials/resolve` - the Discord callback path now performs real code exchange, identity diff --git a/src/app.ts b/src/app.ts index 97d5e45..0b2f2a6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -21,7 +21,7 @@ import { type OAuthStartRequest, type Provider, } from "./contracts.js"; -import { mapIssueClaimRequest, issueAccessClaim, type IssueAccessClaimInput } from "./claims.js"; +import { issueAccessClaim, type IssueAccessClaimInput } from "./claims.js"; import { builtInAppProfiles, getAppProfile, serializeAppProfile, supportsProvider } from "./app-profiles.js"; import { AppRegistrationError, @@ -79,7 +79,6 @@ function buildDiscovery(config: HeimdallConfig) { configurationUri: `${config.publicBaseUrl}/.well-known/heimdall-configuration`, oauthStartEndpoint: `${config.publicBaseUrl}/v1/oauth/{provider}/start`, oauthCallbackEndpoint: `${config.publicBaseUrl}/v1/oauth/{provider}/callback`, - claimIssueEndpoint: `${config.publicBaseUrl}/v1/apps/{appSlug}/claims/issue`, supportedProviders: providers.map((provider) => ({ key: provider, displayName: providerCatalog[provider].displayName, @@ -1333,63 +1332,6 @@ export async function buildApp(options: BuildAppOptions = {}): Promise( - "/v1/apps/:appSlug/claims/issue", - { - schema: { - params: { - type: "object", - required: ["appSlug"], - additionalProperties: false, - properties: { - appSlug: { type: "string", enum: [...appSlugs] }, - }, - }, - body: { - type: "object", - required: ["accountId"], - additionalProperties: false, - properties: { - accountId: { type: "string", minLength: 1 }, - sessionId: { type: "string", minLength: 1 }, - displayName: { type: "string", minLength: 1 }, - facts: { - type: "array", - items: { type: "string" }, - uniqueItems: true, - }, - accessRevision: { type: "integer", minimum: 1 }, - ttlSeconds: { type: "integer", minimum: 60, maximum: 86400 }, - linkedIdentities: { - type: "array", - items: { - type: "object", - required: ["provider", "providerUserId"], - additionalProperties: false, - properties: { - provider: { type: "string", enum: [...providers] }, - providerUserId: { type: "string", minLength: 1 }, - username: { type: "string" }, - displayName: { type: "string" }, - }, - }, - }, - }, - }, - }, - }, - async (request, reply) => { - const issued = await issueAccessClaim({ - config, - keys, - store, - input: mapIssueClaimRequest(request.params.appSlug, request.body), - }); - reply.code(201); - return issued; - } - ); - app.post<{ Params: { appSlug: AppSlug }; Body: RefreshSessionRequest }>( "/v1/apps/:appSlug/sessions/refresh", { diff --git a/src/claims.ts b/src/claims.ts index 65d7a72..40300c1 100644 --- a/src/claims.ts +++ b/src/claims.ts @@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto"; import { type AccessClaimPayload, type AppSlug, - type IssueClaimRequest, type LinkedIdentityInput, type RefreshTokenPayload, } from "./contracts.js"; @@ -181,30 +180,3 @@ export async function issueAccessClaim(options: { hybridCapabilities: profile.capabilities.filter((capability) => capability.mode === "hybrid"), }; } - -export function mapIssueClaimRequest(appSlug: AppSlug, request: IssueClaimRequest): IssueAccessClaimInput { - const input: IssueAccessClaimInput = { - appSlug, - accountId: request.accountId, - linkedIdentities: request.linkedIdentities ?? [], - facts: request.facts ?? [], - }; - - if (request.displayName !== undefined) { - input.displayName = request.displayName; - } - - if (request.sessionId !== undefined) { - input.sessionId = request.sessionId; - } - - if (request.accessRevision !== undefined) { - input.accessRevision = request.accessRevision; - } - - if (request.ttlSeconds !== undefined) { - input.ttlSeconds = request.ttlSeconds; - } - - return input; -} diff --git a/src/contracts.ts b/src/contracts.ts index 970bd7e..743bfdc 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -103,16 +103,6 @@ export interface LinkedIdentityInput { displayName?: string; } -export interface IssueClaimRequest { - accountId: string; - sessionId?: string; - displayName?: string; - facts?: string[]; - linkedIdentities?: LinkedIdentityInput[]; - accessRevision?: number; - ttlSeconds?: number; -} - export interface RedeemAuthCompletionRequest { completionCode: string; } diff --git a/tests/app.test.ts b/tests/app.test.ts index 87e3ee3..938003b 100644 --- a/tests/app.test.ts +++ b/tests/app.test.ts @@ -2,10 +2,9 @@ import { afterEach, describe, expect, it } from "vitest"; import { type FastifyInstance } from "fastify"; import { buildApp } from "../src/app.js"; import { type HeimdallConfig } from "../src/config.js"; -import { entitlementFacts, grantFacts, identityFacts } from "../src/facts.js"; +import { entitlementFacts, identityFacts } from "../src/facts.js"; import { createOAuthRuntimeRegistry, type OAuthProviderRuntime } from "../src/oauth.js"; import { verifyJwt } from "../src/signing.js"; -import { createHeimdallAccessTokenVerifier } from "../src/verifier.js"; import { InMemoryStore } from "../src/store/index.js"; function createTestConfig(): HeimdallConfig { @@ -834,51 +833,24 @@ describe("Heimdall service", () => { expect(verified.valid).toBe(true); }); - it("issues direct claims from provider-agnostic entitlement facts", async () => { + it("no longer exposes a route to mint claims from caller-supplied facts", async () => { const app = await buildApp({ config: createTestConfig() }); apps.push(app); const response = await app.inject({ method: "POST", url: "/v1/apps/repixelizer/claims/issue", - payload: { - accountId: "acct_repixelizer_001", - displayName: "Meta", - facts: [entitlementFacts.appAccess, grantFacts.operator], - linkedIdentities: [ - { - provider: "discord", - providerUserId: "123456789", - username: "meta", - }, - ], - }, + payload: { accountId: "probe" }, }); - expect(response.statusCode).toBe(201); - const payload = response.json(); - expect(payload.sharedCapabilities).toEqual( - expect.arrayContaining(["app_access", "queue_submit", "admin_access"]) - ); + expect(response.statusCode).toBe(404); + expect(app.printRoutes()).not.toContain("claims/issue"); - const verifier = createHeimdallAccessTokenVerifier({ - issuer: "https://heimdall.gamecult.org", - appSlug: "repixelizer", - jwks: { - keys: [getHeimdallContext(app).keys.publicJwk], - }, + const discoveryResponse = await app.inject({ + method: "GET", + url: "/.well-known/heimdall-configuration", }); - expect(verifier.verify(payload.accessToken as string)).toEqual( - expect.objectContaining({ - valid: true, - claimSet: expect.objectContaining({ - aud: "repixelizer", - app: expect.objectContaining({ - slug: "repixelizer", - }), - }), - }) - ); + expect(discoveryResponse.json()).not.toHaveProperty("claimIssueEndpoint"); }); it("exposes StreamPixels hybrid capability seams without granting them blindly", async () => {