The ADR 0050 carve-out, built generic and vendor-neutral. A refreshable-grant licence records one manager node; that node holds the refresh token encrypted at rest, access tokens are still sealed per holder, and the refresh token is never in a holder's delivery. Bounded on the three stated axes: refreshable-grant vendors only, the refresh token only, the manager node only. Anthropic's actual OAuth refresh stays a Phase-C plug-in behind a clean seam. - New at-rest crypto (secrets.SealAtRest/OpenAtRest): envelope encryption distinct from the per-holder anonymous-box seal. The refresh token is under a symmetric data key (secretbox); the data key is wrapped to the manager node's public sealing key. The database alone holds ciphertext and a wrapped key with no private half to open either — only the manager node reads it back. - Refreshable-grant adapter dispatch: anthropic is now refreshable-grant, anthropic-api-key the static-key second case. The adapter implements the Refresher seam by delegating to an injected VendorRefresher (the Phase-C plug, none shipped). static-key is untouched. The type assertion to Refresher is what gates the carve-out to refreshable-grant vendors. - Refresh lease/rotate/publish flow (Licences.Refresh): a transaction-scoped advisory lock is the single-refresher lease; the new access token comes from the vendor refresh, is sealed per holder (secrets.Seal, as Accept does) and delivered on the next push — doc 13's reseal-and-publish half, all-or-nothing. The refresh token stays put, re-encrypted at rest only if the vendor rotated it. - Manager and refresh_grant schema: consolidated into migrations/0001 and carried by a new incremental 0003 (the dual-write rule). - 17 new tests, including the four security checks: KeyFor never carries the refresh token, a static key has no manager and cannot be refreshed, the at-rest token needs the manager's key, and a refresh delivers a new sealed access token. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
20 lines
931 B
SQL
20 lines
931 B
SQL
-- A manager, and the refresh token it holds encrypted at rest.
|
|
--
|
|
-- novox/hq ADR 0050, Phase B. The consolidated schema (0001) now creates the `manager` column and
|
|
-- the `refresh_grant` table; this migration carries an existing database the same distance, so a
|
|
-- database that predates the carve-out gains exactly what a fresh one is created with.
|
|
--
|
|
-- **Guarded, so it is a no-op on a database created after 0001 was updated.** A fresh database
|
|
-- already has both, and re-adding them would fail; `if not exists` on both makes the two paths --
|
|
-- fresh and pre-existing -- end at the same schema.
|
|
|
|
alter table licence add column if not exists manager text;
|
|
|
|
create table if not exists refresh_grant (
|
|
licence text primary key references licence(name) on delete cascade,
|
|
token text not null,
|
|
wrapped_key text not null,
|
|
manager_key text not null,
|
|
updated_at timestamptz not null default now()
|
|
);
|