-- Phase 3 (v1.1.9): group-owned, environment-scoped secrets. -- -- Until now `secrets` was strictly per-app: PK `(app_id, name)`, one -- envelope per app. Phase 3 makes secrets inheritable down the group tree -- (blueprint §11 bullet 3 / docs/design §3), exactly like `vars` (0048): -- a secret may be owned by an app OR an ancestor group, and a descendant -- app resolves the nearest one, environment-filtered. -- -- Reshape (mirrors `vars`): -- * `group_id` — nullable FK→groups, CASCADE (a deleted group drops its -- secrets, same as its vars). -- * `app_id` — made nullable (was NOT NULL); the exactly-one CHECK now -- enforces "owned by an app XOR a group". -- * `environment_scope` — `'*'` (env-agnostic) or a concrete env name; -- the resolver filters on it. Existing rows backfill to `'*'`, so every -- current app secret stays env-agnostic and resolves unchanged. -- * PK `(app_id, name)` → two PARTIAL unique indexes, one per owner, both -- keyed `(owner, environment_scope, name)`. -- -- CRYPTO INVARIANT (audit 2026-06-11 H-D1): the v1 AAD is -- `secret:{app_id}:{name}` for app secrets and `secret:group:{group_id}:{name}` -- for group secrets — it does NOT include `environment_scope`. Adding the -- column therefore leaves every existing ciphertext decryptable byte-for-byte; -- the app-owner AAD is unchanged and the group namespace is disjoint. ALTER TABLE secrets ADD COLUMN group_id UUID REFERENCES groups(id) ON DELETE CASCADE, ADD COLUMN environment_scope TEXT NOT NULL DEFAULT '*'; -- Drop the old composite PK first: `app_id` cannot lose NOT NULL while it -- is a primary-key column. ALTER TABLE secrets DROP CONSTRAINT secrets_pkey; ALTER TABLE secrets ALTER COLUMN app_id DROP NOT NULL; ALTER TABLE secrets ADD CONSTRAINT secrets_owner_exactly_one CHECK ((group_id IS NULL) <> (app_id IS NULL)); -- One secret per (owner, env, name). Partial so each owner column only -- constrains its own rows; the resolver and every upsert restate the -- predicate as the ON CONFLICT arbiter. CREATE UNIQUE INDEX secrets_app_uidx ON secrets (app_id, environment_scope, name) WHERE app_id IS NOT NULL; CREATE UNIQUE INDEX secrets_group_uidx ON secrets (group_id, environment_scope, name) WHERE group_id IS NOT NULL; -- Owner lookup index for the group side (the app side keeps idx_secrets_app). CREATE INDEX idx_secrets_group ON secrets (group_id) WHERE group_id IS NOT NULL;