Repository navigation
v0.17.0
@kitcn/resend
Published @kitcn/resend@0.17.0.
kitcn
Minor Changes
-
#322
b80d734Thanks @MikeyZhang75! - ## Breaking changeskitcn addno longer replaces scaffold files you have edited. Files that still
match the scaffold are upgraded as before; edited files are kept, listed under
Refused files, and the command exits1because the plugin is only partially
installed. Pass--overwritefor the previous behavior.
# Before — edits to crpc.ts were replaced, exit 0 kitcn add auth --yes # After — edits are kept and the run fails until you choose kitcn add auth --yes --overwrite
kitcn add --jsonsplits the oldskippedlist in two.skippednow means
"already up to date", refused files move torefused, andcompletereports
whether everything was applied.
Patches
- Fix
kitcn codegenleaving a hidden parse snapshot behind when a Convex module
fails to load, which kept reporting the old error after the real file was
fixed. Codegen evaluates modules in memory instead of mirroring them to a
sibling file, so it no longer writes to — or deletes from — your Convex
directory, and import-time stack traces now point at the real module rather
than a temporary path. A file of your own ending in.kitcn-parse.tsis left
untouched. - Fix plugin registration landing inside a comment or string that happens to
mentiondefineSchema(, which recorded the plugin as installed while its
tables and relations never reached the schema. - Fix
kitcn add authdeleting a table's index callback when it was written as a
block-bodied arrow or a named function. The callback is now preserved while
managed fields are merged. - Fix schema patching merging value imports into a type-only
kitcn/ormimport,
which produced duplicate identifiers, and skipping the import entirely when
onlyimport * as orm from 'kitcn/orm'was present.
-
#320
799dc4fThanks @MikeyZhang75! - ## Breaking changes-
Improve Convex and HTTP middleware to wrap the whole procedure:
next()
resolves after the handler runs, so timing, error reporting, and cleanup
around it observe the handler, and handler errors propagate through every
wrappingcatch. Actxchanged on the return path no longer reaches the
handler — pass it tonext()instead.// Before — logged 0ms, never saw handler errors, and this ctx was ignored .use(async ({ ctx, next }) => { const start = Date.now(); const result = await next({ ctx }); console.log(`${Date.now() - start}ms`); return { ...result, ctx: { ...ctx, tenant } }; }) // After — times the handler, sees its errors, and passes ctx forward .use(async ({ ctx, next }) => { const start = Date.now(); try { return await next({ ctx: { ...ctx, tenant } }); } finally { console.log(`${Date.now() - start}ms`); } })
-
Improve chained
.input()to apply each schema on its own instead of
flattening them into one shape, so object-level rules run. A key declared by
more than one schema is validated only by the last schema to declare it, even
when an earlier schema carries object-level rules.// Before — the object-level rule was dropped and both fields reached the handler .input(z.object({ password: z.string(), confirm: z.string() })) // After — mismatched values are rejected before the handler runs .input( z .object({ password: z.string(), confirm: z.string() }) .refine((v) => v.password === v.confirm) )
Patches
- Fix
.input()schemas running twice per request, which made field transforms
apply to their own output —z.string().transform(s => s.length)threw on
valid input andz.number().transform(n => n * 2)doubled twice. Transforms
and refinements now run exactly once. - Fix
next({ input })being dropped for every middleware after the first, so
input enrichment placed after an auth middleware no longer silently no-ops. - Fix HTTP routes returning a retryable
500for errors raised by a procedure
they called through a caller.NOT_FOUND,FORBIDDEN, and other codes now
keep their status and message. - Fix HTTP routes returning
500for the twelve error codes missing from the
route status map, includingPAYLOAD_TOO_LARGE(413),
UNSUPPORTED_MEDIA_TYPE(415), andPRECONDITION_FAILED(412). - Fix a malformed or empty JSON body returning
500instead of400,
including when HTTP middleware reads it throughgetRawInput().
-
-
#317
b765f01Thanks @MikeyZhang75! - ## Breaking changesorderByon a field that no index leads now sorts after reading, and under
cursor pagination it raises instead of returning a page ordered by the wrong
column. An index merely containing the field no longer counts: Convex walks an
index in full key order, soon(type, numLikes)orders bytypefirst. Add an
index led by the sort field, or read without a cursor.
// Before: paged, silently ordered by `type` index("numLikesAndType").on(t.type, t.numLikes); await db.query.posts.findMany({ orderBy: { numLikes: "desc" }, cursor: null, limit: 20, }); // After: add an index the sort field leads index("numLikesAndType").on(t.type, t.numLikes); index("by_num_likes").on(t.numLikes); await db.query.posts.findMany({ orderBy: { numLikes: "desc" }, cursor: null, limit: 20, });
.withIndex(name, range)now wins over the index awhereobject would have
selected, so awherethe pinned index cannot serve becomes a scan the caller
has to bound. Under cursor pagination that combination now asks formaxScan
instead of quietly reading through a different index.
// Before: scanned `by_status` and returned rows from every city await db.query.users .withIndex("by_city", (q) => q.eq("cityId", cityId)) .findMany({ where: { status: "active" }, cursor: null, limit: 20 }); // After: bound the scan await db.query.users .withIndex("by_city", (q) => q.eq("cityId", cityId)) .findMany({ where: { status: "active" }, cursor: null, limit: 20, maxScan: 500, });
Patches
- Fix
.withIndex(name, range)being ignored whenever thewhereobject also
matched another index. The index and its bounds you asked for are now the ones
scanned — including under cursor pagination — so a range used to scope a query,
a tenant id for instance, can no longer be dropped and return rows outside it. - Fix
where: { field: { in: [...] } }combined withorderByandlimit
returning an arbitrary slice — usually the oldest rows — instead of the
requested window. - Fix
like,ilike,notLike, andnotIlikematching nothing when the
pattern has a wildcard anywhere but the ends.%now matches any run of
characters and_matches exactly one Unicode character, at any position. - Fix
eq,ne,in, andnotInnever matching array or object columns.
Values are compared by content, in queries and inupdate/deletefilters. - Fix
select()returning raw rows for awhere: { id }lookup, which silently
skippedmap,filter,flatMap, anddistinct.pageByKeywith the same
wherealso returns its page shape instead of a bare array. Awhereonid
orid: { in: [...] }reads those rows by key rather than scanning the table
for them, soselect()costs one read per id however large the table is.
Cursor pagination rejectsid: { in: [...] }withmaxScan, because sorting
arbitrary IDs by creation time requires reading the complete list first. - Apply RLS to source rows before any
select()pipeline callback runs, so a
mapper or flat-map stage cannot inspect or project a forbidden document. - Fix
flatMap'slimitcounting rows excluded by itswhere, which returned
fewer children than asked for and often none. The limit now counts matching
children, stays stable across pages instead of yielding a fresh batch per page,
and reads each child once within themaxScanbudget. It also stops on the
last child it can return instead of reading one past it, so a smalllimit
across many parents no longer spends reads no page can show. Exhausted and
missing optional relations advance cursors without duplicates or loops. - Fix a relation
limitcombined with a relationwherereturning too few
children — none when enough non-matching children sorted first. The limit now
counts matching children. Without an explicit relationorderBy, the scan
also stops after the requested visible rows, including rows filtered by RLS.
-
#316
59b80d0Thanks @MikeyZhang75! - ## Breaking changes- Require
rls.roleResolverfor policies scoped withto. A role-scoped policy
previously applied to every caller when no resolver was configured; it now
throwsRLS_ROLE_RESOLVER_REQUIRED. Queries and mutations check this for
every table they touch before reading rows, so the error depends only on
configuration and not on whether the table holds rows. SQL pseudo-roles
(public,current_user,current_role,session_user) apply to everyone
and still need no resolver.
// Before const ormDb = orm.db(ctx, { rls: { ctx } }); // After const ormDb = orm.db(ctx, { rls: { ctx, roleResolver: (ctx) => ctx.roles ?? [] }, });
- Deny RLS policy comparisons against a missing value, following SQL null
semantics. A policy written aseq(column, null)now denies instead of
matching explicitly-null columns, and an unauthenticated caller no longer
matches rows whose owner column was never set. UseisNullto match absent
or null columns.
// Before rlsPolicy("read_unassigned", { for: "select", using: (ctx, t) => eq(t.ownerId, null), }); // After rlsPolicy("read_unassigned", { for: "select", using: (ctx, t) => isNull(t.ownerId), });
Patches
- Fix many-to-many relations ignoring the junction table's RLS policies, which
revealed which rows other users were linked to. Loading a relation withwith
now enforces the junction table's policies alongside the related table's.
- Require
-
#319
2aee478Thanks @MikeyZhang75! - ## Breaking changes-
Enforce the configured total budget across all
shards.// 20 per minute in total, spread over 4 shards Ratelimit.fixedWindow(20, "1 m", { shards: 4 });
-
Preserve configured
maxReservedheadroom across sharded limiters. -
Reject limiter budgets that leave any shard with less than one usable token.
-
Reject non-positive, non-finite, or unservable dynamic limit overrides.
-
Evaluate the requested
count/rateincheck()against the tokens already
spent. It previously evaluated nothing and returnedsuccess: truefor every
caller, so a pre-flight gate now reportssuccess: falsewhere it always said
"allowed".// Before: always true, even for an exhausted identifier const gate = await limiter.check(userId, { count: 5 }); // After: matches what limit() would decide, without consuming tokens const gate = await limiter.check(userId, { count: 5 }); if (!gate.success) return { retryAt: gate.reset };
Features
- Add
snapshotToStateto convert agetValue()snapshot into the state shape
calculateRatelimit()expects. Snapshots retain the full projected state,
including sliding-window current and previous counts, so later projections
preserve boundary decay. - Add
remainingRawtocalculateRatelimit()results for the exact token
balance, including the negative value when a request overdraws.
Patches
- Fix
getRemaining()inverting sliding-window quotas. An identifier with no
traffic reportedremaining: 0, and quota banners orX-RateLimit-Remaining
headers showed the opposite of the truth. - Fix
resetUsedTokens()leaving an identifier blocked by the ephemeral cache,
so an admin quota reset silently failed for the rest of the window. - Key the ephemeral block cache per shard. One exhausted shard used to block the
identifier outright, stranding every other shard's tokens until the window
reset and enforcing well under the configured limit. - Sum every shard in
getRemaining()rather than extrapolating the fullest one.
A half-drained sharded limiter reported its full budget as still available. - Improve shard selection to compare exact token balances, so sharded limiters
spread load onto the emptier shard instead of tying on rounded counts. - Retry the remaining shards when the preferred candidates are exhausted, so
routing cannot deny a request while another shard can still serve it. - Preserve whole-request capacity for fractional budgets by dealing the whole
portion and keeping the fractional remainder on one shard. - Scale fixed-window snapshots and response balances by
capacityrather than
the refilllimit, so burst configurations report valid remaining tokens. - Scope ephemeral blocks by shard, requested count, and reservation mode, so a
failed large or ordinary request does not hide tokens from a smaller or
reserved request, and include cached shards when reporting the earliest
global retry time. - Prune expired ephemeral block variants when recording a new block.
- Allocate token-bucket refill rates in proportion to shard capacity, preserving
the full configured refill when capacity shares are uneven. - Compute reserved-request retry times against
maxReservedheadroom rather
than the non-reserved zero-debt threshold. - Evaluate sampled shard states at one common read timestamp, avoid refilling
their aggregate again, and sum each shard's independently usable whole tokens
ingetRemaining()without netting debt or fractions across isolated shards. - Preserve sampled per-shard state in snapshots so all-shard projections retain
independent capacity saturation and sliding-window decay. - Read each candidate shard set concurrently, including exhaustion fallbacks.
- Reject requests that exceed every shard's capacity and reservation headroom
withreason: "requestTooLarge"and no retry deadline or shard reads. - Exclude permanently undersized shards from retry deadlines and invalidate
local snapshot and block-decision caches when dynamic limits change. - Preserve permanent oversized denials through all-shard snapshots and React
projections without scheduling an infinite retry timer. - Apply per-shard capacity guards to partial snapshots while retaining uncapped
reservation headroom whenmaxReservedis omitted. - Reject negative, non-finite
maxReservedvalues before sharding or retry
calculation. - Normalize shard-local algorithms, enforce per-shard capacity for fresh
projections, and generation-guard cache writes across dynamic updates. - Skip infinite block-cache entries and retain at most 32 finite variants per
identifier.
-
Patch Changes
-
#318
eddfc08Thanks @MikeyZhang75! - ## Patches- Fix auth queries that filter by
idresolving records from the wrong model,
which let a lookup, update, or delete for one model read, patch, or destroy a
record belonging to another. IDs are now checked against the model being
queried, and IDs that belong elsewhere or are malformed resolve to "not
found" instead of a foreign record or an error. - Fix
findManyandcountreturning duplicated records past the first page,
which also inflated counts used for membership, role, and API key limits. - Fix mixed
AND/ORfilters infindManyandcountreturning records the
filter excluded, such as rows outside the requested organization. Mixed
filters are now rejected, matchingupdateManyanddeleteMany. - Fix
not_infilters returning no match when the matching record was not
among the first records scanned, which made those updates fail and those
deletes silently do nothing. - Fix
auth.jwtCache: falsein the Next.js integration disabling
authentication instead of just the JWT cookie cache, which made every server
request anonymous.
- Fix auth queries that filter by
-
#321
9a7feabThanks @MikeyZhang75! - ## Patches- Fix signed-in users being signed out when their name or email contains
non-ASCII characters. - Fix server callers re-running a failed mutation or action after a
non-authorization error, which could charge a card or write a row twice. - Fix results of auth-scoped actions surviving sign-out and being served to the
next user in the same tab. - Fix
skipUnauthbeing ignored on queries and action queries, so backend
authorization errors resolve tonullas documented instead of surfacing as
query errors. - Fix
Datevalues in query args crashing the render. - Fix a function-form
enabledbeing ignored on queries, action queries, and
infinite queries, which ran queries the caller had disabled. - Fix mutating a query args object in place returning another component's args
for a previously used key, which subscribed to the wrong Convex query. - Fix RSC prefetch of
crpc.http.*building a different URL than the browser
client forparamsandsearchParams, so prefetched data now hydrates
instead of being refetched.
- Fix signed-in users being signed out when their name or email contains