/** The redacted, wire-safe view of a pairing token, no hash, no secret. */ export interface PublicPairingToken { readonly id: string; readonly name: string; readonly createdAt: number; readonly lastSeenAt?: number | undefined; } /** The result of minting a token, the ONLY time the plaintext secret is exposed. */ export interface MintedPairingToken { readonly id: string; readonly name: string; /** The plaintext token, returned once, never stored, never listed again. */ readonly token: string; readonly createdAt: number; } /** What a successful authenticate resolves to: the identity behind the token. */ export interface AuthenticatedPairingToken { readonly id: string; readonly name: string; /** Stable per-token principal id (`pairing:`), so step-up keys per token. */ readonly principalId: string; } /** The `pairing:` principal id a pairing-token request authenticates as. */ export declare function pairingPrincipalId(tokenId: string): string; /** * A pairing was refused because the paired-node cap is full. * * Carries the setting name, the cap and the live count so the refusal a caller * renders says what to change and what the current state is, rather than a bare * "failed". `code` is what the control-plane verbs map to their wire error. */ export declare class PairingLimitReachedError extends Error { readonly maxPaired: number; readonly pairedCount: number; readonly code = "DEVICE_NODES_MAX_PAIRED"; readonly setting = "device.nodes.maxPaired"; constructor(maxPaired: number, pairedCount: number); } /** How the store learns the live cap. Absent ⇒ unbounded, exactly as before. */ export interface PairingTokenManagerOptions { /** * Reads `device.nodes.maxPaired` at mint time. A function, not a number, so a * cap change takes effect on the next pairing without re-constructing the * store, and so this module never has to depend on ConfigManager. */ readonly maxPaired?: (() => number | undefined) | undefined; } export declare class PairingTokenManager { private readonly filePath; private snapshot; /** hash -> record, for O(1) synchronous auth lookup. */ private index; private lastSeenFlushAt; private readonly readMaxPaired; constructor(filePath: string, options?: PairingTokenManagerOptions); /** * The configured cap, or null when there is none / it is unusable. * * A non-positive or non-finite value is treated as "no cap" rather than "no * device may ever pair": a broken setting must not lock the owner out of * their own daemon. */ private currentCap; /** How many paired device nodes exist right now, one record per node. */ pairedCount(): number; /** * Whether `name` is a node that is already paired (case/whitespace-insensitive). * * The pairing exchange carries a name and nothing else, so the name IS the * node's identity here. A device re-pairing under the name it already holds is * the same node, not an additional one. */ private findByName; private load; private reindex; private flush; /** * Mint a new named per-device token. The plaintext is returned only here. * * Bounded by `device.nodes.maxPaired` when the host supplied a reader for it. * The rules, all of which are exercised by tests: * * - Below the cap nothing changes at all, same append, same result. * - At the cap, a NEW node is refused with {@link PairingLimitReachedError}, * which names the setting, the cap and the live count. * - At the cap, a node that is ALREADY paired (same name) is never refused: it * supersedes its own record, so re-pairing a phone that is already in the * list keeps working and the count does not creep past the cap. Nobody * else's pairing is touched. * - Lowering the cap below the current count unpairs NO ONE: existing tokens * keep authenticating; only the next NEW pairing is refused, until enough * devices are unpaired to fit under the cap again. */ mint(input: { readonly name: string; }): MintedPairingToken; private mintInternal; /** * A client currently on the legacy shared token moves to its own per-device * token. The "one receipt" is this single return; it does NOT revoke the * shared token (that is a separate, explicit step). * * Deliberately EXEMPT from `device.nodes.maxPaired`: this device is already * using this daemon on the shared token. Refusing it would strand a working * device on a credential it is being asked to give up, which is a worse * outcome than being one over a cap that no longer describes reality. A new * device pairing for the first time is still bounded. */ mintForMigration(input: { readonly name: string; }): MintedPairingToken; /** * Authenticate a presented token by hashing it and looking the hash up. * Immediate revocation: a revoked (deleted) token misses here and the caller * treats the request as unauthorized. Stamps last-seen (throttled to disk). */ authenticate(token: string): AuthenticatedPairingToken | null; /** Every per-pairing token, redacted (name / created / last-seen), never the secret. */ list(): PublicPairingToken[]; /** Rename a token's user-visible label. False when the id is unknown. */ rename(id: string, name: string): boolean; /** * Revoke a single device's token. Delete means delete: the record is dropped * and the token fails the very next authenticate. False when already absent. */ revoke(id: string): boolean; /** Whether the legacy single shared token has been revoked here. */ isLegacyRevoked(): boolean; /** Revoke the legacy single shared token; it stops authenticating immediately. */ revokeLegacyShared(): void; } //# sourceMappingURL=pairing-token-store.d.ts.map