Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"agent": "04/20",
|
||||
"title": "Unified contract: plan features + staff roles + legacy",
|
||||
"status": "design_only",
|
||||
"version": "1.0.0",
|
||||
"coordinates_with": [
|
||||
"docs/admin-roles-support/01-ux-research.md",
|
||||
"docs/admin-roles-support/02-current-inventory.md",
|
||||
"docs/admin-roles-support/03-roles-matrix.md",
|
||||
"docs/admin-roles-support/03-roles-matrix.json",
|
||||
"docs/plan-permissions/03-permission-contract.md",
|
||||
"docs/plan-permissions/03-permission-contract.json",
|
||||
"docs/plan-permissions/01-feature-keys.json",
|
||||
"docs/plan-permissions/06-defaults-matrix.json"
|
||||
],
|
||||
"assumptions": [
|
||||
"Product cohort (legacy/standard/custom) is a plan feature_profile, not a stored end-user RBAC role; legacy_user/standard_user are derived labels",
|
||||
"support_staff is least privilege: ticket queue assign/reply only — narrower than optional read-assist in 03",
|
||||
"packages ≡ plans rows; no parallel permission service outside billing + auth staff flags",
|
||||
"is_platform_admin retained; staff_role additive with null+admin-flag ⇒ staff_role admin back-compat",
|
||||
"A1/Local Demo Co Enterprise → Legacy assign is explicit ops step, not silent migration"
|
||||
],
|
||||
"axes": {
|
||||
"plan_features": {
|
||||
"subject": "company.active_plan",
|
||||
"storage": ["plans.features", "plans.feature_profile", "platform_feature_gates"],
|
||||
"resolver": "ResolveEffectiveFeatures / CapabilitiesForCompany",
|
||||
"failure": { "http": 402, "code": "plan_gate" }
|
||||
},
|
||||
"company_membership": {
|
||||
"subject": "user in company",
|
||||
"storage": "memberships.role",
|
||||
"values": ["admin", "member"],
|
||||
"notes": "Orthogonal; do not overload with platform admin"
|
||||
},
|
||||
"platform_staff": {
|
||||
"subject": "user",
|
||||
"storage": ["users.is_platform_admin", "users.staff_role"],
|
||||
"resolver": "resolve_staff_role + staff_allows(capability)",
|
||||
"failure": { "http": 403, "error": "staff capability required" }
|
||||
}
|
||||
},
|
||||
"runtime_formulas": {
|
||||
"effective_feature": "plan_allows(key) AND global_section_enabled(section(key)) AND global_feature_enabled(key)",
|
||||
"plan_allows": [
|
||||
"if key in plans.features -> plans.features[key]",
|
||||
"else if resolve_plan_profile == legacy -> LegacyMatrix[key] (missing -> false)",
|
||||
"else if resolve_plan_profile == custom -> true",
|
||||
"else -> DefaultPlanFeatures(name, false)[key]"
|
||||
],
|
||||
"resolve_plan_profile_priority": [
|
||||
"plans.feature_profile if set (legacy|ladder|custom)",
|
||||
"legacy name patterns from 03-roles-matrix",
|
||||
"IsCustomPackage -> custom",
|
||||
"else ladder"
|
||||
],
|
||||
"staff_allows": "user.is_active AND resolve_staff_role(user) in allowed_roles(capability)",
|
||||
"resolve_staff_role": [
|
||||
"inactive -> none",
|
||||
"staff_role if not null",
|
||||
"else if is_platform_admin -> admin",
|
||||
"else none"
|
||||
]
|
||||
},
|
||||
"legacy": {
|
||||
"profile": "legacy",
|
||||
"shipping_modes": [
|
||||
{
|
||||
"id": "named_profile",
|
||||
"preferred": true,
|
||||
"mechanism": "plans.feature_profile='legacy' or name-pattern derive; DefaultPlanFeatures/LegacyMatrix"
|
||||
},
|
||||
{
|
||||
"id": "sparse_or_dense_features",
|
||||
"preferred": false,
|
||||
"mechanism": "plans.features explicit map; requires profile precedence so missing keys are not custom-all-on"
|
||||
}
|
||||
],
|
||||
"must_not": "Fall through IsCustomPackage all-ON for A1/legacy names",
|
||||
"allowlist_authority": "docs/admin-roles-support/03-roles-matrix.md",
|
||||
"excluded_sections": [
|
||||
"processing.monitor",
|
||||
"stores.*",
|
||||
"marketing.*",
|
||||
"integrations.*",
|
||||
"support.*"
|
||||
],
|
||||
"name_patterns_ref": "docs/admin-roles-support/03-roles-matrix.json#plan_name_patterns",
|
||||
"a1_example": {
|
||||
"legacy_company_id": "97e1a309-3d23-4aa2-b518-8e8d7afdfec7",
|
||||
"demo_note": "Local Demo Co may still be on Enterprise until explicit Legacy assign"
|
||||
}
|
||||
},
|
||||
"staff_roles": {
|
||||
"values": ["admin", "developer", "support_staff"],
|
||||
"storage": {
|
||||
"keep": "users.is_platform_admin",
|
||||
"add": {
|
||||
"column": "users.staff_role",
|
||||
"type": "TEXT NULL CHECK IN (admin, developer, support_staff)",
|
||||
"invariant": "staff_role set implies is_platform_admin=true"
|
||||
}
|
||||
},
|
||||
"backfill": "UPDATE users SET staff_role='admin' WHERE is_platform_admin AND staff_role IS NULL",
|
||||
"capabilities": {
|
||||
"staff.admin_shell": ["admin", "developer", "support_staff"],
|
||||
"staff.support.queue": ["admin", "developer", "support_staff"],
|
||||
"staff.support.reply": ["admin", "developer", "support_staff"],
|
||||
"staff.support.assign": ["admin", "developer", "support_staff"],
|
||||
"staff.users.read": ["admin", "developer"],
|
||||
"staff.users.write": ["admin", "developer"],
|
||||
"staff.analytics": ["admin", "developer"],
|
||||
"staff.billing": ["admin", "developer"],
|
||||
"staff.plans_features": ["admin", "developer"],
|
||||
"staff.feature_gates": ["admin", "developer"],
|
||||
"staff.settings": ["admin", "developer"],
|
||||
"staff.jobs_stuck": ["admin", "developer"],
|
||||
"staff.impersonate": ["admin", "developer"],
|
||||
"staff.dev_password": ["admin", "developer"]
|
||||
},
|
||||
"support_staff_least_privilege": {
|
||||
"allowed_web": ["/admin/support", "/admin/support/[id]"],
|
||||
"allowed_api": [
|
||||
"GET /api/admin/support/tickets",
|
||||
"GET /api/admin/support/tickets/{id}",
|
||||
"POST /api/admin/support/tickets/{id}/messages",
|
||||
"PATCH /api/admin/support/tickets/{id}"
|
||||
],
|
||||
"denied": [
|
||||
"billing",
|
||||
"plans",
|
||||
"feature_gates",
|
||||
"credits",
|
||||
"settings",
|
||||
"users_write",
|
||||
"impersonate",
|
||||
"stuck_cleanup",
|
||||
"self_escalate_staff_role"
|
||||
],
|
||||
"deferred": "ticket-side read-only company context without tenant write APIs"
|
||||
},
|
||||
"middleware": {
|
||||
"prefer": ["RequireStaff", "RequireStaffCapability"],
|
||||
"extend": [
|
||||
"apps/api/internal/httpapi/middleware.go#RequirePlatformAdmin",
|
||||
"apps/api/internal/httpapi/middleware.go#checkPlatformAdmin",
|
||||
"apps/api/internal/auth/invites.go#IsPlatformAdmin"
|
||||
],
|
||||
"router_today": "apps/api/internal/httpapi/server.go /api/admin RequireSession+RequirePlatformAdmin",
|
||||
"ui": [
|
||||
"apps/web/src/lib/admin-gate.ts",
|
||||
"apps/web/src/lib/components/AdminNav.svelte"
|
||||
]
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"session": "scs cookie + RequireSession; staff flags from DB not client claims",
|
||||
"csrf": "double-submit X-CSRF-Token on dashboard /api including /api/admin; v1/public/webhooks exempt",
|
||||
"company_isolation": [
|
||||
"tenant APIs RequireCompany scoped",
|
||||
"admin support cross-company ticket view does not open tenant write APIs",
|
||||
"impersonate admin|developer non-prod only; never support_staff",
|
||||
"allowCompanyAdminOrPlatform: only admin|developer count as platform bypass — not support_staff"
|
||||
],
|
||||
"no_privilege_escalation": [
|
||||
"capability middleware on admin routes",
|
||||
"no self-service staff_role",
|
||||
"plan features never grant /admin",
|
||||
"staff never granted by plan JSON",
|
||||
"fail closed on missing role/capability"
|
||||
]
|
||||
},
|
||||
"performance": {
|
||||
"existing": {
|
||||
"symbol": "featureETag",
|
||||
"path": "apps/api/internal/billing/plan_features.go",
|
||||
"semantics": "sha256 of sorted enabled feature keys"
|
||||
},
|
||||
"rules": [
|
||||
"Do not fold staff_role into feature_etag",
|
||||
"Keep company capabilities cache keyed by company_id + feature_etag",
|
||||
"Invalidate on plan assign, plans.features write, global gates write",
|
||||
"etag remains pure function of effective enabled-key set (profile already reflected in map)",
|
||||
"Resolve staff role once per request in middleware"
|
||||
],
|
||||
"me_payload_additive": {
|
||||
"user.staff_role": "string|null",
|
||||
"staff_capabilities": "string[]",
|
||||
"credits.feature_profile": "legacy|ladder|custom|null",
|
||||
"credits.feature_etag": "unchanged semantics"
|
||||
}
|
||||
},
|
||||
"migration": {
|
||||
"file_suggestion": "apps/api/sql/schema/027_staff_roles_legacy_profile.sql",
|
||||
"idempotent": true,
|
||||
"destructive": false,
|
||||
"steps": [
|
||||
"ALTER users ADD staff_role NULL CHECK",
|
||||
"ALTER plans ADD feature_profile NULL CHECK",
|
||||
"Backfill staff_role=admin for existing platform admins",
|
||||
"Ensure Legacy plan row with feature_profile=legacy",
|
||||
"Optional sparse plans.features from LegacyMatrix",
|
||||
"Explicit A1/demo assign — no silent Enterprise downgrade",
|
||||
"EnsureDefaultPlans must not clobber features or feature_profile"
|
||||
],
|
||||
"optional_flag": "STAFF_RBAC=1 to enforce capability middleware",
|
||||
"breaking_changes": [],
|
||||
"preserve": [
|
||||
"is_platform_admin",
|
||||
"public ladder names",
|
||||
"Stripe public-ladder checkout",
|
||||
"402 plan_gate shapes",
|
||||
"support assignee_admin_user_id schema",
|
||||
"CSRF session behavior"
|
||||
]
|
||||
},
|
||||
"reuse_symbols": {
|
||||
"billing": [
|
||||
"DefaultPlanFeatures",
|
||||
"SparseDefaultOverrides",
|
||||
"IsCustomPackage",
|
||||
"PlanAllowsFeature",
|
||||
"ResolveEffectiveFeatures",
|
||||
"CapabilitiesForCompany",
|
||||
"featureETag",
|
||||
"EntitlementsForCompany"
|
||||
],
|
||||
"auth_http": [
|
||||
"IsPlatformAdmin",
|
||||
"RequirePlatformAdmin",
|
||||
"checkPlatformAdmin",
|
||||
"CSRF",
|
||||
"RequireSession",
|
||||
"allowCompanyAdminOrPlatform"
|
||||
],
|
||||
"support": [
|
||||
"ListAdmin",
|
||||
"GetAdmin",
|
||||
"ReplyAsAgent",
|
||||
"UpdateAdmin",
|
||||
"handleAdminListSupportTickets"
|
||||
],
|
||||
"web": [
|
||||
"requirePlatformAdmin",
|
||||
"AdminNav",
|
||||
"PlanPermissionsPanel",
|
||||
"planCapabilities",
|
||||
"Nav.svelte"
|
||||
]
|
||||
},
|
||||
"do_not_add": [
|
||||
"parallel permissions package outside billing/auth",
|
||||
"packages table",
|
||||
"tenant feature_keys for /admin/*",
|
||||
"mixing LLM admin-ai-roles into staff RBAC",
|
||||
"staff fields inside feature_etag"
|
||||
],
|
||||
"verification": [
|
||||
"legacy excludes processing/stores/marketing/integrations/support",
|
||||
"A1/legacy profile not custom-all-on",
|
||||
"custom non-legacy still all-on",
|
||||
"ladder Free/Starter denials unchanged",
|
||||
"null staff_role + is_platform_admin => admin",
|
||||
"support_staff 403 on billing/plans/settings",
|
||||
"support_staff AdminNav Support-only",
|
||||
"CSRF still on admin mutations",
|
||||
"feature_etag stable across staff_role-only changes",
|
||||
"tenant APIs remain company-scoped for support_staff"
|
||||
],
|
||||
"open_questions_defaults": {
|
||||
"a1_demo_cutover": "explicit_ops_assign",
|
||||
"support_staff_sets_is_platform_admin": true,
|
||||
"company_admin_platform_bypass": "admin_and_developer_only",
|
||||
"legacy_seed_density": "profile_first_sparse_optional"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user