Project credentials
Project credentials
Section titled “Project credentials”After r.projects.provision(...), the result has project_id, anon_key, service_key, schema_slot. The Node entry’s credentials provider auto-saves keys to the active profile’s local project-key cache (credentials/project-keys.v1.json). Legacy projects.json files are one-way migration input only.
anon_key— read-only by default; safe in browser HTML. RLS policies apply.service_key— server-side admin. Never embed in browser code.
Neither key expires. Lease enforcement happens server-side. Server project reads such as r.projects.list(), r.projects.get(id), and r.projects.use(id) authorize with the current principal and do not require local cache membership.
Rotatable project credentials — the replacement for the derived pair
Section titled “Rotatable project credentials — the replacement for the derived pair”The anon_key/service_key above are DERIVED from the platform signing key: they never expire and cannot be revoked individually. A project credential (r402_…) is a ROW instead — named, listable, expiring, individually revocable — and several may be live per kind at once, which is exactly how you rotate with no downtime.
r.credentials carries both surfaces, and they do not overlap: r.credentials.<verb> is the gateway’s rows, r.credentials.projectKeys.<verb> is the local cache on this machine.
// Am I still on the retiring key? Only project.read is needed, so an agent can// check its own posture. retirement.deadline is ALWAYS null on purpose —// retirement is condition-gated, never a date. Read retirement.gated_on.const posture = await r.credentials.status(projectId); // { state: "legacy" | "rotatable", ... }
// Mint one. The secret is returned EXACTLY ONCE; there is no read that// returns it. Persist it before doing anything else.const cred = await r.credentials.issue(projectId, { kind: "service", name: "ci-deploy" });cred.secret; // r402_… — once, and never again
await r.credentials.list(projectId, { includeRevoked: true }); // metadata onlyawait r.credentials.rotate(projectId, cred.credential_id); // replace in one tx; new secret onceawait r.credentials.revoke(projectId, cred.credential_id, { reason: "leaked in a log" });Zero-downtime rotation is issue a second live credential → deploy it → revoke the first. rotate() collapses that into one transaction (same name, records replacement_of) and is the right call when the old secret is already compromised.
issue/rotate/revoke require owner membership on the project’s owning org PLUS a fresh step-up, and a delegate can NEVER satisfy them — a scoped agent credential must not be able to escalate itself into a permanent root.
// The one exception, and the cold-restart recovery path: an agent that lost// local state but still holds a delegate mints a SHORT-LIVED token with no// human present. No step-up, because there is nobody to prompt; it expires,// so it cannot become a durable root.const token = await r.credentials.mintToken(projectId); // { secret, expires_in, … }Never write an issue / rotate / mintToken response to a result cache, tmp file, or expansion handle — they are secret-bearing, like provision and project keys.
Deploys self-recover on a cold machine. The apikey-gated deploy legs (content upload, operation polling) read the project’s anon key from the local credential cache — and when the cache has no entry (the returning-agent case: the wallet survives, the once-issued keys did not), the SDK mints a short-lived ANON token via this same route automatically, uses it as the apikey, and memoizes it in process memory until it expires. Nothing durable is written anywhere; a client that cannot mint (no signer, no authority) falls through to the pre-existing 401, whose envelope names the recovery. So deploy.apply() on a fresh machine with a wallet needs zero extra commands. For a durable local re-key, run402 credentials issue --kind <service|anon> --name <n> --import writes the minted secret straight into the cache.
await r.projects.use(projectId); // make this the active projectconst keys = await r.projects.keys(projectId);const info = await r.projects.info(projectId);