Add merchant-onboard scoped-key role for self-serve onboarding

New scoped API-key role granting read + products:write + policies:write +
licenses:write — the least-privilege credential for end-to-end catalog
setup and license issuance (create product, define policies/tiers, issue
licenses against them) without holding the master key.

The catalog write scopes already existed and were enforced on the
endpoints; only the role->scope expansion was missing. So this is a new
Role variant, not a scope-model change. grants() matches scope strings
explicitly (never by :write suffix) so the role can't widen into
settings / payment / merchant-profile / webhook writes, and every
master-only operation stays behind require_admin and so is structurally
unreachable. Existing tier caps still bound it (Creator: 5 products /
5 policies per product).

Migration 0023 rebuilds scoped_api_keys to widen the role CHECK (SQLite
can't alter a CHECK in place); the table has no FKs, so it's a plain
copy/drop/rename. Test covers the full onboard chain under the key's own
credential plus denial of master-only gates and support-only writes.
This commit is contained in:
Grant
2026-06-16 18:55:18 -05:00
parent 6b02992013
commit d5885d1d97
5 changed files with 163 additions and 4 deletions
+1
View File
@@ -6618,6 +6618,7 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; }
el('option', { value: 'read-only' }, 'Read-only — list everything; mutate nothing'),
el('option', { value: 'license-issuer' }, 'License issuer — read + issue / revoke / change-tier licenses'),
el('option', { value: 'support' }, 'Support — license issuer + cancel subs + deactivate machines'),
el('option', { value: 'merchant-onboard' }, 'Merchant onboard — read + create products / policies + issue licenses (self-serve catalog setup)'),
el('option', { value: 'full-admin' }, 'Full admin — every scope (use sparingly)'),
])
const status = el('div', { class: 'muted', style: 'margin-top:8px; font-size:12.5px; min-height:18px' }, '')