feat(control-plane): POST /auth/login mints device:enroll bearer (B1)
Unblocks the phone-enrollment track: an operator-password login mints a short-lived device:enroll capability token that POST /device/enroll requires. Constant-time (SHA-256 fixed-length) compare, per-client rate-limit, fail-closed when unset. 260 tests pass.
This commit is contained in:
@@ -66,3 +66,52 @@ describe('T1 loadEnv (INV9 fail-fast)', () => {
|
||||
expect(() => loadEnv({ ...base(), CAPABILITY_SIGN_PUBKEY_B64: short })).toThrow(/32 bytes/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('B1 operator-login env (set-together-or-none, fail-closed)', () => {
|
||||
const OPERATOR_PASSWORD = 'operator-secret-abcdef123456'
|
||||
const ACCOUNT = '11111111-1111-4111-8111-111111111111'
|
||||
const PRIVKEY = bytesToBase64(new Uint8Array(48).fill(3)) // shape-valid base64; import validated at boot
|
||||
|
||||
test('none set → login fields undefined (feature off, route fail-closed at runtime)', () => {
|
||||
const env = loadEnv(base())
|
||||
expect(env.operatorPassword).toBeUndefined()
|
||||
expect(env.operatorAccountId).toBeUndefined()
|
||||
expect(env.capabilitySignPrivkey).toBeUndefined()
|
||||
})
|
||||
|
||||
test('all three set → parsed together', () => {
|
||||
const env = loadEnv({
|
||||
...base(),
|
||||
OPERATOR_PASSWORD,
|
||||
OPERATOR_ACCOUNT_ID: ACCOUNT,
|
||||
CAPABILITY_SIGN_PRIVKEY_B64: PRIVKEY,
|
||||
})
|
||||
expect(env.operatorPassword).toBe(OPERATOR_PASSWORD)
|
||||
expect(env.operatorAccountId).toBe(ACCOUNT)
|
||||
expect(env.capabilitySignPrivkey?.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('password without account/key → fail-fast (no silent half-open)', () => {
|
||||
expect(() => loadEnv({ ...base(), OPERATOR_PASSWORD })).toThrow(/OPERATOR_ACCOUNT_ID|CAPABILITY_SIGN_PRIVKEY_B64/)
|
||||
})
|
||||
|
||||
test('too-short operator password rejected (16–512 charset rule)', () => {
|
||||
expect(() =>
|
||||
loadEnv({ ...base(), OPERATOR_PASSWORD: 'short', OPERATOR_ACCOUNT_ID: ACCOUNT, CAPABILITY_SIGN_PRIVKEY_B64: PRIVKEY }),
|
||||
).toThrow(/OPERATOR_PASSWORD/)
|
||||
})
|
||||
|
||||
test('non-UUID operator account rejected', () => {
|
||||
expect(() =>
|
||||
loadEnv({ ...base(), OPERATOR_PASSWORD, OPERATOR_ACCOUNT_ID: 'not-a-uuid', CAPABILITY_SIGN_PRIVKEY_B64: PRIVKEY }),
|
||||
).toThrow(/OPERATOR_ACCOUNT_ID/)
|
||||
})
|
||||
|
||||
test('never echoes the operator secret on a partial-config failure (INV9)', () => {
|
||||
try {
|
||||
loadEnv({ ...base(), OPERATOR_PASSWORD })
|
||||
} catch (e) {
|
||||
expect(e instanceof Error ? e.message : '').not.toContain(OPERATOR_PASSWORD)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user