/**
* requireIdentity — the gate for SELF-SCOPED reads.
*
* A newly TOFU-registered user gets `['read']` by default
* (handlers/users.js:457). requireWriter passes only 'admin' or 'write', so
* gating a self-scoped handler with it locks a read-only identity out of its
* OWN data. That is what stopped a fresh identity from listing its own devices
* and copying its link invite: the daemon's rpc-audit-log recorded
* {"method":"list-my-devices","trust":"authenticated","error":"writer permission required"}
* alongside anonymous denials of the same method.
*
* requireIdentity must sit strictly between anonymous and writer:
* - anonymous -> DENIED (does not reopen the #12 default-deny surface)
* - revoked -> DENIED (#14: a revoked identity keeps its perms, so the
* revocation gate must run before/without a perms check)
* - read-only -> ALLOWED <- the regression this fixes
* - uds -> ALLOWED (local transport is trusted)
*/
import test from 'brittle'
import { requireIdentity, requireWriter } from '../lib/auth/check.js'
const PUB = 'a'.repeat(64)
// Minimal registry stand-in. isIdentityRevoked only calls registry.isRevoked;
// requireWriter additionally calls getUserByIdentity for the perms lookup.
function makeRegistry ({ perms = ['read'], revoked = false } = {}) {
return {
async isRevoked () { return revoked },
async getUserByIdentity () { return { userId: 'u1', globalPermissions: perms } }
}
}
function ctxFor (publicKey) {
return { transport: 'http', identity: publicKey ? { publicKey } : null }
}
test('read-only identity is ALLOWED (the regression)', async (t) => {
await t.execution(
requireIdentity(ctxFor(PUB), makeRegistry({ perms: ['read'] })),
'a default ["read"] user reaches its own self-scoped data'
)
})
test('the same read-only identity is REJECTED by requireWriter', async (t) => {
// Documents precisely why the gate had to change rather than the perms.
await t.exception(
requireWriter(ctxFor(PUB), makeRegistry({ perms: ['read'] })),
/writer permission required/,
'requireWriter denies a read-only user — the observed audit-log error'
)
})
test('anonymous is REJECTED — #12 default-deny is not reopened', async (t) => {
await t.exception(
requireIdentity(ctxFor(null), makeRegistry()),
/authentication required/,
'no identity means no "my" anything'
)
})
test('revoked identity is REJECTED even though its perms would pass', async (t) => {
// #14: revocation must be denied BEFORE perms are consulted, because a
// revoked identity retains its User record and globalPermissions.
await t.exception(
requireIdentity(ctxFor(PUB), makeRegistry({ perms: ['admin'], revoked: true })),
/identity revoked/,
'revocation gate still runs'
)
})
test('uds transport is trusted', async (t) => {
await t.execution(
requireIdentity({ transport: 'uds', identity: null }, makeRegistry()),
'local UDS bypasses, matching requireWriter'
)
})
test('intrinsic owner passes without a registry record', async (t) => {
await t.execution(
requireIdentity({ transport: 'http', identity: { publicKey: PUB, ownerMatched: true } }, null),
'ownerMatched short-circuits, matching requireWriter'
)
})
test('missing registry is a 500, not a silent allow', async (t) => {
await t.exception(
requireIdentity(ctxFor(PUB), null),
/identity registry not available/,
'cannot verify revocation => refuse rather than pass'
)
})