cvx-kit 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/components/approvals/audit.mjs +1 -1
- package/dist/components/approvals/workflow.d.mts +1 -1
- package/dist/components/foundation/client.d.mts +14 -8
- package/dist/components/foundation/client.mjs +1 -1
- package/dist/components/foundation/client.mjs.map +1 -1
- package/dist/components/foundation/modules/command/command.d.mts +14 -5
- package/dist/components/foundation/modules/command/command.mjs +1 -1
- package/dist/components/foundation/modules/command/command.mjs.map +1 -1
- package/dist/components/foundation/operation.d.mts +74 -0
- package/dist/components/foundation/operation.mjs +2 -0
- package/dist/components/foundation/operation.mjs.map +1 -0
- package/dist/zod-table.d.mts +4 -4
- package/dist/zod-table.mjs +1 -1
- package/dist/zod-table.mjs.map +1 -1
- package/docs/commands.md +130 -15
- package/docs/upgrading.md +42 -0
- package/docs/zod-table.md +22 -0
- package/package.json +13 -11
- package/scripts/extract-changelog.mjs +1 -1
- package/scripts/smoke-packed-components.mjs +25 -24
- package/scripts/test-packed-ids.mjs +157 -0
- package/src/test.ts +2 -1
- package/docs/plans/2026-08-14-001-feat-v0.1.0-feature-set-plan.md +0 -461
- package/docs/plans/2026-08-21-0930-feat-resolve-organization-hook-plan.md +0 -178
|
@@ -1,461 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "feat: v0.1.0 feature set — the six roadmap helpers + release discipline"
|
|
3
|
-
type: feat
|
|
4
|
-
date: 2026-08-14
|
|
5
|
-
artifact_contract: ce-unified-plan/v1
|
|
6
|
-
artifact_readiness: implementation-ready
|
|
7
|
-
execution: code
|
|
8
|
-
product_contract_source: ce-plan-bootstrap
|
|
9
|
-
origin: ROADMAP.md
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# feat: v0.1.0 feature set — the six roadmap helpers + release discipline
|
|
13
|
-
|
|
14
|
-
## Summary
|
|
15
|
-
|
|
16
|
-
Implement cvx-kit `v0.1.0` as defined in `ROADMAP.md`: the six planned
|
|
17
|
-
features (pagination bridge, CRUD command factory, state-machine helper,
|
|
18
|
-
rate-limit middleware factory, webhook boundary helper, agent tool
|
|
19
|
-
generator), the typed-middleware-context tightening they build on, and the
|
|
20
|
-
release discipline that starts at 0.1.0 — `CHANGELOG.md`, GitHub Releases on
|
|
21
|
-
every tag, and a shipped upgrade guide (the "migration file") documenting the
|
|
22
|
-
old way → new way for consumers on 0.0.x.
|
|
23
|
-
|
|
24
|
-
**Product Contract preservation:** n/a — solo plan; scope confirmed in-session.
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Problem Frame
|
|
29
|
-
|
|
30
|
-
cvx-kit 0.0.8 has the structural kernel (boundaries, auth, tenancy, commands,
|
|
31
|
-
middleware) but consumers still hand-write the same per-entity code: CRUD
|
|
32
|
-
operations, transition guards, pagination plumbing, rate limits, webhook
|
|
33
|
-
dedup, and agent tool definitions. 0.1.0 is "the big one" (see
|
|
34
|
-
`docs/explain/roadmap-to-1.0.0.html`): ship the full helper surface in one
|
|
35
|
-
release so consumers adopt a complete kit once, and switch on the semver
|
|
36
|
-
discipline (breaking = minor only, CHANGELOG, GitHub Releases) at the same
|
|
37
|
-
moment.
|
|
38
|
-
|
|
39
|
-
## Requirements
|
|
40
|
-
|
|
41
|
-
- R1. `paginated(dto)` helpers + a `paginate` terminal on `include` give
|
|
42
|
-
cursor pagination with zod-typed args/returns.
|
|
43
|
-
- R2. `createCrudCommands` generates create/update/archive operations from a
|
|
44
|
-
`zodTable`, fully inside the audited command pipeline.
|
|
45
|
-
- R3. `createStateMachine` produces typed transition predicates/assertions
|
|
46
|
-
from a constants tuple, usable as command guards.
|
|
47
|
-
- R4. A packaged rate-limit middleware factory works on both Command and
|
|
48
|
-
Query middleware seams, keyed by ctx (default `ctx.tenant`).
|
|
49
|
-
- R5. A webhook boundary helper provides signature verification + natural-key
|
|
50
|
-
dedup + delegation to internal functions.
|
|
51
|
-
- R6. `createAgentTools` emits agent tool definitions from the existing
|
|
52
|
-
`tools.*` masks whose handlers route through command executors —
|
|
53
|
-
shape-compatible with `@convex-dev/agent`, no dependency on it.
|
|
54
|
-
- R7. Middleware context enrichment is typed: `next({ context })` widens the
|
|
55
|
-
downstream ctx type; the existing loose form keeps compiling.
|
|
56
|
-
- R8. `CHANGELOG.md` exists; every release tag gets a GitHub Release whose
|
|
57
|
-
notes come from the CHANGELOG (user-directed: changelog also on GitHub
|
|
58
|
-
versioning).
|
|
59
|
-
- R9. A shipped upgrade guide documents 0.0.x → 0.1.0 per feature (old way →
|
|
60
|
-
new way), and every new feature has a docs page/section + `llms.md` entry
|
|
61
|
-
+ tests.
|
|
62
|
-
- R10. All existing invariants hold: facade-only foundation runtime exports,
|
|
63
|
-
middleware inside pipeline invariants, strict boundary schemas, tarball =
|
|
64
|
-
dist + docs + `src/test.ts` only.
|
|
65
|
-
|
|
66
|
-
## Key Technical Decisions
|
|
67
|
-
|
|
68
|
-
- KTD1. **One new runtime dependency only: `@convex-dev/rate-limiter`**
|
|
69
|
-
(session-settled: user-approved — chosen over also depending on
|
|
70
|
-
`@convex-dev/agent`: the rate-limit factory calls the limiter at runtime,
|
|
71
|
-
while agent tools can be emitted shape-compatible). The host mounts the
|
|
72
|
-
rate-limiter component; the kit factory receives the constructed
|
|
73
|
-
`RateLimiter` instance (injection, per the kit's no-singletons rule).
|
|
74
|
-
- KTD2. **Agent tool generator is dependency-free** (session-settled:
|
|
75
|
-
user-approved). It returns `{ name, description, args, handler }` records
|
|
76
|
-
where `args` is a zod schema (using `jsonSafeZid` internally) — directly
|
|
77
|
-
consumable by `@convex-dev/agent`'s `createTool` and adaptable to ai-sdk
|
|
78
|
-
`tool()`. Verified against `@convex-dev/agent@0.6.4`'s contract.
|
|
79
|
-
- KTD3. **Typed middleware context lands first** (session-settled:
|
|
80
|
-
user-directed — pulled from the consolidation list because the rate-limit
|
|
81
|
-
factory should land on final typing, not the loose v1). Backward
|
|
82
|
-
compatible: an untyped `next({ context })` still compiles.
|
|
83
|
-
- KTD4. **New helpers ship as subpath exports** matching the existing
|
|
84
|
-
pattern (`cvx-kit/pagination` style is rejected — see below): `crud`,
|
|
85
|
-
`state-machine`, `middleware`, `webhooks`, `agent-tools` each get a
|
|
86
|
-
subpath + index re-export; pagination joins existing homes (`zod-table`
|
|
87
|
-
for schema helpers, `auth` for the include terminal) rather than a new
|
|
88
|
-
module, since it derives from those seams.
|
|
89
|
-
- KTD5a. convex-helpers' same-named `crud` helper was considered and
|
|
90
|
-
rejected: it bypasses the command pipeline (no audits, no guards, hard
|
|
91
|
-
deletes) — `src/docs/crud.md` states the distinction.
|
|
92
|
-
- KTD5. **CRUD factory takes the Foundation-bound `Command` class as input**
|
|
93
|
-
— `createCrudCommands({ Command, table, ... })` — never imports it. Keeps
|
|
94
|
-
the facade-only rule intact: the factory is a *user* of the destructured
|
|
95
|
-
facade, exactly like a domain module.
|
|
96
|
-
- KTD6. **Webhook dedup storage is host-owned**: the helper exports a
|
|
97
|
-
`webhookEventsTable()` (a plain `zodTable`) the host registers in its
|
|
98
|
-
schema, and the boundary helper receives the table name + internal
|
|
99
|
-
function references. The kit gains no tables (components own zero
|
|
100
|
-
application capability).
|
|
101
|
-
- KTD7. **GitHub Releases are created by the existing release workflow**
|
|
102
|
-
(user-directed: "the changelog also should be on the github versioning") —
|
|
103
|
-
a step after successful publish extracts the current version's CHANGELOG
|
|
104
|
-
section and runs `gh release create` with it. The deprecated action pins
|
|
105
|
-
(`actions/checkout`, `actions/setup-node`) get bumped in the same touch.
|
|
106
|
-
- KTD8. **Upgrade guide ships in package docs** (session-settled:
|
|
107
|
-
user-approved — chosen over root-only `MIGRATING.md`): authored at
|
|
108
|
-
`src/docs/upgrading.md`, synced to `docs/` like the rest, linked from
|
|
109
|
-
README and `llms.md`.
|
|
110
|
-
|
|
111
|
-
## High-Level Technical Design
|
|
112
|
-
|
|
113
|
-
How the six features attach to existing seams (nothing new is load-bearing —
|
|
114
|
-
every feature is a consumer of an existing extension point):
|
|
115
|
-
|
|
116
|
-
```mermaid
|
|
117
|
-
flowchart LR
|
|
118
|
-
subgraph existing [Existing seams — unchanged]
|
|
119
|
-
ZT[zodTable masks]
|
|
120
|
-
INC[include builder]
|
|
121
|
-
CMD[Foundation Command\npipeline + guards]
|
|
122
|
-
MW[middleware chain]
|
|
123
|
-
SYS[system* constructors]
|
|
124
|
-
end
|
|
125
|
-
P[pagination bridge] --> ZT
|
|
126
|
-
P --> INC
|
|
127
|
-
CRUD[createCrudCommands] --> CMD
|
|
128
|
-
SM[createStateMachine] -->|guard| CMD
|
|
129
|
-
RL[rateLimit factory] --> MW
|
|
130
|
-
WH[webhook boundary] --> SYS
|
|
131
|
-
AT[createAgentTools] -->|handlers| CMD
|
|
132
|
-
AT --> ZT
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Directional guidance, not implementation specification.
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
## Implementation Units
|
|
140
|
-
|
|
141
|
-
### U1. Typed middleware context accumulation
|
|
142
|
-
|
|
143
|
-
**Goal:** `next({ context })` widens the downstream ctx type; guards and
|
|
144
|
-
handlers after an enriching middleware see the enrichment in types.
|
|
145
|
-
**Requirements:** R7, R10. **Dependencies:** none.
|
|
146
|
-
**Files:** `src/components/foundation/client.ts`,
|
|
147
|
-
`src/components/foundation/modules/query/query.ts`,
|
|
148
|
-
`test/command.test.ts`, `test/query-middleware.test.ts`.
|
|
149
|
-
**Approach:** Generic parameter on the middleware helper
|
|
150
|
-
(`Command.middleware<Ctx, Extension>`) so the middleware's declared extension
|
|
151
|
-
threads into the chain's context type; runtime unchanged (merge already
|
|
152
|
-
works). Keep `CommandMiddleware`/`QueryMiddleware` assignable from the loose
|
|
153
|
-
form. TanStack-style full inference across an array is explicitly NOT the
|
|
154
|
-
goal for 0.1.0 — per-middleware declared extensions are enough.
|
|
155
|
-
**Test scenarios:**
|
|
156
|
-
- A middleware declaring `{ vendor: string }` lets the guard/handler use
|
|
157
|
-
`ctx.vendor` without casts (type-level test via `tsc` on a fixture).
|
|
158
|
-
- Existing untyped middleware in `test/command.test.ts` compiles unchanged.
|
|
159
|
-
- Runtime ordering tests keep passing (no behavior change).
|
|
160
|
-
**Verification:** typecheck + full suite green; no `as never` needed in the
|
|
161
|
-
middleware tests that previously required it.
|
|
162
|
-
|
|
163
|
-
### U2. Pagination bridge
|
|
164
|
-
|
|
165
|
-
**Goal:** cursor pagination with zod-typed boundaries.
|
|
166
|
-
**Requirements:** R1, R10. **Dependencies:** none.
|
|
167
|
-
**Files:** `src/zod-table.ts` (arg/return schema helpers), `src/auth.ts`
|
|
168
|
-
(`paginate` terminal on `IncludedQuery`), `test/pagination.test.ts`,
|
|
169
|
-
`test/fixture/functions.ts` (one paginated query),
|
|
170
|
-
`src/docs/zod-table.md`, `src/docs/auth.md`, `src/docs/llms.md`.
|
|
171
|
-
**Approach:**
|
|
172
|
-
1. `paginated(dto)` → `{ args: convexToZod(paginationOptsValidator)`-based
|
|
173
|
-
opts schema, `returns:` a zod page result (`page: dto[]`, `isDone`,
|
|
174
|
-
`continueCursor`) via convex-helpers bridges.
|
|
175
|
-
2. `include(...).paginate(opts, transform?)` terminal calling Convex's
|
|
176
|
-
`query.paginate(opts)` on the selected indexed query; page size bounded by
|
|
177
|
-
the same `maxRows` policy as `execute`.
|
|
178
|
-
**Execution note:** verify early that a paginated read works through the
|
|
179
|
-
RLS-wrapped reader (tenancy fixture) — if the wrapped reader doesn't support
|
|
180
|
-
`paginate`, that's a planning-assumption failure to surface, not to patch
|
|
181
|
-
around silently.
|
|
182
|
-
**Test scenarios:**
|
|
183
|
-
- Happy path: two pages traverse a seeded fixture table via an index;
|
|
184
|
-
cursors chain; `isDone` terminates.
|
|
185
|
-
- DTO projection: page items pass through `toPublicDto` transform.
|
|
186
|
-
- Bound: page size above `maxRows` rejects with `INVALID_REQUEST_BOUNDARY`.
|
|
187
|
-
- Tenancy: paginated scan in the tenant fixture returns only the caller
|
|
188
|
-
tenant's rows.
|
|
189
|
-
- Short pages: with foreign-tenant rows interleaved, pages may be shorter
|
|
190
|
-
than `numItems` but cursors remain correct and traversal completes.
|
|
191
|
-
**Verification:** suite green including the tenancy pagination case.
|
|
192
|
-
|
|
193
|
-
### U3. State-machine helper
|
|
194
|
-
|
|
195
|
-
**Goal:** typed transition legality from constants tuples.
|
|
196
|
-
**Requirements:** R3. **Dependencies:** none.
|
|
197
|
-
**Files:** `src/state-machine.ts` (new), `src/index.ts`, `package.json`
|
|
198
|
-
(exports subpath `./state-machine`), `test/state-machine.test.ts`,
|
|
199
|
-
`src/docs/commands.md` (guard example), `src/docs/llms.md`.
|
|
200
|
-
**Approach:** `createStateMachine(STATES, transitions)` where `transitions`
|
|
201
|
-
is `Partial<Record<State, readonly State[]>>` typed against the tuple.
|
|
202
|
-
Returns `{ states, can(from, to), assert(from, to, errors?) }`; `assert`
|
|
203
|
-
throws via the injected `ErrorFactory` (default `KitError`,
|
|
204
|
-
`INVALID_TRANSITION`) so it drops into command guards.
|
|
205
|
-
**Patterns to follow:** vocabulary-tuple convention from `conventions.md`;
|
|
206
|
-
error injection from `src/errors.ts`.
|
|
207
|
-
**Test scenarios:**
|
|
208
|
-
- Legal transition passes; illegal throws `INVALID_TRANSITION`.
|
|
209
|
-
- A state with no outgoing transitions rejects everything.
|
|
210
|
-
- Type-level: a literal not in the tuple fails to compile in `transitions`.
|
|
211
|
-
- Custom `ErrorFactory` receives the throw.
|
|
212
|
-
**Verification:** suite green; the commands.md guard example compiles as
|
|
213
|
-
written in a fixture.
|
|
214
|
-
|
|
215
|
-
### U4. CRUD command factory
|
|
216
|
-
|
|
217
|
-
**Goal:** standard create/update/archive operations from one declaration.
|
|
218
|
-
**Requirements:** R2, R10. **Dependencies:** U1 (types). (U3 is referenced
|
|
219
|
-
only by the archive-guard docs example, not by the implementation — U3 and U4
|
|
220
|
-
can proceed in parallel.)
|
|
221
|
-
**Files:** `src/crud.ts` (new), `src/index.ts`, `package.json` (subpath
|
|
222
|
-
`./crud`), `test/crud.test.ts` (real-runtime via a fixture),
|
|
223
|
-
`test/fixture/` additions, `src/docs/crud.md` (new), `src/docs/llms.md`,
|
|
224
|
-
README modules section.
|
|
225
|
-
**Approach:** `createCrudCommands({ Command, table, aggregateType,
|
|
226
|
-
classification?, actor, archive?, guards?, middleware? })` returning
|
|
227
|
-
`{ operations, executeCreate, executeUpdate, executeArchive }`:
|
|
228
|
-
1. Operation keys derived `<tableName>.create|update|archive`.
|
|
229
|
-
2. `create` input = `table.commandInput` (+ injected ids the host declares);
|
|
230
|
-
handler inserts with `tenant`/server fields stamped via a host-provided
|
|
231
|
-
`enrich(ctx, command)` callback (keeps the factory tenancy-agnostic).
|
|
232
|
-
3. `update` input = commandInput partial + id; `archive` sets `archivedAt`
|
|
233
|
-
(soft delete per kit doctrine — no hard-delete operation is generated).
|
|
234
|
-
4. Audit derivation built from `aggregateType` + actor mapping; `aggregates:
|
|
235
|
-
[aggregateType]` declared so the allowlist enforces it.
|
|
236
|
-
**Test scenarios:**
|
|
237
|
-
- Create/update/archive execute on the real runtime through authMutation
|
|
238
|
-
adapters in the fixture; rows reflect changes; audits written with the
|
|
239
|
-
declared aggregate type.
|
|
240
|
-
- Update with a field outside `commandFields` rejects (strict schema).
|
|
241
|
-
- Archive sets `archivedAt`, leaves the row (no delete).
|
|
242
|
-
- Guards passed per-operation run before handlers (reuse guard-order test
|
|
243
|
-
approach from `test/command.test.ts`).
|
|
244
|
-
- Generated operations respect `classification` default `'business'`.
|
|
245
|
-
- Tenancy: in the tenant fixture (factory registered behind tenant RLS rules),
|
|
246
|
-
an update/archive targeting another tenant's document id rejects, and a
|
|
247
|
-
create whose enrich fails to stamp `tenant` is rejected by RLS — mirrors
|
|
248
|
-
U2's tenancy case. `enrich` is REQUIRED when the table is a tenantTable
|
|
249
|
-
(compile-level where feasible).
|
|
250
|
-
**Verification:** suite green; `docs/crud.md` example is the fixture code.
|
|
251
|
-
|
|
252
|
-
### U5. Rate-limit middleware factory
|
|
253
|
-
|
|
254
|
-
**Goal:** first packaged middleware.
|
|
255
|
-
**Requirements:** R4, R10. **Dependencies:** U1.
|
|
256
|
-
**Files:** `src/middleware.ts` (new; home for packaged middleware),
|
|
257
|
-
`src/index.ts`, `package.json` (dependency `@convex-dev/rate-limiter@^0.3`,
|
|
258
|
-
subpath `./middleware`), `test/middleware.test.ts`,
|
|
259
|
-
`src/docs/commands.md` (middleware section), `src/docs/tenancy.md` (keying
|
|
260
|
-
note), `src/docs/llms.md`.
|
|
261
|
-
**Approach:** `rateLimit({ limiter, name, key?, onLimit? })` returning a
|
|
262
|
-
`CommandMiddleware` (and a query variant): before `next()`, call
|
|
263
|
-
`limiter.limit(ctx, name, { key: key?.(ctx) ?? ctx.tenant })`; on rejection
|
|
264
|
-
throw via `ErrorFactory` (`RATE_LIMITED`, carrying `retryAfter` in the
|
|
265
|
-
message/metadata) or call `onLimit`. The limiter instance is injected —
|
|
266
|
-
checked against registry snapshot of `@convex-dev/rate-limiter@0.3.2` (re-verify at install) (`new
|
|
267
|
-
RateLimiter(components.rateLimiter, { limits })`, `limit(ctx, name, { key
|
|
268
|
-
})` returning status). The kit never mounts the component; docs show host
|
|
269
|
-
mounting.
|
|
270
|
-
**Test scenarios:**
|
|
271
|
-
- Fake limiter injected: under-limit passes through and handler runs.
|
|
272
|
-
- Over-limit: handler never runs, `RATE_LIMITED` thrown, audit skipped.
|
|
273
|
-
- Default key is `ctx.tenant`; custom `key` fn receives the ctx.
|
|
274
|
-
- No `key` fn and `ctx.tenant` undefined: configuration error, handler never
|
|
275
|
-
runs.
|
|
276
|
-
- Works in both registry-wide and per-operation middleware slots (order
|
|
277
|
-
test).
|
|
278
|
-
**Verification:** suite green; publint clean with the new dependency external
|
|
279
|
-
in the build (attw stays deferred per Scope Boundaries).
|
|
280
|
-
|
|
281
|
-
### U6. Webhook boundary helper
|
|
282
|
-
|
|
283
|
-
**Goal:** the sanctioned webhook pattern as code.
|
|
284
|
-
**Requirements:** R5, R10. **Dependencies:** none.
|
|
285
|
-
**Files:** `src/webhooks.ts` (new), `src/index.ts`, `package.json` (subpath
|
|
286
|
-
`./webhooks`), `test/webhooks.test.ts` (real runtime),
|
|
287
|
-
`test/fixture-webhooks/` (schema + functions + `_generated` stub),
|
|
288
|
-
`src/docs/webhooks.md` (new), `src/docs/llms.md`.
|
|
289
|
-
**Approach:**
|
|
290
|
-
1. `webhookEventsTable()` — a plain `zodTable` (`eventKey`, `receivedAt`,
|
|
291
|
-
`source`) the host registers; `by_eventKey` index documented.
|
|
292
|
-
2. `createWebhookBoundary({ verify, eventKey, table?, errors? })` returning
|
|
293
|
-
`handle(ctx, request, target)` for use inside the host's `httpAction`:
|
|
294
|
-
verify signature over the RAW request body (`request.text()` handed to
|
|
295
|
-
`verify` before any parsing; docs example uses constant-time comparison —
|
|
296
|
-
`crypto.subtle` HMAC verify — with the secret read from a Convex env var;
|
|
297
|
-
throw on failure), compute the natural key, `runMutation(target, payload)` where `target` is a host internal
|
|
298
|
-
mutation that calls the kit's `recordWebhookEvent(ctx, key)` guard —
|
|
299
|
-
insert-if-absent, return `duplicate: true` when seen (dedup is
|
|
300
|
-
transactional inside the mutation, not in the action).
|
|
301
|
-
**Execution note:** dedup must live in the mutation (transactional), not the
|
|
302
|
-
action — design the API so the helper makes this the easy path.
|
|
303
|
-
**Test scenarios:**
|
|
304
|
-
- First delivery processes; identical redelivery (same natural key) is a
|
|
305
|
-
no-op reporting duplicate.
|
|
306
|
-
- Failed signature verification throws before any write.
|
|
307
|
-
- Verification runs against the raw body bytes, not a re-serialized parse.
|
|
308
|
-
- Two different events with different keys both process.
|
|
309
|
-
- Key derivation follows `${event}:${id}:${updatedAt}` in the docs example.
|
|
310
|
-
**Verification:** suite green on the real runtime fixture.
|
|
311
|
-
|
|
312
|
-
### U7. Agent tool generator
|
|
313
|
-
|
|
314
|
-
**Goal:** LLM tools that are audited commands.
|
|
315
|
-
**Requirements:** R6, R10. **Dependencies:** U2 (list args derive from
|
|
316
|
-
pagination opts), U4 (mutation executors are the natural handlers).
|
|
317
|
-
**Files:** `src/agent-tools.ts` (new), `src/zod-table.ts` (switch `tools.update`/`tools.id`
|
|
318
|
-
masks from `zid` to `jsonSafeZid` so the masks are JSON-schema-safe as KTD2
|
|
319
|
-
assumes; compile-time Id types preserved), `src/index.ts`,
|
|
320
|
-
`package.json` (subpath `./agent-tools`), `test/agent-tools.test.ts`,
|
|
321
|
-
`src/docs/agent-tools.md` (new), `src/docs/llms.md`.
|
|
322
|
-
**Approach:** `createAgentTools(table, { create?, update?, get?, list? })`
|
|
323
|
-
mapping the existing `table.tools.*` masks to tool records `{ name,
|
|
324
|
-
description, args, handler }`: `args` are the masks (already `jsonSafeZid`
|
|
325
|
-
-safe), `description` derived (`Create a <tableName> record`, overridable),
|
|
326
|
-
`handler: (ctx, input) => executor(ctx, input)` wiring the caller-supplied
|
|
327
|
-
executors. Read contract: `get` uses the `tools.id`
|
|
328
|
-
mask with a caller-supplied read handler; `list` args derive from U2's
|
|
329
|
-
pagination opts schema with a caller-supplied query handler (reads do not
|
|
330
|
-
route through commands). Shape-compatible with `@convex-dev/agent`'s
|
|
331
|
-
`createTool` (checked against registry snapshot 2026-08-14; re-verify at
|
|
332
|
-
install time) — the host spreads the record into `createTool` or
|
|
333
|
-
ai-sdk `tool()`; the kit imports neither.
|
|
334
|
-
**Test scenarios:**
|
|
335
|
-
- Generated `args` for create equals `table.tools.insert` (mask reuse, no
|
|
336
|
-
drift).
|
|
337
|
-
- Handler invocation routes to the supplied executor with parsed input.
|
|
338
|
-
- Ids present as plain strings in JSON schema (jsonSafeZid behavior holds
|
|
339
|
-
through the mask).
|
|
340
|
-
- Only requested tool kinds are emitted; names are `<tableName>_<verb>`.
|
|
341
|
-
**Verification:** suite green; docs example type-checks against a stub
|
|
342
|
-
`createTool` signature in tests.
|
|
343
|
-
|
|
344
|
-
### U8. CHANGELOG + GitHub Releases + workflow hardening
|
|
345
|
-
|
|
346
|
-
**Goal:** release discipline switched on.
|
|
347
|
-
**Requirements:** R8. **Dependencies:** none (can land first).
|
|
348
|
-
**Files:** `CHANGELOG.md` (new; backfill 0.0.1–0.0.8 briefly, then 0.1.0),
|
|
349
|
-
`.github/workflows/release.yml` (release-notes step + updated action pins),
|
|
350
|
-
`README.md` (Releasing section), `ROADMAP.md` (check boxes as they complete).
|
|
351
|
-
**Approach:** Keep-a-Changelog format. After the publish step succeeds, a
|
|
352
|
-
step extracts the current tag's section from `CHANGELOG.md` and creates the
|
|
353
|
-
GitHub Release (`gh release create "$TAG" --notes-file`), using the
|
|
354
|
-
workflow's `GITHUB_TOKEN` (needs `contents: write` on the job — scope it to
|
|
355
|
-
the release job only). Bump `actions/checkout` and `actions/setup-node` pins
|
|
356
|
-
to current majors to clear the Node-20 deprecation annotation.
|
|
357
|
-
**Test scenarios:** `Test expectation: none — CI configuration; verified by
|
|
358
|
-
the 0.1.0 release run producing a GitHub Release with the changelog section
|
|
359
|
-
as notes.`
|
|
360
|
-
**Verification:** (1) the CHANGELOG-section extraction runs locally against
|
|
361
|
-
the backfilled file and yields the 0.1.0 section verbatim BEFORE tagging;
|
|
362
|
-
(2) the v0.1.0 tag run publishes to npm AND creates a GitHub Release with
|
|
363
|
-
correct notes. Backfill source of truth: `git log` + release history, not
|
|
364
|
-
memory.
|
|
365
|
-
|
|
366
|
-
### U9. Upgrade guide (the migration file) + docs pass
|
|
367
|
-
|
|
368
|
-
**Goal:** consumers on 0.0.x know the new way.
|
|
369
|
-
**Requirements:** R9. **Dependencies:** U1–U7 (documents them).
|
|
370
|
-
**Files:** `src/docs/upgrading.md` (new), `src/docs/llms.md` (exports map +
|
|
371
|
-
rules updates for all new modules), `README.md` (docs list + modules),
|
|
372
|
-
`src/docs/conventions.md` (where the new helpers fit the file anatomy).
|
|
373
|
-
**Approach:** `upgrading.md` is organized old way → new way per feature:
|
|
374
|
-
hand-written CRUD operations → `createCrudCommands`; hand-rolled transition
|
|
375
|
-
`if`s → `createStateMachine`; `.take()` loops → `paginate`; hand-rolled
|
|
376
|
-
limiter checks → `rateLimit` middleware; ad-hoc webhook dedup →
|
|
377
|
-
`createWebhookBoundary`; hand-written agent tools → `createAgentTools`;
|
|
378
|
-
untyped middleware ctx → typed extensions. Each entry: before-snippet,
|
|
379
|
-
after-snippet, and what stays unchanged. Also records the 0.1.0 semver
|
|
380
|
-
contract switch WITH the caveat (per review): the six new export surfaces
|
|
381
|
-
get final-name review during implementation, and the breaking window
|
|
382
|
-
formally closes at the follow-up consolidation release (name decision + API
|
|
383
|
-
audit) — CHANGELOG and upgrading.md both state this.
|
|
384
|
-
**Test scenarios:** `Test expectation: none — documentation; correctness is
|
|
385
|
-
enforced by U1–U7 examples being lifted from fixture code.`
|
|
386
|
-
**Verification:** every new export appears in `llms.md`'s tables; docs sync
|
|
387
|
-
ships the new pages in the tarball (dry-run check).
|
|
388
|
-
|
|
389
|
-
### U10. Release v0.1.0
|
|
390
|
-
|
|
391
|
-
**Goal:** ship it. **Requirements:** all. **Dependencies:** U1–U9.
|
|
392
|
-
**Files:** `package.json` (version via `npm version minor`).
|
|
393
|
-
**Approach:** full local verification (build → typecheck → test → tarball
|
|
394
|
-
dry-run), commit, `npm version minor`, push tag; watch the workflow publish
|
|
395
|
-
npm + GitHub Release.
|
|
396
|
-
**Test scenarios:** `Test expectation: none — release mechanics; gates are
|
|
397
|
-
the Verification Contract.`
|
|
398
|
-
**Verification:** registry shows 0.1.0 with provenance; GitHub Release
|
|
399
|
-
exists with changelog notes; a consumer install shows the new subpaths.
|
|
400
|
-
|
|
401
|
-
---
|
|
402
|
-
|
|
403
|
-
## Scope Boundaries
|
|
404
|
-
|
|
405
|
-
**In scope:** the ten units above.
|
|
406
|
-
|
|
407
|
-
### Deferred to Follow-Up Work
|
|
408
|
-
- Package-name decision + full export-name API audit (user decision;
|
|
409
|
-
remaining 0.1.0 roadmap items — do before or immediately after this ships,
|
|
410
|
-
since the upgrade guide will need a rename note if the name changes).
|
|
411
|
-
- `attw` + pack dry-run in CI (roadmap 0.1.0 CI-hardening item not blocking
|
|
412
|
-
the features; the pin bumps land in U8).
|
|
413
|
-
- Full TanStack-style middleware chain inference (per-middleware declared
|
|
414
|
-
extensions are the 0.1.0 contract; array-level inference is future work).
|
|
415
|
-
|
|
416
|
-
### Outside this product's identity
|
|
417
|
-
- Idempotency store, frontend bindings, multi-provider auth abstractions
|
|
418
|
-
(per `ROADMAP.md` out-of-scope section).
|
|
419
|
-
|
|
420
|
-
## Open Questions (deferred to implementation)
|
|
421
|
-
- ~~RLS-wrapped `paginate` support~~ RESOLVED during review: the wrapper
|
|
422
|
-
supports `paginate` (convex-helpers filter WrapQuery). Real caveat:
|
|
423
|
-
filtered pagination can return pages SHORTER than `numItems` when RLS
|
|
424
|
-
rejects rows — U2 tests cursor correctness under mid-page filtering and
|
|
425
|
-
the docs state the short-page contract.
|
|
426
|
-
- Exact `RateLimiter` status shape on rejection (`retryAfter` field name) —
|
|
427
|
-
read from the installed package at implementation time.
|
|
428
|
-
|
|
429
|
-
## Risks
|
|
430
|
-
- **Dependency drift:** `@convex-dev/rate-limiter` peer-depends on
|
|
431
|
-
`convex ^1.24.8` (compatible) but pulls a React peer — verify it stays a
|
|
432
|
-
server-only import path so consumers without React aren't warned. If
|
|
433
|
-
noisy, document or mark peer-optional in kit docs.
|
|
434
|
-
- **U4 API surface:** the CRUD factory is the most opinion-dense new API;
|
|
435
|
-
wrong defaults here are expensive post-0.1.0. Mitigation: fixture-first
|
|
436
|
-
design (write the consuming fixture before the factory), and the archive
|
|
437
|
-
operation stays soft-delete-only.
|
|
438
|
-
- **Release workflow token:** `gh release create` needs `contents: write`;
|
|
439
|
-
the job currently has `contents: read`. Scope the elevation to the release
|
|
440
|
-
job only.
|
|
441
|
-
|
|
442
|
-
## Verification Contract
|
|
443
|
-
- `bun run build` (includes bridge + publint), `bun run typecheck`,
|
|
444
|
-
`bun run test` all green.
|
|
445
|
-
- Tarball dry-run: dist + docs + `src/test.ts` only; all new docs pages
|
|
446
|
-
present; new subpaths resolve (publint).
|
|
447
|
-
- Tenancy suite still proves isolation with pagination added.
|
|
448
|
-
- Every new runtime export has ≥1 test and an `llms.md` row.
|
|
449
|
-
|
|
450
|
-
## Definition of Done
|
|
451
|
-
- All ten units complete; `cvx-kit@0.1.0` live on npm with provenance.
|
|
452
|
-
- GitHub Release v0.1.0 exists with CHANGELOG-derived notes.
|
|
453
|
-
- `ROADMAP.md` 0.1.0 feature checkboxes checked (consolidation items that
|
|
454
|
-
remain open — name decision, API audit — explicitly unchecked with a note).
|
|
455
|
-
|
|
456
|
-
## Sources & Research
|
|
457
|
-
- `ROADMAP.md` (origin), `docs/explain/roadmap-to-1.0.0.html`.
|
|
458
|
-
- Session context: this repo's full source (authored this session).
|
|
459
|
-
- Registry: `@convex-dev/rate-limiter@0.3.2` API (constructor, named limits,
|
|
460
|
-
`limit(ctx, name, { key })`); `@convex-dev/agent@0.6.4` peers/contract.
|
|
461
|
-
- convex-helpers: `convexToZod` (zod4), `paginationOptsValidator` (convex).
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: resolveOrganization Auth Hook - Plan
|
|
3
|
-
type: feat
|
|
4
|
-
date: 2026-08-21
|
|
5
|
-
artifact_contract: ce-unified-plan/v1
|
|
6
|
-
artifact_readiness: implementation-ready
|
|
7
|
-
product_contract_source: ce-plan-bootstrap
|
|
8
|
-
execution: code
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# resolveOrganization Auth Hook - Plan
|
|
12
|
-
|
|
13
|
-
## Goal Capsule
|
|
14
|
-
|
|
15
|
-
- **Objective:** Apps whose identity tokens carry no organization/role claims can use every cvx-kit auth constructor by resolving membership from their own database, and the capability ships documented in the next release.
|
|
16
|
-
- **Means:** An optional `resolveOrganization` hook on `AuthFunctionsConfig`, per the API shape in issue #8 (KTD1).
|
|
17
|
-
- **Authority:** This plan; issue #8 (`ImRLopezAG/cvx-kit#8`) for the API contract; existing fail-closed semantics in `src/auth.ts` for behavior precedent.
|
|
18
|
-
- **Stop conditions:** Stop if implementing the hook requires changing the tenancy/RLS engine (`src/tenancy.ts`) or breaking any existing config field — that contradicts R5 and needs a user decision.
|
|
19
|
-
- **Tail ownership:** Ends when the Definition of Done holds; releasing the version itself (`release:minor`) is the maintainer's call.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Product Contract
|
|
24
|
-
|
|
25
|
-
### Summary
|
|
26
|
-
|
|
27
|
-
Add an optional `resolveOrganization` hook to `createAuthFunctions` so identity providers without org JWT claims (custom credentials, magic codes, app-table memberships) can resolve organization and role from the database. Claim-based resolution stays the default; behavior is fully backwards compatible. Ships with tests, docs, and a changelog entry targeting the next minor version.
|
|
28
|
-
|
|
29
|
-
### Problem Frame
|
|
30
|
-
|
|
31
|
-
`authenticatedUser` in `src/auth.ts` resolves `organizationId` and `roleSlug` exclusively from JWT claims (`identity.organization.*`, `identity.org_id`, `identity.role`). Providers that keep membership in app tables cannot use the kit's auth constructors without stuffing claims at sign-in or bypassing the kit entirely — defeating centralized tenant resolution. Issue #8 reports this from a real migration and proposes the hook; the reporter offered a PR, so landing the agreed shape upstream also closes the issue.
|
|
32
|
-
|
|
33
|
-
### Requirements
|
|
34
|
-
|
|
35
|
-
- R1. `AuthFunctionsConfig` accepts an optional `resolveOrganization` hook called with `{ ctx, identity, user }` after the user is resolved, returning `Promise<{ organizationId: string; roleSlug: string } | null>`.
|
|
36
|
-
- R2. When the hook is configured, its result overrides claim-based resolution for `organizationId` and role; claim parsing is not consulted.
|
|
37
|
-
- R3. When the hook returns `null`, authentication fails with `FORBIDDEN`.
|
|
38
|
-
- R4. The hook's `roleSlug` passes through `mapRole`; unknown roles reject with `FORBIDDEN`, exactly as claim-derived roles do today.
|
|
39
|
-
- R5. When the hook is not configured, observable behavior is unchanged for hook-less configurations (backwards compatible), proven by the existing claim-based regression suite.
|
|
40
|
-
- R6. The hook composes with existing machinery: `tenant` derives from the hook-resolved organization (via `security.tenancy.resolve` when set), and `verifyMembership` for actions continues to run against the hook-resolved `organizationId`.
|
|
41
|
-
- R7. The capability is documented (`src/docs/auth.md`, `src/docs/llms.md`, the `README.md` example) and recorded in `CHANGELOG.md` under Unreleased for the next minor release.
|
|
42
|
-
- R8. The documented hook contract carries two resolver obligations, each covered by a test: the lookup must bind its result to the verified user (never a caller-influenced or non-unique attribute), and revoked, inactive, or expired memberships must resolve to `null` so fail-closed revocation holds on queries and mutations.
|
|
43
|
-
- R9. The hook is usable from all three function kinds: in action invocations the hook's `ctx` is an action context with no `ctx.db`, so the documented pattern (and the docs example) must branch to `ctx.runQuery` of an internal query there.
|
|
44
|
-
|
|
45
|
-
### Key Decisions
|
|
46
|
-
|
|
47
|
-
- **Fail closed on `null`** — a configured hook is the source of truth; falling back to claims would let a revoked membership pass on stale claims. (session-settled: user-directed — chosen over claims-fallback: confirmed at scoping) Governs R3.
|
|
48
|
-
- **Hook composes with, not replaces, `verifyMembership`** — actions keep their live re-verification pipeline unchanged. (session-settled: user-directed — chosen over hook-as-live-check: confirmed at scoping) Governs R6.
|
|
49
|
-
|
|
50
|
-
### Scope Boundaries
|
|
51
|
-
|
|
52
|
-
- No changes to the tenancy/RLS engine (`src/tenancy.ts`) or trigger machinery.
|
|
53
|
-
- No new auth providers or provider adapters.
|
|
54
|
-
|
|
55
|
-
#### Deferred to Follow-Up Work
|
|
56
|
-
|
|
57
|
-
- A convenience helper that builds `resolveOrganization` from a membership table + index name (only if consumer demand appears after release).
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Planning Contract
|
|
62
|
-
|
|
63
|
-
### Key Technical Decisions
|
|
64
|
-
|
|
65
|
-
- KTD1. **API shape follows issue #8 verbatim** — `resolveOrganization?: (input: { ctx, identity, user }) => Promise<{ organizationId, roleSlug } | null>`, typed against the existing `AnyAuthContext<DataModel>` and `UserIdentity`. Rationale: the shape already composes with `mapRole` and `verifyMembership`, and matching it lets the issue close without API renegotiation. (session-settled: user-approved — chosen over redesigning the signature: confirmed at scoping)
|
|
66
|
-
- KTD2. **Resolution happens inside `authenticatedUser`** so queries, mutations, AND actions all inherit it from one place; `actionInput`'s `verifyMembership` re-resolution runs afterward, unchanged. Rationale: `actionInput` calls `authenticatedUser` first, so no second integration point is needed. Constraint (R9): in actions the hook receives a `GenericActionCtx`, which has no `db` — a hook body doing `ctx.db.query(...)` will not typecheck against `AnyAuthContext` and would throw in actions, which KTD3's fail-closed catch masks as `FORBIDDEN`. The docs example must therefore branch (`'db' in ctx ? ctx.db... : ctx.runQuery(internal...)`) or use a `runQuery`-based lookup throughout.
|
|
67
|
-
- KTD3. **Hook errors fail closed with `FORBIDDEN`**, mirroring the existing `verifiedMembership` catch-and-reject pattern (`src/auth.ts`, `verifiedMembership`). Rationale: an erroring membership lookup must not authenticate anyone; consistency with the kit's only other injected-lookup precedent.
|
|
68
|
-
- KTD4. **The `UNAUTHENTICATED`-vs-`FORBIDDEN` boundary is preserved**: missing identity/user stays `UNAUTHENTICATED`; a resolved user without a usable org/role (hook `null`, unknown role) is `FORBIDDEN`. The existing missing-claim `UNAUTHENTICATED` path is untouched for hook-less configs.
|
|
69
|
-
|
|
70
|
-
### Assumptions
|
|
71
|
-
|
|
72
|
-
- Hook return of a non-null result with an empty-string `organizationId` is treated as a hook-author bug, not specially validated (same trust level as `verifyMembership`'s return today).
|
|
73
|
-
|
|
74
|
-
### High-Level Technical Design
|
|
75
|
-
|
|
76
|
-
Resolution order inside `authenticatedUser` after the hook lands:
|
|
77
|
-
|
|
78
|
-
```mermaid
|
|
79
|
-
flowchart TB
|
|
80
|
-
A[ctx.auth.getUserIdentity] -->|null| U1[UNAUTHENTICATED]
|
|
81
|
-
A --> B[getAuthUser]
|
|
82
|
-
B -->|null| U1
|
|
83
|
-
B --> C{resolveOrganization configured?}
|
|
84
|
-
C -->|yes| D[await hook with ctx, identity, user]
|
|
85
|
-
D -->|throws or null| F1[FORBIDDEN]
|
|
86
|
-
D -->|result| E[organizationId + roleSlug from hook]
|
|
87
|
-
C -->|no| G[claim parsing: identity.organization / org_id / role]
|
|
88
|
-
G -->|no org claim| U1
|
|
89
|
-
G --> E2[organizationId + roleSlug from claims]
|
|
90
|
-
E --> H[mapRole]
|
|
91
|
-
E2 --> H
|
|
92
|
-
H -->|null| F1
|
|
93
|
-
H --> I[freeze bundle; tenant = tenancy.resolve or organizationId]
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Actions then proceed through `actionInput` → `verifiedMembership` exactly as today, using the bundle's `organizationId` (now possibly hook-resolved).
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## Implementation Units
|
|
101
|
-
|
|
102
|
-
### U1. Add the `resolveOrganization` hook to `createAuthFunctions`
|
|
103
|
-
|
|
104
|
-
- **Goal:** Hook-configured apps resolve org/role from the database; unconfigured apps are unchanged.
|
|
105
|
-
- **Requirements:** R1, R2, R3, R4, R5, R6
|
|
106
|
-
- **Dependencies:** none
|
|
107
|
-
- **Files:** `src/auth.ts`
|
|
108
|
-
- **Approach:**
|
|
109
|
-
- Add the `resolveOrganization` field to `AuthFunctionsConfig` with the KTD1 signature and a doc comment matching the style of `verifyMembership`'s.
|
|
110
|
-
- In `authenticatedUser`, after the identity/user guard: when the hook is configured, call it in a try/catch; a throw or `null` result rejects with `FORBIDDEN` (KTD3); otherwise use its `organizationId`/`roleSlug` and skip claim parsing (KTD2, KTD4). The rest of the function (`mapRole`, freeze, tenant derivation) is shared by both branches.
|
|
111
|
-
- **Patterns to follow:** `verifiedMembership` in `src/auth.ts` for fail-closed error handling; existing doc-comment voice on `AuthFunctionsConfig` fields.
|
|
112
|
-
- **Test scenarios:** covered in U2 (same behavior surface).
|
|
113
|
-
- **Verification:** `bun run typecheck` passes; no existing test changes needed for hook-less configs.
|
|
114
|
-
|
|
115
|
-
### U2. Real-runtime tests for hook-based resolution
|
|
116
|
-
|
|
117
|
-
- **Goal:** The hook's contract (override, fail-closed, mapRole, composition) is proven on the real Convex runtime.
|
|
118
|
-
- **Requirements:** R2, R3, R4, R5, R6, R8, R9
|
|
119
|
-
- **Dependencies:** U1
|
|
120
|
-
- **Files:** `test/convex-auth.test.ts` (or a sibling `test/convex-auth-resolve-org.test.ts` if the fixture diverges), new or extended fixture under `test/fixture/` with a membership table and a `createAuthFunctions` config using `resolveOrganization`. Base identities in these tests carry **no** `org_id`/`role` claims; scenarios that test claim-override behavior create additional identities **with** claims as needed.
|
|
121
|
-
- **Approach:** Mirror the existing `convexTest(schema, modules)` harness and `withIdentity` style. Seed membership rows via a system/internal function or `t.run`.
|
|
122
|
-
- **Test scenarios:**
|
|
123
|
-
- Happy path: identity without org claims + membership row → query and mutation succeed; `ctx.actor.organizationId` matches the membership row.
|
|
124
|
-
- Hook returns `null` (no membership row) → `FORBIDDEN`.
|
|
125
|
-
- Hook returns an unknown `roleSlug` → `FORBIDDEN` (mapRole rejection).
|
|
126
|
-
- Hook configured but identity missing entirely → `UNAUTHENTICATED` (boundary preserved, KTD4).
|
|
127
|
-
- Hook throws → `FORBIDDEN` (fail closed, KTD3).
|
|
128
|
-
- Hook result overrides present-but-different claims: identity carries `org_id: 'org_claims'`, membership row says `org_db` → actor org is `org_db`.
|
|
129
|
-
- Claim parsing is fully bypassed (R2): identity carries malformed/unknown org and role claims plus a valid membership row → authentication succeeds on the hook-derived org and mapped role.
|
|
130
|
-
- Action path (R9): an `authAction` with the hook configured (a `runQuery`-based hook body) authenticates a claims-less identity successfully.
|
|
131
|
-
- Composition with `verifyMembership` (R6): with both configured, an action asserts `verifyMembership` receives the hook-resolved `organizationId` (not a claim value); a `verifyMembership` null/mismatch result rejects with `FORBIDDEN`.
|
|
132
|
-
- Identity binding (R8): a negative cross-user test — user A's identity can never receive user B's membership row or organization.
|
|
133
|
-
- Revocation (R8): a revoked/inactive membership row resolves to `null` in the example predicate → `FORBIDDEN` on queries and mutations.
|
|
134
|
-
- Composition with tenancy (mandatory, end-to-end): with `security.tenancy` configured, rows written under two different hook-resolved tenants are mutually invisible through the auth constructors' RLS-wrapped database (a direct `ctx.tenant` assertion is not an acceptable substitute).
|
|
135
|
-
- Hook-less missing-claim regression (R5): a valid identity with a resolved user but no org/role claims, and no hook configured → `UNAUTHENTICATED`, exactly as today.
|
|
136
|
-
- Regression: existing claim-based suite passes unmodified (R5).
|
|
137
|
-
- **Verification:** `bun run test` green.
|
|
138
|
-
|
|
139
|
-
### U3. Documentation
|
|
140
|
-
|
|
141
|
-
- **Goal:** A consumer with a claims-less provider can adopt the hook from the docs alone.
|
|
142
|
-
- **Requirements:** R7, R8, R9
|
|
143
|
-
- **Dependencies:** U1
|
|
144
|
-
- **Files:** `src/docs/auth.md`, `src/docs/llms.md` (edit sources — `docs/` is generated by `docs:sync`, never edit it directly), `README.md` (extend its existing `createAuthFunctions` example with the new field).
|
|
145
|
-
- **Approach:** In `auth.md`, add a section alongside "Queries/mutations trust the JWT; actions re-verify": when to use the hook (no org claims in tokens), resolution order (hook overrides claims; `null`/error → `FORBIDDEN`), a `companyUsers`-style example handler that is action-safe per R9 (branching to `ctx.runQuery` where `ctx.db` is absent), the resolver obligations per R8 (bind the lookup to the verified user; resolve revoked/inactive/expired memberships to `null`), the composition notes (mapRole still applies; tenant derives from the hook's org; `verifyMembership` unchanged), and the cost note (one DB read per authed call vs. free claims). Add the field to `llms.md`'s config surface.
|
|
146
|
-
- **Test scenarios:** Test expectation: none — documentation-only unit; U4's verification covers `docs:sync` freshness.
|
|
147
|
-
- **Verification:** Rendered docs read coherently; example compiles conceptually against the U1 signature.
|
|
148
|
-
|
|
149
|
-
### U4. Changelog and release readiness
|
|
150
|
-
|
|
151
|
-
- **Goal:** The feature is recorded for the next minor release and issue #8 can be closed by the shipping PR.
|
|
152
|
-
- **Requirements:** R7
|
|
153
|
-
- **Dependencies:** U1, U2, U3
|
|
154
|
-
- **Files:** `CHANGELOG.md`
|
|
155
|
-
- **Approach:** Add an `### Added` entry under `## [Unreleased]` describing the hook and its fail-closed semantics, in the file's existing voice. The PR description references "Closes #8". Actual version bump stays with the maintainer via `release:minor`.
|
|
156
|
-
- **Test scenarios:** Test expectation: none — release metadata only.
|
|
157
|
-
- **Verification:** `bun run prepublishOnly` (build + typecheck + test + docs:sync) succeeds locally.
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## Verification Contract
|
|
162
|
-
|
|
163
|
-
| Gate | Command | Applies to |
|
|
164
|
-
|---|---|---|
|
|
165
|
-
| Types | `bun run typecheck` | U1 |
|
|
166
|
-
| Runtime test suite | `bun run test` | U1, U2 |
|
|
167
|
-
| Full release gate | `bun run prepublishOnly` | U4 (final) |
|
|
168
|
-
|
|
169
|
-
All new tests run on the real Convex runtime via `convex-test`, matching the existing suite; no mocked auth paths.
|
|
170
|
-
|
|
171
|
-
## Definition of Done
|
|
172
|
-
|
|
173
|
-
- All four units complete; R1–R9 each traceable to landed code, tests, or docs.
|
|
174
|
-
- Existing test suite passes unmodified (R5 proof).
|
|
175
|
-
- `bun run prepublishOnly` green.
|
|
176
|
-
- `CHANGELOG.md` Unreleased section carries the entry; docs sources updated (not the generated `docs/` copies).
|
|
177
|
-
- No dead-end or experimental code left in the diff.
|
|
178
|
-
- Follow-up: shipping PR references issue #8 so it closes on merge.
|