Initial v1 code layout

The destination of the wayfinder map: directory and module structure, the
domain model as real schema files, permission boundaries expressed as code,
and typed seams where the undecided parts land.

Satisfies the four structural requirements earlier resolutions handed to a
layout that did not exist:

  - a single database entry point (#11) — the pool is not exported
  - a scheduler entry point with actor context and no request (#13)
  - an entitlement assertion helper (#20, #34) — its call sites are the
    paid-feature list
  - an enumerated list of privileged RLS bypasses (#33) — three of them

Seams carry real types and throw with the ticket that owns them, so the
skeleton wires up and fails only where a decision is genuinely missing.

Verified: tsc --noEmit clean; drizzle-kit generate produces RLS on 18 tables
and 6 policies calling the release-level functions.

Resolves #35

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian
2026-08-03 17:20:47 -04:00
commit da74362d00
46 changed files with 9927 additions and 0 deletions

71
src/auth/entitlement.ts Normal file
View File

@@ -0,0 +1,71 @@
import { Seam } from '../seam.js'
import { type ActorContext } from '../db/actor.js'
/**
* The entitlement assertion helper. Required by #20 decision 3.
*
* Entitlement is resolved once into actor context and asserted per feature at
* each feature's own entry — not as row visibility, and not at one chokepoint.
* Writing a document is free; writing it FOR REUSE is paid. The same INSERT
* serves both, so an operation deny-list would have to encode intent it
* cannot see.
*
* ── THE GOVERNING PRINCIPLE ─────────────────────────────────────────────
*
* Entitlement gates what a vendor may newly undertake,
* never what a retailer has already come to rely on.
*
* Restated by #34 when the free tier was removed. Concretely: expiry state is
* computed for every vendor regardless of billing; eligibility never depends
* on subscription state; and on lapse, live participations run to completion.
* ────────────────────────────────────────────────────────────────────────
*
* Useful side effect: the call sites of `require` ARE the paid-feature list.
* The pricing page is derived from the code rather than maintained alongside it.
*/
export type Feature =
| 'ai.draft'
| 'ai.matching'
| 'ai.go_no_go'
| 'ai.response_completeness'
| 'integration.api'
| 'sso'
export const FEATURE_TIER: Record<Feature, 'base' | 'pro' | 'enterprise'> = {
'ai.draft': 'pro',
'ai.matching': 'pro',
'ai.go_no_go': 'pro',
'ai.response_completeness': 'pro',
'integration.api': 'enterprise',
sso: 'enterprise',
}
export interface Entitled extends ActorContext {
tier: 'base' | 'pro' | 'enterprise'
lapsed: boolean
}
export class NotEntitled extends Error {
constructor(readonly feature: Feature, readonly required: string) {
super(`${feature} requires ${required}`)
this.name = 'NotEntitled'
}
}
/**
* Assert at a feature entry. Throwing is the API — a missing entitlement is
* never a silent empty result, because that is what RLS denial looks like and
* the two must stay distinguishable.
*/
export function require(ctx: Entitled, feature: Feature): void {
throw new Seam('#35', `entitlement resolution for ${feature} (${ctx.tier})`)
}
/**
* Visible-but-locked is the DEFAULT rendering for a missing entitlement.
* Absent is a bug — Pro has no upgrade moment if the affordance is invisible
* (#34, carried into #23).
*/
export function affordance(ctx: Entitled, feature: Feature): 'enabled' | 'locked' {
return ctx.tier === FEATURE_TIER[feature] || ctx.tier === 'enterprise' ? 'enabled' : 'locked'
}