diff --git a/packages/server/package.json b/packages/server/package.json index f2ad2656..2b142a39 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -21,7 +21,8 @@ "@underlay/web": "workspace:*", "better-auth": "^1.6.11", "drizzle-orm": "^0.45.2", - "hono": "^4.12.18" + "hono": "^4.12.18", + "jose": "^6.2.3" }, "devDependencies": { "@cloudflare/workers-types": "^5.20261003.1", diff --git a/packages/server/src/api/admin.ts b/packages/server/src/api/admin.ts index aeae199e..beb5402a 100644 --- a/packages/server/src/api/admin.ts +++ b/packages/server/src/api/admin.ts @@ -4,7 +4,7 @@ * * GET|PUT /api/admin/explore-tags {tags: string[]} stewards * GET|PUT /api/admin/explore-collections {collections: "owner/slug"[]} stewards - * GET /api/kf/summary?kf_org_id= KF Auth, with the internal API key + * GET /api/kf/summary?kf_org_id= the KF console, with its KF-Console-Token * POST /api/abuse-reports {hash?, url?, reason, contact?}: anyone * GET /api/admin/abuse-reports stewards: ?status=open|blocked|dismissed * PATCH /api/admin/abuse-reports/:id stewards: {status: 'dismissed'|'open'} @@ -108,11 +108,14 @@ export function adminRoutes() { } app.get('/api/kf/summary', async (c) => { - const kfOrgId = c.req.query('kf_org_id') - if (!kfOrgId) return jsonError(c, 400, 'kf_org_id is required') - if (!c.var.kf?.isInternalCall(c.req.header('authorization'))) { + // The token is for the URL registered as underlay's summary_url in kf-console. + const registered = new URL('/api/kf/summary', c.var.config.appUrl).href + const token = c.req.header('kf-console-token') + if (!(await c.var.kf?.isConsoleCall(token, 'GET', registered))) { return jsonError(c, 401, 'Unauthorized') } + const kfOrgId = c.req.query('kf_org_id') + if (!kfOrgId) return jsonError(c, 400, 'kf_org_id is required') const { db } = c.var.ports const appUrl = c.var.config.appUrl const orgs = await db diff --git a/packages/server/src/auth/kf.ts b/packages/server/src/auth/kf.ts index 74293808..8e849363 100644 --- a/packages/server/src/auth/kf.ts +++ b/packages/server/src/auth/kf.ts @@ -1,8 +1,10 @@ /** * KF Auth beyond sign-in: a user's profile and role (OIDC userinfo with their - * stored token, refreshed when it expires) and, when the deployment has the - * service key, the internal API for the KF orgs a user belongs to. Ported from - * v1's `src/lib/auth.server.ts` and `src/lib/auth-internal.server.ts`. + * stored token, refreshed when it expires), the internal API for the KF orgs a + * user belongs to (called as the app, with its client credentials), and the + * check on calls from the console (its signed `KF-Console-Token`). Ported from + * v1's `src/lib/auth.server.ts` and `src/lib/auth-internal.server.ts`; the + * console side follows kf-console's docs/calling-kf.md. * * Every call fails soft: KF Auth being down costs a role or an org list, never * a page. @@ -13,17 +15,20 @@ * account row again for the token the winner stored. */ import { and, eq } from 'drizzle-orm' +import { createRemoteJWKSet, customFetch, jwtVerify } from 'jose' import * as schema from '../db/schema.js' import type { Db } from '../ports.js' export interface KfConfig { + /** KF Auth's public origin: console tokens are issued by `/api/auth`. */ + issuerUrl: string /** Issuer URL for server-to-server calls. */ internalUrl: string + /** This app's client (`kf_underlay`): the audience of console tokens. */ clientId: string + /** Its secret: with the id, the Basic credentials for KF Auth's internal API. */ clientSecret: string - /** Shared secret for KF Auth's internal API (`AUTH_INTERNAL_API_KEY`); optional. */ - internalApiKey?: string | undefined } export interface KfProfile { @@ -49,18 +54,25 @@ export interface Kf { * read, the role read in the last 30 s. */ role(userId: string): Promise - /** KF orgs the user belongs to; [] without the internal API. */ + /** KF orgs the user belongs to; [] without a client secret. */ orgs(userId: string): Promise /** Whether the user belongs to KF org `kfOrgId`. */ entitled(userId: string, kfOrgId: string): Promise /** The user's personal KF org (else their first), for new orgs. */ defaultOrgId(userId: string): Promise - /** Whether an Authorization header carries the internal API key. */ - isInternalCall(authorization: string | undefined): boolean + /** + * Whether `token` (a request's `KF-Console-Token`) is the console calling + * `method registeredUrl` on this app. The registered URL, not the one the + * request arrived at, which a proxy may have rewritten. + */ + isConsoleCall(token: string | undefined, method: string, registeredUrl: string): Promise } const PROFILE_TTL_MS = 30_000 const TIMEOUT_MS = 5_000 +/** Console tokens live 60 s; KF allows 30 s of clock skew. */ +const CONSOLE_TOKEN_MAX_AGE_S = 60 +const CLOCK_SKEW_S = 30 export interface KfOptions { /** How long a failed refresh waits before it reads the account row again. */ @@ -87,6 +99,13 @@ export function createKf( const refreshing = new Map>() const call = (url: string, init: RequestInit = {}) => fetcher(url, { ...init, signal: AbortSignal.timeout(TIMEOUT_MS) }) + const basic = `Basic ${btoa(`${cfg.clientId}:${cfg.clientSecret}`)}` + const issuer = `${cfg.issuerUrl}/api/auth` + // Cached per isolate; fetched again when a token names a new key (keys rotate). + const consoleKeys = createRemoteJWKSet(new URL(`${cfg.internalUrl}/api/auth/jwks`), { + timeoutDuration: TIMEOUT_MS, + [customFetch]: (url, init) => fetcher(url, init), + }) const account = async (userId: string): Promise => { const [acct] = await db @@ -190,10 +209,10 @@ export function createKf( } async function internal(path: string): Promise { - if (!cfg.internalApiKey) return null + if (!cfg.clientSecret) return null try { const res = await call(`${cfg.internalUrl}/api/internal${path}`, { - headers: { authorization: `Bearer ${cfg.internalApiKey}` }, + headers: { authorization: basic }, }) return res.ok ? ((await res.json()) as T) : null } catch { @@ -238,7 +257,7 @@ export function createKf( return hit && Date.now() - hit.at < PROFILE_TTL_MS ? hit.profile.role : null }, async orgs(userId) { - if (!cfg.internalApiKey) return [] + if (!cfg.clientSecret) return [] const [acct] = await db .select({ accountId: schema.account.accountId }) .from(schema.account) @@ -264,19 +283,25 @@ export function createKf( const orgs = await kf.orgs(userId) return (orgs.find((o) => o.type === 'personal') ?? orgs[0])?.id ?? null }, - isInternalCall(authorization) { - return ( - !!cfg.internalApiKey && timingSafeEqual(authorization ?? '', `Bearer ${cfg.internalApiKey}`) - ) + async isConsoleCall(token, method, registeredUrl) { + if (!token) return false + try { + const { payload } = await jwtVerify(token, consoleKeys, { + issuer, + audience: cfg.clientId, + // What tells it apart from an ID token, which has the same issuer and audience. + subject: 'kf-console', + algorithms: ['EdDSA'], + maxTokenAge: CONSOLE_TOKEN_MAX_AGE_S, + clockTolerance: CLOCK_SKEW_S, + }) + const url = new URL(registeredUrl) + return payload.htm === method && payload.htu === `${url.origin}${url.pathname}` + } catch (err) { + console.warn('[kf] console token refused:', String(err)) + return false + } }, } return kf } - -function timingSafeEqual(a: string, b: string): boolean { - const x = new TextEncoder().encode(a) - const y = new TextEncoder().encode(b) - let diff = x.length ^ y.length - for (let i = 0; i < Math.max(x.length, y.length); i++) diff |= (x[i] ?? 0) ^ (y[i] ?? 0) - return diff === 0 -} diff --git a/packages/server/src/node/main.ts b/packages/server/src/node/main.ts index 51555d0c..3182e69a 100644 --- a/packages/server/src/node/main.ts +++ b/packages/server/src/node/main.ts @@ -178,7 +178,7 @@ const oidc = { clientId: env.OIDC_CLIENT_ID ?? 'kf_underlay', clientSecret: env.OIDC_CLIENT_SECRET ?? '', } -const kf = createKf(db, { ...oidc, internalApiKey: env.AUTH_INTERNAL_API_KEY }) +const kf = createKf(db, oidc) const auth = createAuth( db, { appUrl, secret: env.SESSION_SECRET ?? 'dev-secret-change-me', oidc }, diff --git a/packages/server/src/worker.ts b/packages/server/src/worker.ts index 564033ae..47c46870 100644 --- a/packages/server/src/worker.ts +++ b/packages/server/src/worker.ts @@ -64,8 +64,6 @@ export interface Env { RL_USER?: RateLimitBinding /** Anonymous pages, ARK and the auth routes (lib/limits.ts). */ RL_PAGE?: RateLimitBinding - /** KF Auth's internal API key: KF orgs for new orgs, and /api/kf/summary. Optional. */ - AUTH_INTERNAL_API_KEY?: string REPO_PREFIX?: string INTERNAL_PREFIX?: string /** @@ -182,11 +180,11 @@ function kfFor(env: Env): Kf { let kf = kfs.get(env.DB) if (!kf) { kf = createKf(primaryDb(env), { + issuerUrl: env.OIDC_ISSUER_URL, // No private network on Workers: server-to-server calls use the public URL. internalUrl: env.OIDC_ISSUER_URL, clientId: env.OIDC_CLIENT_ID, clientSecret: env.OIDC_CLIENT_SECRET, - internalApiKey: env.AUTH_INTERNAL_API_KEY, }) kfs.set(env.DB, kf) } diff --git a/packages/server/test/admin.test.ts b/packages/server/test/admin.test.ts index 9e873985..513bcacc 100644 --- a/packages/server/test/admin.test.ts +++ b/packages/server/test/admin.test.ts @@ -3,6 +3,7 @@ import { afterAll, describe, expect, it } from 'vitest' import * as schema from '../src/db/schema.js' import { cleanup } from './harness.js' import { setup } from './kf-app.js' +import { kfConsole } from './kf-console.js' afterAll(cleanup) @@ -27,18 +28,19 @@ describe('steward and KF summary routes', () => { } }) - it('answers KF Auth with an org’s collections, with the internal key only', async () => { + it('answers the KF console with an org’s collections, with its token only', async () => { const { h, call, db } = await setup() await h.collection('c') await db.update(schema.organization).set({ kfOrgId: 'kf-1' }) const url = '/api/kf/summary?kf_org_id=kf-1' expect((await call(url)).status).toBe(401) - expect((await call(url, { authorization: 'Bearer wrong' })).status).toBe(401) - expect((await call('/api/kf/summary', { authorization: 'Bearer internal-key' })).status).toBe( - 400, - ) - const res = await (await call(url, { authorization: 'Bearer internal-key' })).json() + expect((await call(url, { consoleToken: 'wrong' })).status).toBe(401) + // The old shared key, which the console still sends alongside, is no longer read. + expect((await call(url, { authorization: 'Bearer internal-key' })).status).toBe(401) + expect((await call('/api/kf/summary')).status).toBe(401) + expect((await call('/api/kf/summary', { consoleToken: 'console-token' })).status).toBe(400) + const res = await (await call(url, { consoleToken: 'console-token' })).json() expect(res.orgs).toEqual([ { id: 'org1', @@ -55,8 +57,31 @@ describe('steward and KF summary routes', () => { }, ]) const none = await call('/api/kf/summary?kf_org_id=kf-none', { - authorization: 'Bearer internal-key', + consoleToken: 'console-token', }) expect(await none.json()).toEqual({ orgs: [] }) }) + + it('checks a real console token against the registered summary URL', async () => { + let t!: Awaited> + const { h, call, db } = await setup({ + kf: async (h) => (t = await kfConsole(h.ports.db)).kf, + }) + await h.collection('c') + await db.update(schema.organization).set({ kfOrgId: 'kf-1' }) + const url = '/api/kf/summary?kf_org_id=kf-1' + const warn = console.warn + console.warn = () => {} + try { + const ok = await call(url, { consoleToken: await t.sign('http://test/api/kf/summary') }) + expect(ok.status).toBe(200) + expect((await ok.json()).orgs).toHaveLength(1) + // A token for another app's summary URL, or for another of this app's routes. + for (const htu of ['https://www.pubpub.org/api/kf/summary', 'http://test/api/kf/other']) { + expect((await call(url, { consoleToken: await t.sign(htu) })).status).toBe(401) + } + } finally { + console.warn = warn + } + }) }) diff --git a/packages/server/test/auth.test.ts b/packages/server/test/auth.test.ts index 5f8f4802..1a748272 100644 --- a/packages/server/test/auth.test.ts +++ b/packages/server/test/auth.test.ts @@ -233,7 +233,7 @@ describe('KF org links on organizations', () => { orgs: async () => [], entitled: async (_u: string, id: string) => id === 'kf-mine', defaultOrgId: async () => 'kf-default', - isInternalCall: () => false, + isConsoleCall: async () => false, } const auth = createAuth( h.ports.db, diff --git a/packages/server/test/kf-app.ts b/packages/server/test/kf-app.ts index b0d7e8a6..d144130e 100644 --- a/packages/server/test/kf-app.ts +++ b/packages/server/test/kf-app.ts @@ -6,7 +6,7 @@ import type { Principal } from '../src/api/access.js' import { createApp } from '../src/app.js' import type { Kf } from '../src/auth/kf.js' import * as schema from '../src/db/schema.js' -import { harness } from './harness.js' +import { type Harness, harness } from './harness.js' /** A KF Auth stand-in: user u1 belongs to KF org kf-1 and is a steward. */ export const fakeKf: Kf = { @@ -19,20 +19,24 @@ export const fakeKf: Kf = { : [], entitled: async (userId, id) => userId === 'u1' && id === 'kf-1', defaultOrgId: async () => null, - isInternalCall: (h) => h === 'Bearer internal-key', + // Kf's own tests check real tokens; this one, that the route asks about the registered URL. + isConsoleCall: async (token, method, url) => + token === 'console-token' && method === 'GET' && url === 'http://test/api/kf/summary', } /** - * The harness's ports behind an app that knows sessions and keys: + * The harness's ports behind an app that knows sessions and keys (and `fakeKf`, unless + * `opts.kf` makes another): * `x-test-user` signs in (session `s-`), `x-test-key: read|write|admin|scoped|org` * (or `scoped-write:`, `scoped-admin:`) makes the caller an API key of that kind. */ -export async function setup() { +export async function setup(opts: { kf?: (h: Harness) => Kf | Promise } = {}) { const h = await harness() + const kf = opts.kf ? await opts.kf(h) : fakeKf const app = createApp(() => ({ ports: h.ports, config: { appUrl: 'http://test', deployment: 'test' }, - kf: fakeKf, + kf, authenticate: async (req): Promise => { const user = req.headers.get('x-test-user') if (!user) return null @@ -62,12 +66,14 @@ export async function setup() { key?: string json?: unknown authorization?: string + consoleToken?: string } = {}, ) => { const headers = new Headers() if (init.user) headers.set('x-test-user', init.user) if (init.key) headers.set('x-test-key', init.key) if (init.authorization) headers.set('authorization', init.authorization) + if (init.consoleToken) headers.set('kf-console-token', init.consoleToken) if (init.json !== undefined) headers.set('content-type', 'application/json') return app.fetch( new Request(`http://test${path}`, { diff --git a/packages/server/test/kf-console.ts b/packages/server/test/kf-console.ts new file mode 100644 index 00000000..c9b37855 --- /dev/null +++ b/packages/server/test/kf-console.ts @@ -0,0 +1,52 @@ +/** + * KF Auth as the console's token issuer, without the network: a local EdDSA key, + * its JWKS served by a stub fetch, and a signer for `KF-Console-Token`s. + */ +import { exportJWK, generateKeyPair, SignJWT } from 'jose' + +import { createKf } from '../src/auth/kf.js' +import type { Db } from '../src/ports.js' + +export const ISSUER_URL = 'https://kf.test' +export const INTERNAL_URL = 'http://kf-internal' +export const CLIENT_ID = 'kf_underlay' + +export interface TokenOptions { + iss?: string + aud?: string + sub?: string + iat?: number + exp?: number + key?: CryptoKey +} + +export async function kfConsole(db: Db) { + const { privateKey, publicKey } = await generateKeyPair('EdDSA', { extractable: true }) + const jwk = { ...(await exportJWK(publicKey)), kid: 'k1', alg: 'EdDSA', use: 'sig' } + let jwksFetches = 0 + const fetcher = (async (input: RequestInfo | URL) => { + if (String(input) !== `${INTERNAL_URL}/api/auth/jwks`) + return new Response('{}', { status: 404 }) + jwksFetches++ + return Response.json({ keys: [jwk] }) + }) as typeof fetch + const kf = createKf( + db, + { issuerUrl: ISSUER_URL, internalUrl: INTERNAL_URL, clientId: CLIENT_ID, clientSecret: 's' }, + fetcher, + ) + /** A console token for `GET htu`, as KF signs them; `claims` and `opts` change it. */ + const sign = (htu: string, claims: Record = {}, opts: TokenOptions = {}) => { + const now = Math.floor(Date.now() / 1000) + return new SignJWT({ htm: 'GET', htu, ...claims }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'k1', typ: 'JWT' }) + .setIssuer(opts.iss ?? `${ISSUER_URL}/api/auth`) + .setAudience(opts.aud ?? CLIENT_ID) + .setSubject(opts.sub ?? 'kf-console') + .setIssuedAt(opts.iat ?? now) + .setExpirationTime(opts.exp ?? now + 60) + .setJti(crypto.randomUUID()) + .sign(opts.key ?? privateKey) + } + return { kf, sign, jwksFetches: () => jwksFetches } +} diff --git a/packages/server/test/kf.test.ts b/packages/server/test/kf.test.ts index 4dac200b..bca09a87 100644 --- a/packages/server/test/kf.test.ts +++ b/packages/server/test/kf.test.ts @@ -1,9 +1,11 @@ import { eq } from 'drizzle-orm' +import { generateKeyPair } from 'jose' import { afterAll, describe, expect, it } from 'vitest' import { createKf } from '../src/auth/kf.js' import * as schema from '../src/db/schema.js' import { cleanup, harness } from './harness.js' +import { ISSUER_URL, kfConsole } from './kf-console.js' afterAll(cleanup) @@ -56,7 +58,7 @@ describe('KF Auth client', () => { const kf = createKf( db, - { internalUrl: 'http://kf', clientId: 'c', clientSecret: 's', internalApiKey: 'ik' }, + { issuerUrl: 'http://kf', internalUrl: 'http://kf', clientId: 'c', clientSecret: 's' }, fetcher, ) expect(await kf.profile('u1')).toEqual({ name: 'Ada L', image: null, role: 'admin' }) @@ -73,27 +75,25 @@ describe('KF Auth client', () => { expect(await kf.defaultOrgId('u1')).toBe('kf-own') expect(await kf.entitled('u1', 'kf-shared')).toBe(true) expect(await kf.entitled('u1', 'kf-else')).toBe(false) - expect( - calls.filter((c) => c.includes('/internal/')).every((c) => c.endsWith('Bearer ik')), - ).toBe(true) - - expect(kf.isInternalCall('Bearer ik')).toBe(true) - expect(kf.isInternalCall('Bearer i')).toBe(false) - expect(kf.isInternalCall(undefined)).toBe(false) + // The internal API is called as the app: Basic with its client id and secret, no key. + const internal = calls.filter((c) => c.includes('/internal/')) + expect(internal.length).toBeGreaterThan(0) + expect(internal.every((c) => c.endsWith(`Basic ${btoa('c:s')}`))).toBe(true) + expect(internal.some((c) => c.includes('Bearer'))).toBe(false) }) - it('without the internal key, has no orgs and accepts no internal calls', async () => { + it('without a client secret, has no orgs; without a token, no console call', async () => { const h = await harness() const kf = createKf( h.ports.db, - { internalUrl: 'http://kf', clientId: 'c', clientSecret: 's' }, + { issuerUrl: 'http://kf', internalUrl: 'http://kf', clientId: 'c', clientSecret: '' }, (() => { throw new Error('no network') }) as typeof fetch, ) expect(await kf.orgs('u1')).toEqual([]) expect(await kf.profile('u1')).toBeNull() - expect(kf.isInternalCall('Bearer ')).toBe(false) + expect(await kf.isConsoleCall(undefined, 'GET', 'http://app/api/kf/summary')).toBe(false) }) describe('refreshing an expired token', () => { @@ -137,7 +137,7 @@ describe('KF Auth client', () => { }) as typeof fetch const kf = createKf( db, - { internalUrl: 'http://kf', clientId: 'c', clientSecret: 's' }, + { issuerUrl: 'http://kf', internalUrl: 'http://kf', clientId: 'c', clientSecret: 's' }, fetcher, ) const roles = await Promise.all(Array.from({ length: 8 }, () => kf.role('u1'))) @@ -166,7 +166,7 @@ describe('KF Auth client', () => { }) as typeof fetch const kf = createKf( db, - { internalUrl: 'http://kf', clientId: 'c', clientSecret: 's' }, + { issuerUrl: 'http://kf', internalUrl: 'http://kf', clientId: 'c', clientSecret: 's' }, fetcher, { rereadDelayMs: 0 }, ) @@ -194,7 +194,7 @@ describe('KF Auth client', () => { try { const kf = createKf( db, - { internalUrl: 'http://kf', clientId: 'c', clientSecret: 's' }, + { issuerUrl: 'http://kf', internalUrl: 'http://kf', clientId: 'c', clientSecret: 's' }, fetcher, { rereadDelayMs: 0 }, ) @@ -207,3 +207,56 @@ describe('KF Auth client', () => { }) }) }) + +describe('calls from the KF console', () => { + const summary = 'https://app.test/api/kf/summary' + + async function setup() { + const h = await harness() + const t = await kfConsole(h.ports.db) + // Refusals are logged. + const warn = console.warn + console.warn = () => {} + return { ...t, [Symbol.dispose]: () => void (console.warn = warn) } + } + + it('accepts a token for this app and this call', async () => { + using t = await setup() + expect(await t.kf.isConsoleCall(await t.sign(summary), 'GET', summary)).toBe(true) + // The registered URL carries no query; KF's htu doesn't either. + const withQuery = `${summary}?kf_org_id=x` + expect(await t.kf.isConsoleCall(await t.sign(summary), 'GET', withQuery)).toBe(true) + // The JWKS is fetched once (from the internal URL) and cached. + expect(await t.kf.isConsoleCall(await t.sign(summary), 'GET', summary)).toBe(true) + expect(t.jwksFetches()).toBe(1) + }) + + it('refuses a missing token, or one for anyone or anything else', async () => { + using t = await setup() + const now = Math.floor(Date.now() / 1000) + const stranger = await generateKeyPair('EdDSA') + const refused = { + missing: undefined, + 'not a JWT': 'Bearer internal-key', + 'another app': await t.sign(summary, {}, { aud: 'kf_pubpub' }), + 'an ID token (a user as sub)': await t.sign(summary, {}, { sub: 'user-1' }), + 'another issuer': await t.sign(summary, {}, { iss: 'https://evil.test/api/auth' }), + 'the bare auth origin as issuer': await t.sign(summary, {}, { iss: ISSUER_URL }), + expired: await t.sign(summary, {}, { iat: now - 200, exp: now - 100 }), + 'older than 60 s': await t.sign(summary, {}, { iat: now - 200, exp: now + 60 }), + 'another method': await t.sign(summary, { htm: 'POST' }), + 'another path': await t.sign('https://app.test/api/kf/webhooks'), + 'another host': await t.sign('https://evil.test/api/kf/summary'), + 'no htm/htu': await t.sign(summary, { htm: undefined, htu: undefined }), + 'a key KF does not publish': await t.sign(summary, {}, { key: stranger.privateKey }), + } + for (const [why, token] of Object.entries(refused)) { + expect(await t.kf.isConsoleCall(token, 'GET', summary), why).toBe(false) + } + // A good token with its payload changed. + const [head, body, sig] = (await t.sign(summary)).split('.') + const payload = { ...JSON.parse(atob(body!)), aud: 'kf_pubpub' } + const tampered = `${head}.${btoa(JSON.stringify(payload))}.${sig}` + expect(await t.kf.isConsoleCall(tampered, 'GET', summary)).toBe(false) + }) +}) diff --git a/packages/server/wrangler.jsonc b/packages/server/wrangler.jsonc index a594714b..beb14cb7 100644 --- a/packages/server/wrangler.jsonc +++ b/packages/server/wrangler.jsonc @@ -15,10 +15,10 @@ // LOCATION_KEY 32 bytes base64url; encrypts storage credentials // SESSION_SECRET better-auth secret // OIDC_CLIENT_SECRET kf_underlay secret of that env's KF Auth instance -// (staging: dev-auth's, which staging-auth kept) -// AUTH_INTERNAL_API_KEY optional: KF Auth's internal API key (as v1). Without -// it new orgs can't link a KF org and -// /api/kf/summary answers 401. +// (staging: dev-auth's, which staging-auth kept). +// Also the Basic credentials for KF Auth's internal +// API (the KF orgs new orgs link to). /api/kf/summary +// checks the console's KF-Console-Token instead. // ASSETS_ACCESS_KEY_ID, ASSETS_SECRET_ACCESS_KEY // optional: R2 S3 keys for ASSETS_BUCKET when there's // no PUBLIC_ASSETS binding (else the R2_* keys) diff --git a/packages/web/scripts/ssr-smoke.ts b/packages/web/scripts/ssr-smoke.ts index b9899047..11b75a83 100644 --- a/packages/web/scripts/ssr-smoke.ts +++ b/packages/web/scripts/ssr-smoke.ts @@ -70,7 +70,7 @@ try { orgs: async () => [], entitled: async () => false, defaultOrgId: async () => null, - isInternalCall: () => false, + isConsoleCall: async () => false, }, })) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be8d3896..18830223 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,6 +149,9 @@ importers: hono: specifier: ^4.12.18 version: 4.12.18 + jose: + specifier: ^6.2.3 + version: 6.2.3 devDependencies: '@cloudflare/workers-types': specifier: ^5.20261003.1