perm sets + cli fixes
37 unmodified lines38394041424344454623 unmodified lines37 unmodified lines "e2e:harness": "docker compose -p wispplace-monorepo-e2e -f docker-compose.dev.yml -f docker-compose.e2e.yml --profile dev up --build --abort-on-container-exit --exit-code-from e2e-runner e2e-runner", "e2e:harness:down": "docker compose -p wispplace-monorepo-e2e -f docker-compose.dev.yml -f docker-compose.e2e.yml --profile dev down -v --remove-orphans", "codegen": "./scripts/codegen.sh", "download:aturi": "bun run scripts/download-place-wisp-fs.ts", "publish:permission-sets": "bun run scripts/publish-permission-sets.ts", "publish:cli": "cd cli && bun run build && npm publish && cd ../packages/create-wisp && npm publish" }, "trustedDependencies": [ "@parcel/watcher",23 unmodified lines1233456744 unmodified lines{ "name": "wispctl", "version": "1.3.0", "version": "1.3.1", "main": "./dist/index.js", "devDependencies": { "@atproto/api": "^0.20.28", "@atproto/identity": "^0.5.6",44 unmodified lines44 unmodified lines45464748495049505152535455565657585959 unmodified lines44 unmodified lines} satisfies Record<string, Author>
const posts: Post[] = [ { slug: 'wispctl-1-3', title: 'wispctl 1.3 ', slug: 'wispctl-1-3-1', title: 'wispctl 1.3.1', description: 'Making wispctl\'s auth more ergonomic to use', date: '2026-08-26', author: authors.nekomimi, tags: ['release', 'cli'], draft: true, }, { slug: 'hello-world', title: 'wisp.place has a blog now',59 unmodified lines37 unmodified lines383940414243444546474849505152535455565758381 unmodified linesExpand all44044144244343344444544644744844945045145245345415 unmodified lines470471472473474475476477478153 unmodified linesExpand all37 unmodified lines addedAt: number lastUsedAt: number /** When `handle` was last confirmed against the network. */ handleCheckedAt?: number /** * Which OAuth scope strategy minted this session. * * A loopback client declares its scopes inside the `client_id`, so the two * strategies are two different client identities. Restoring a session has to * rebuild the same one or the token refresh is rejected. */ oauthScope?: OAuthScopeStrategy}
/** `sets` requests `include:place.wisp.*`; `granular` requests their expansion. */export type OAuthScopeStrategy = 'sets' | 'granular'
// ---------------------------------------------------------------------------// Keychain access// ---------------------------------------------------------------------------
381 unmodified linesExpand all */export function upsertAccount( kv: KvAdapter, did: string, updates: { handle?: string; method?: AuthMethod; pdsUrl?: string; handleChecked?: boolean } = {}, updates: { handle?: string method?: AuthMethod pdsUrl?: string handleChecked?: boolean oauthScope?: OAuthScopeStrategy } = {},): StoredAccount { const now = Date.now() const existing = readAccount(kv, did) const handle = updates.handle ? normalizeHandle(updates.handle) : existing?.handle15 unmodified lines pdsUrl: updates.pdsUrl ?? existing?.pdsUrl, addedAt: existing?.addedAt ?? now, lastUsedAt: now, handleCheckedAt: updates.handleChecked ? now : handle !== existing?.handle ? undefined : existing?.handleCheckedAt, oauthScope: updates.oauthScope ?? existing?.oauthScope, } writeAccount(kv, account) return account}153 unmodified linesExpand all11 unmodified lines12131415161718192011 unmodified lines3233343536373839404 unmodified lines4546474847495051525354555657160 unmodified linesExpand all2182192202212172222232242252262272192202282292302222232242312322332342352367 unmodified lines2442452462472482492502512522532542402552562572582432592602612622632 unmodified lines26626726826925427027127227327427527627727827928028128228322 unmodified lines3063073083092903103113123133143153163173183193203213223233243257 unmodified lines33333433533633733833934034134234331134434534634734869 unmodified lines4184194204214223893903913923934234244254264274283953963973983994004014024034294304314324334344354364374384054064074084094104114124134144154164394404414424434444454464474484494504514184194204524534544554224234244254564574584594604274284294304314614624634644654664334344354364374674684694704714724394404414424434444454464474484494504514524734744754764774784794804814824834844854864874544554564574884894904914924934944954964974984995005015025035045055065075085095105115125135145154594604614624635164655175185195205215225235245255264674684694704714724734744754764774784794804815275285295305314834844854864874884894904915325335345354934944954965365375385395405415425435444985455465475485495505515525535545005015025555565575585595605615625635645655045055665675685695705715725735745755765775785795075085805815825835845855865125875885895905918 unmodified lines600601602603529604605606607608194 unmodified linesExpand all11 unmodified linesimport { log } from '@clack/prompts'import { serve as honoNodeServe } from '@hono/node-server'import { resolvePdsFromHandle } from '@wispplace/atproto-utils'import { isBun } from '@wispplace/bun-firehose'import { describeCapability, missingCapabilities, wispCliRequiredCapabilities } from '@wispplace/constants'import { Hono } from 'hono'import open from 'open'import { type AuthMethod,11 unmodified lines kvSet, listAccounts, listDirsForDid, normalizeHandle, type OAuthScopeStrategy, openAccountStore, probeKeychain, readAccount, resolveAccountForDir,4 unmodified lines setStoredAppPassword, setStoredOAuthSession, upsertAccount,} from './account-store.ts'import { WISP_OAUTH_SCOPE } from './wisp-service'import { WISP_OAUTH_LEGACY_SCOPE, WISP_OAUTH_SCOPE } from './wisp-service'
/** Public resolvers used only when the system nameservers fail to answer. */const FALLBACK_NAMESERVERS = ['1.1.1.1', '8.8.8.8']
const LOOPBACK_PORT = 4000const LOOPBACK_HOST = '127.0.0.1'
interface LoopbackPortSelection {160 unmodified linesExpand all }}
/** * Return the subset of `WISP_OAUTH_SCOPE` not present in a token's granted scope. * Return the capabilities the CLI needs that a token's granted scope lacks. * * The granted scope is always the *expanded* granular form — an authorization * server rewrites `include:place.wisp.authSites` into the `repo:`/`rpc:` values * the permission set contains before minting the token — so this compares * meaning rather than scope strings. Reused after a fresh OAuth callback (to * Reused both after a fresh OAuth callback (to warn) and at session restore (to * transparently re-auth when a stored token predates an expanded scope set). * detect a server that ignored the permission sets) and at session restore (to * transparently re-auth when a stored token predates a widened scope set). */function missingScopesFor(grantedScope: string | undefined): string[] { const granted = new Set((grantedScope || '').split(/\s+/).filter(Boolean)) return WISP_OAUTH_SCOPE.split(' ').filter((s) => !granted.has(decodeURIComponent(s)))function missingScopesFor(grantedScope: string | undefined) { return missingCapabilities(grantedScope, wispCliRequiredCapabilities())}
/** * Authenticate with AT Protocol using OAuth loopback flow7 unmodified lines const keychainProbe = await probeKeychain() const useKeychain = keychainProbe.available const stateStore = createStateStore(kv) const sessionStore = createSessionStore(kv, useKeychain) // A loopback client declares its scopes inside the `client_id`, so declaring // both strategies at once would double the length of every authorization URL // the user sees. Build one client per strategy instead and only reach for the // granular one if the permission sets are refused. const scopeFor = (strategy: OAuthScopeStrategy) => strategy === 'granular' ? WISP_OAUTH_LEGACY_SCOPE : WISP_OAUTH_SCOPE
const createOAuthClient = (redirectUri: string): NodeOAuthClient => { const createOAuthClient = (redirectUri: string, strategy: OAuthScopeStrategy = 'sets'): NodeOAuthClient => { const scope = scopeFor(strategy) const clientIdParams = new URLSearchParams() clientIdParams.append('redirect_uri', redirectUri) clientIdParams.append('scope', WISP_OAUTH_SCOPE) clientIdParams.append('scope', scope) return new NodeOAuthClient({ clientMetadata: { client_id: `http://localhost?${clientIdParams.toString()}`, client_name: 'Wisp CLI',2 unmodified lines grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], application_type: 'web', token_endpoint_auth_method: 'none', scope: WISP_OAUTH_SCOPE, scope, dpop_bound_access_tokens: false, }, stateStore, sessionStore, requestLock: requestLocalLock, // A handle with no /.well-known/atproto-did has DNS as its only route, // so one flaky system resolver is enough to fail a login outright. // These are consulted only after the system resolver gives nothing. fallbackNameservers: FALLBACK_NAMESERVERS, }) }
// Work out which account this invocation is for. An explicit handle or DID22 unmodified lines }
// Record the account against this directory so the next bare command here // resolves without a handle, and remember the handle for display. const finish = async (agent: Agent, did: string, method: AuthMethod): Promise<{ agent: Agent; did: string }> => { const finish = async ( agent: Agent, did: string, method: AuthMethod, oauthScope?: OAuthScopeStrategy, ): Promise<{ agent: Agent; did: string }> => { const explicitHandle = handle && !handle.startsWith('did:') ? normalizeHandle(handle) : undefined upsertAccount(kv, did, { handle: explicitHandle, handleChecked: explicitHandle ? true : undefined, method, oauthScope, }) setDirDid(kv, did) await backfillHandle(kv, did) return { agent, did }7 unmodified lines const label = account?.handle ?? accountDid
const tryOAuth = async (): Promise<{ agent: Agent; did: string } | undefined> => { try { // The stored session belongs to whichever client_id minted it, and a // loopback client_id contains its scopes — so restore through the same // strategy or the refresh is rejected as a different client. const restoreClient = account?.oauthScope && account.oauthScope !== 'sets' ? createOAuthClient(redirectUri, account.oauthScope) : client const session = await client.restore(accountDid) const session = await restoreClient.restore(accountDid) if (!session) return undefined // A stored token may predate an expanded scope set (e.g. the // privateSite/domain.verify scopes). Re-auth transparently so the // subsequent XRPC call doesn't fail with a scope error.69 unmodified lines
// Start new OAuth flow emitStatus(options, `Starting OAuth flow for ${loginIdentifier}...`)
const createCallbackServer = (): Promise<{ params: URLSearchParams }> => const callbackPromise = new Promise<{ params: URLSearchParams }>((resolve, reject) => { const app = new Hono() let serverHandle: { close: () => void } | null = null let timeoutHandle: ReturnType<typeof setTimeout> | undefined let settled = false new Promise<{ params: URLSearchParams }>((resolve, reject) => { const app = new Hono() let serverHandle: { close: () => void } | null = null let timeoutHandle: ReturnType<typeof setTimeout> | undefined let settled = false
const successHtml = ` <html> <head> <title>Wisp CLI - Authentication Successful</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <style> :root { color-scheme: light dark; } const successHtml = ` <html> <head> <title>Wisp CLI - Authentication Successful</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <style> :root { color-scheme: light dark; }
body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; font-family: system-ui, -apple-system, Segoe UI, sans-serif; background: #f4f5f7; color: #111827; text-align: center; padding: 24px; } body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; font-family: system-ui, -apple-system, Segoe UI, sans-serif; background: #f4f5f7; color: #111827; text-align: center; padding: 24px; }
.content { max-width: 560px; } .content { max-width: 560px; }
h1 { margin: 0 0 10px; font-size: 22px; } h1 { margin: 0 0 10px; font-size: 22px; }
p { margin: 0; color: #4b5563; line-height: 1.5; } p { margin: 0; color: #4b5563; line-height: 1.5; }
@media (prefers-color-scheme: dark) { body { background: #1e1e1e; color: #f3f4f6; } @media (prefers-color-scheme: dark) { body { background: #1e1e1e; color: #f3f4f6; }
p { color: #d1d5db; } } </style> </head> <body> <div class="content"> <h1>Authentication Successful</h1> <p>You can close this window and return to the CLI.</p> </div> </body> </html> ` p { color: #d1d5db; } } </style> </head> <body> <div class="content"> <h1>Authentication Successful</h1> <p>You can close this window and return to the CLI.</p> </div> </body> </html> `
app.get('/oauth/callback', (c) => { const params = new URLSearchParams(c.req.url.split('?')[1] || '') if (timeoutHandle) { clearTimeout(timeoutHandle) app.get('/oauth/callback', (c) => { const params = new URLSearchParams(c.req.url.split('?')[1] || '') if (timeoutHandle) { clearTimeout(timeoutHandle) } settled = true setTimeout(() => serverHandle?.close(), 100) resolve({ params }) return c.html(successHtml) })
app.all('*', (c) => c.text('Not found', 404))
if (isBun) { const bunServer = Bun.serve({ port: oauthPort, hostname: LOOPBACK_HOST, fetch: app.fetch, }) serverHandle = { close: () => bunServer.stop() } } else { const nodeServer = honoNodeServe({ fetch: app.fetch, port: oauthPort, hostname: LOOPBACK_HOST, }) serverHandle = { close: () => nodeServer.close() } } settled = true setTimeout(() => serverHandle?.close(), 100) resolve({ params }) return c.html(successHtml) })
app.all('*', (c) => c.text('Not found', 404)) timeoutHandle = setTimeout( () => { if (settled) return settled = true serverHandle?.close() reject(new Error('OAuth callback timeout')) }, 5 * 60 * 1000, )
if (isBun) { const bunServer = Bun.serve({ port: oauthPort, hostname: LOOPBACK_HOST, fetch: app.fetch, }) serverHandle = { close: () => bunServer.stop() } } else { const nodeServer = honoNodeServe({ fetch: app.fetch, port: oauthPort, hostname: LOOPBACK_HOST, }) serverHandle = { close: () => nodeServer.close() } } if (typeof (timeoutHandle as { unref?: () => void }).unref === 'function') { ;(timeoutHandle as { unref: () => void }).unref() } })
timeoutHandle = setTimeout( () => { if (settled) return settled = true serverHandle?.close() reject(new Error('OAuth callback timeout')) }, 5 * 60 * 1000, ) // `loginIdentifier` is checked above; capture it so the closures below keep // the narrowed type. const identifier = loginIdentifier
if (typeof (timeoutHandle as { unref?: () => void }).unref === 'function') { ;(timeoutHandle as { unref: () => void }).unref() } }) const completeFlow = async (authUrl: URL) => { const callbackPromise = createCallbackServer() emitStatus(options, 'Opening browser for authentication...') emitStatus(options, `If browser does not open, visit: ${authUrl}`) await open(authUrl.toString()) const { params } = await callbackPromise return await client.callback(params) }
const authUrl = await client.authorize(loginIdentifier, { scope: WISP_OAUTH_SCOPE }) // Prefer the published permission sets. A server that cannot resolve them // rejects the pushed authorization request outright, so fall straight back // to the granular expansion. let strategy: OAuthScopeStrategy = 'sets' const useGranular = async (): Promise<URL> => { strategy = 'granular' client = createOAuthClient(redirectUri, 'granular') return await client.authorize(identifier, { scope: scopeFor('granular') }) }
emitStatus(options, 'Opening browser for authentication...') emitStatus(options, `If browser does not open, visit: ${authUrl}`) await open(authUrl.toString()) let authUrl: URL try { authUrl = await client.authorize(identifier, { scope: scopeFor('sets') }) } catch (err) { emitWarning( options, `Authorization server rejected the wisp.place permission sets (${err instanceof Error ? err.message : String(err)}). Falling back to granular scopes.`, ) authUrl = await useGranular() }
const { params } = await callbackPromise const { session } = await client.callback(params) let { session } = await completeFlow(authUrl) let tokenInfo = await session.getTokenInfo(false) let missingScopes = missingScopesFor(tokenInfo.scope)
// An older authorization server accepts the request but silently drops the // `include:` values it does not understand, leaving a session that can not // write anything. That only shows up in the granted scope. if (missingScopes.length > 0 && strategy === 'sets') { emitWarning( options, 'Authorization server ignored the wisp.place permission sets. Retrying with granular scopes...', ) await sessionStore.del(session.did) ;({ session } = await completeFlow(await useGranular())) const tokenInfo = await session.getTokenInfo(false) const missingScopes = missingScopesFor(tokenInfo.scope) tokenInfo = await session.getTokenInfo(false) missingScopes = missingScopesFor(tokenInfo.scope) }
if (missingScopes.length > 0) { emitWarning( options, `OAuth token is missing ${missingScopes.length} requested scope(s). First missing: ${missingScopes[0]}`, `OAuth token is missing ${missingScopes.length} requested permission(s). First missing: ${describeCapability(missingScopes[0])}`, ) }
const agent = new Agent(session)8 unmodified lines } else { kvSet(kv, dirOAuthPortKey, String(oauthPort)) }
const result = await finish(agent, did, 'oauth') const result = await finish(agent, did, 'oauth', strategy)
emitStatus(options, `Authenticated as ${did}`)
return result194 unmodified linesExpand all1234545678910678121314151617181920212223242526272829910111223 unmodified lines36373839616263404165666742434445464748697071727349505152535455import { buildWispScopes, WISP_CLI_PERMISSION_SETS, WISP_HOSTING_LXMS } from '@wispplace/constants'
export const DEFAULT_WISP_SERVICE_DID = 'did:web:wisp.place'export const WISP_PROXY_SERVICE_ID = 'wisp_xrpc'
export const WISP_OAUTH_BASE_SCOPES = [ 'atproto', 'repo:place.wisp.fs', 'repo:place.wisp.subfs', 'repo:place.wisp.settings', 'blob:*/*',] as const/** XRPC methods the CLI reaches through the hosting service proxy. */export const WISP_SERVICE_LXMS = WISP_HOSTING_LXMS
export const WISP_SERVICE_LXMS = [ 'place.wisp.v2.domain.addSite', 'place.wisp.v2.domain.claim', 'place.wisp.v2.domain.claimSubdomain', 'place.wisp.v2.domain.delete', 'place.wisp.v2.domain.getList', 'place.wisp.v2.domain.getStatus', 'place.wisp.v2.domain.verify', 'place.wisp.v2.privateSite.create', 'place.wisp.v2.privateSite.createShare', 'place.wisp.v2.privateSite.delete', 'place.wisp.v2.privateSite.list', 'place.wisp.v2.privateSite.listShares', 'place.wisp.v2.privateSite.revokeShare', 'place.wisp.v2.site.delete', 'place.wisp.v2.site.getList',] as const
function isDid(value: string): value is `did:${string}:${string}` { if (!value.startsWith('did:')) { return false }23 unmodified lines
return value}
function buildRpcScope(aud: string, lxm: string): string { return `rpc:${lxm}?aud=${aud}`}const WISP_CLI_SCOPES = buildWispScopes([...WISP_CLI_PERMISSION_SETS])
export function buildWispRpcScopes(): string[] { return WISP_SERVICE_LXMS.map((lxm) => buildRpcScope('*', lxm))}/** * Scope the CLI asks for: one `include:` per published permission set, so the * consent screen shows "Manage your wisp.place sites" instead of a wall of * `repo:`/`rpc:` values. */export const WISP_OAUTH_SCOPE = WISP_CLI_SCOPES.preferred
export function buildWispOAuthScopes(): string[] { return [...WISP_OAUTH_BASE_SCOPES, ...buildWispRpcScopes()]}
export const WISP_OAUTH_SCOPE = buildWispOAuthScopes().join(' ')/** * The granular expansion of those sets. Authorization servers that support * granular scopes but not permission sets ignore an unknown `include:` value * and hand back a session with no permissions at all, so the login flow falls * back to this. */export const WISP_OAUTH_LEGACY_SCOPE = WISP_CLI_SCOPES.legacy1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556{ "lexicon": 1, "id": "place.wisp.authFullAccess", "defs": { "main": { "type": "permission-set", "title": "Full wisp.place access", "detail": "Full control of your sites, domains, and webhooks.", "permissions": [ { "type": "permission", "resource": "repo", "collection": [ "place.wisp.fs", "place.wisp.subfs", "place.wisp.settings", "place.wisp.domain", "place.wisp.v2.wh" ], "action": [ "create", "update", "delete" ] }, { "type": "permission", "resource": "rpc", "aud": "*", "lxm": [ "place.wisp.v2.domain.addSite", "place.wisp.v2.domain.claim", "place.wisp.v2.domain.claimSubdomain", "place.wisp.v2.domain.delete", "place.wisp.v2.domain.getList", "place.wisp.v2.domain.getStatus", "place.wisp.v2.domain.verify", "place.wisp.v2.privateSite.create", "place.wisp.v2.privateSite.createShare", "place.wisp.v2.privateSite.delete", "place.wisp.v2.privateSite.list", "place.wisp.v2.privateSite.listShares", "place.wisp.v2.privateSite.revokeShare", "place.wisp.v2.site.delete", "place.wisp.v2.site.getDomains", "place.wisp.v2.site.getList", "place.wisp.v2.secret.create", "place.wisp.v2.secret.delete", "place.wisp.v2.secret.list", "place.wisp.v2.secret.rotate" ] } ] } }}123456789101112131415161718192021222324252627282930313233343536{ "lexicon": 1, "id": "place.wisp.authHosting", "defs": { "main": { "type": "permission-set", "title": "Use wisp.place hosting features", "detail": "Manage your sites, custom domains, and private site links.", "permissions": [ { "type": "permission", "resource": "rpc", "aud": "*", "lxm": [ "place.wisp.v2.domain.addSite", "place.wisp.v2.domain.claim", "place.wisp.v2.domain.claimSubdomain", "place.wisp.v2.domain.delete", "place.wisp.v2.domain.getList", "place.wisp.v2.domain.getStatus", "place.wisp.v2.domain.verify", "place.wisp.v2.privateSite.create", "place.wisp.v2.privateSite.createShare", "place.wisp.v2.privateSite.delete", "place.wisp.v2.privateSite.list", "place.wisp.v2.privateSite.listShares", "place.wisp.v2.privateSite.revokeShare", "place.wisp.v2.site.delete", "place.wisp.v2.site.getDomains", "place.wisp.v2.site.getList" ] } ] } }}12345678910111213141516171819202122232425262728{ "lexicon": 1, "id": "place.wisp.authSites", "defs": { "main": { "type": "permission-set", "title": "Manage your wisp.place sites", "detail": "Create, update, and delete your site files and settings.", "permissions": [ { "type": "permission", "resource": "repo", "collection": [ "place.wisp.fs", "place.wisp.subfs", "place.wisp.settings", "place.wisp.domain" ], "action": [ "create", "update", "delete" ] } ] } }}12345678910111213141516171819202122232425{ "lexicon": 1, "id": "place.wisp.authWebhooks", "defs": { "main": { "type": "permission-set", "title": "Manage your wisp.place webhooks", "detail": "Create, update, and delete your webhook subscriptions.", "permissions": [ { "type": "permission", "resource": "repo", "collection": [ "place.wisp.v2.wh" ], "action": [ "create", "update", "delete" ] } ] } }}123345671 unmodified line910111213131415{ "name": "create-wisp", "version": "1.1.5", "version": "1.1.6", "description": "CLI for wisp.place - deploy static sites to the AT Protocol", "type": "module", "bin": { "create-wisp": "bin.js"1 unmodified line "files": [ "bin.js" ], "dependencies": { "wispctl": "^1.3.0" "wispctl": "^1.3.1" }}214 unmodified linesExpand all2152162172182192192202212222232242252252262272282293 unmodified lines23323423523623723723823924024124224324424524624624724824925021 unmodified lines214 unmodified linesExpand all expect(storageGetWithMetadataKeys).not.toContain(`${DID}/${RKEY}/modelapp/index.html`) expect(storageGetWithMetadataKeys).toContain(`${DID}/${RKEY}/modelapp.html`) })
test('serves fingerprinted javascript assets with immutable cache headers', async () => { test('keeps fingerprinted javascript assets on the standard cache policy', async () => { storeFile('assets/typescript-COf36OFD.js', 'export const ts = true', 'text/javascript')
const response = await serveFileInternal(DID, RKEY, 'assets/typescript-COf36OFD.js')
expect(response.status).toBe(200) expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000, immutable') expect(response.headers.get('Cache-Control')).toBe('public, max-age=600') })
test('keeps non-fingerprinted javascript assets on the standard cache policy', async () => { storeFile('assets/app.js', 'export const app = true', 'text/javascript')3 unmodified lines expect(response.status).toBe(200) expect(response.headers.get('Cache-Control')).toBe('public, max-age=600') })
test('returns immutable cache headers on fingerprinted asset 304 responses', async () => { test('returns standard cache headers on fingerprinted asset 304 responses', async () => { storeFile('assets/typescript-COf36OFD.js', 'export const ts = true', 'text/javascript')
const response = await serveFileInternal(DID, RKEY, 'assets/typescript-COf36OFD.js', null, { 'if-none-match': '"test-checksum"', })
expect(response.status).toBe(304) expect(response.headers.get('ETag')).toBe('"test-checksum"') expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000, immutable') expect(response.headers.get('Cache-Control')).toBe('public, max-age=600') })
test('rejects absolute 200 rewrite targets without proxying them', async () => { storeFile(21 unmodified lines