
Security News
arXiv Is Rate Limiting Authors Following a Flood of AI Slop Submissions
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.
@ultimat3/action
Advanced tools
The action primitive: one declaration projected to route, OpenAPI, client, MCP tool, job handle, tests
One declaration → six artifacts. Five are projected here; the MCP tool is @ultimat3/mcp's
(tier 4), from the same declaration.
| # | Artifact | Reach it with | Guarantees |
|---|---|---|---|
| 1 | HTTP route POST /api/<resource>/<verb> | toRoute(publishPost) — the server mounts it | policy + validation + idempotency + invalidation, non-optional |
| 2 | OpenAPI 3.1 operation + document | publishPost.openapi() / buildOpenApi() | byte-stable output, diffed by x verify |
| 3 | Typed RPC client | publishPost.client({ baseUrl }) / rpc<Api['actions']>() | server typo = compile error in Solid |
| 4 | MCP tool | toolFrom(publishPost) from @ultimat3/mcp — what tools/list serves | identical policy evaluation to the route: it calls this package's invoke with surface: 'mcp' |
| 5 | Job handle | publishPost.job() | enqueue durable work, no rewrite |
| 6 | Contract tests | publishPost.contract() | garbage rejected, anonymous denied, spec present |
An action carries its own projections, so app code never reaches through .def and
never imports a projection function:
publishPost.input // the declared input schema
publishPost.output // the declared output schema
publishPost.policy // the one policy object
publishPost.mcp // { expose, description }, as declared
await publishPost.as(actor, { postId }) // run as someone, one execution path
publishPost.openapi() // OpenAPI operation
publishPost.client({ baseUrl }) // typed RPC method
publishPost.job() // durable-work handle
publishPost.contract() // the three generated assertions
There is no .tool() (As of 25.0.0): it was a second MCP projection built a tier below the
one @ultimat3/mcp serves, and the two disagreed. The tool an agent is shown is
toolFrom(publishPost), and its every call lands in this package's invoke — the one
policy object, so an MCP call cannot reach a different authz path. .as() keeps the
surrounding context whole and swaps only the actor: impersonation, not a second context.
t is re-exported here — the same object @ultimat3/schema exports, so an action file
imports one package for the primitive and its schemas, never two.
import { action, t } from '@ultimat3/action';
export const publishPost = action({
input: t.object({ postId: t.uuid, orgId: t.uuid, notify: t.boolean.default(true) }),
output: PostView,
policy: can('post:publish', ({ input, actor }) => ownsPost(actor, input.postId)),
cache: { invalidates: [tag.post, tag.feed] },
mcp: { expose: true, description: 'Publish a draft post' },
idempotent: true,
async handle({ input, ctx }) {
const post = await ctx.posts.publish(input.postId);
if (input.notify) await notifySubscribers.enqueue({ postId: post.id, orgId: input.orgId });
return post;
},
});
apps/web/api/index.ts is the whole API surface. Importing it IS the boot.
import { defineApi } from '@ultimat3/action';
import * as postActions from '../app/posts/actions';
import * as postMutators from '../app/posts/mutator';
import * as postQueries from '../app/posts/live';
export const api = defineApi({
actions: [postActions],
mutators: [postMutators],
queries: [postQueries],
});
export type Api = typeof api;
Six keys, all optional:
| Key | Goes to | Why |
|---|---|---|
actions | the action registry | the primitive |
mutators | the action registry | a mutator IS an action, on the same authz path |
llm | the action registry | llm() returns an action, not a ninth primitive |
queries | @ultimat3/query's registry, via core's registrar table | query is on this tier, so importing it here would be a build error |
jobs | @ultimat3/jobs' registry, the same way | the export name becomes the durable queue key — a job row names the handle, not a counter |
tasks | @ultimat3/jobs' registry, the same way | handing a task over is what names its cron after its export |
Jobs register before tasks: a task's descriptor lists the jobs it enqueues by name, so the
other order would read the queue keys one boot step before they were assigned. Api carries all
four maps back — api.actions, api.queries, api.jobs, api.tasks — keyed by the name
registration stamped.
Names come from export names — that is what makes the path, the tool name and the
OpenAPI operationId derivable everywhere without a second declaration. Registration
stamps the name onto the action the module exported, so the binding you imported is the
one that projects; a projection attempted before boot is X_ACTION_UNREGISTERED. Two
features exporting one name collide with X_ACTION_DUPLICATE rather than merging, and two
names deriving one route collide with X_ACTION_PATH_DUPLICATE — pluralize leaves a trailing
s alone, so archiveOrder and archiveOrders are two exports and one POST /api/orders/archive.
registerActions is what defineApi composes for the three action-shaped keys; the other three
go through core's primitiveRegistrar(kind), because this package may not import @ultimat3/query
or @ultimat3/jobs sideways. An app calling either directly is a second path.
rpcimport { rpc } from '@ultimat3/action';
import type { Api } from '../api';
export const client = rpc<Api['actions']>({ baseUrl: '/' });
Api['actions'] is the merged module type, so client.publishPost is typed from the
declaration with no codegen step — at any app size: client-scale-pins.ts compiles the idiom over
300 actions in 100 modules on every typecheck (the limit was 47 modules in one list, #534).
x new writes this file as apps/web/shared/browser-client.ts. Api is imported as a type
only, which is what keeps a page's module graph free of any edge to a feature's implementation.
Every call dispatches through @ultimat3/core's clientTransport — the one browser HTTP function.
It owns credentials, the JSON body, the Idempotency-Key header, the principal fence and the
record envelope. The method still resolves to the action's output and nothing else.
An action whose output: references an entity row (posts.$schema, at any depth — inside an
object, an array, .nullable()) answers { data, records } with x-ultimate-records: 1, and the
transport adopts records into the page's one record store on the way past. There is no
records: option: the envelope is derived from the schema, never declared. It is decided per
action, from the schema — an output that only may carry a row is enveloped even when this call
returned none, so one operation has one wire shape. Every other action's body is byte-identical to
what it was before the envelope existed, and its OpenAPI operation is unchanged; an enveloped one
documents data, records and removed, and the header, generated from the declaration.
clientFlight, opt-inSame object as @ultimat3/query's, installed the same way (rpc({ baseUrl, flight })), and the
write half is the half made of refusals:
| Rule | Why |
|---|---|
| a mutation never joins another mutation | client.ts never calls flight.keyFor, so there is no dedup path to reach; two writes are two writes |
| a fence bump never aborts a write | closing the socket does not un-commit it, it only destroys the one chance this caller had of learning whether it landed. The caller still gets X_SUPERSEDED — the answer is retired, the request is not |
retry is honoured only alongside an idempotencyKey | a retried POST without one is a second write. Without a key the call is narrowed to a single attempt, silently and by construction |
the same Idempotency-Key rides every attempt | that is what makes the retry a retry rather than a duplicate |
declare const api: { charge: (input: { orderId: string }, options?: {
idempotencyKey?: string; retry?: { attempts: number };
}) => Promise<unknown> };
declare const orderId: string;
await api.charge({ orderId }, { idempotencyKey: `charge:${orderId}`, retry: { attempts: 3 } });
clientFlight is @ultimat3/core's, and imported from there — import { clientFlight } from '@ultimat3/core'. It shipped as a byte-identical copy here and in
@ultimat3/query (both tier 3, neither may import the other); the copies are gone, and since 25.0.0
so are the re-exports: one value, one import path (X_HELPER_COPY, bun run flight-copies). The
types ClientFlight and ClientRetry stay re-exported, because rpc's options name them.
Importing rpc alone from this package is 19,671 B minified for the browser; adding
clientFlight (measured through this barrel, before 25.0.0 moved the import to core) is 25,954 B (As of 2026-10-01; CLAUDE.md carries the before/after and
what the delta is). ClientFlight is a TYPE inside client.ts and never a value, which is what
keeps the second number off the first caller's bill.
One rule per app, declared once — defineApi({ http: { pathStyle } }) — plus a per-action pin.
pathStyle | publishPost | signIn | health | viewCustomer360 |
|---|---|---|---|---|
'resource' (default) | /api/posts/publish | /api/ins/sign | /api/healths/invoke | /api/customer360s/view |
'readable' | /api/publish-post | /api/sign-in | /api/health | /api/view-customer360 |
'resource' guesses a noun from the words after the first and pluralizes it — right for
verbNoun, ungrammatical otherwise. It stays the default: changing it moves every URL an app
serves, so an app opts in.'readable' guesses nothing: /api/<kebab-cased export name>, the rule /_x/query/<kebab>
follows for reads. The OpenAPI tag becomes the resource the POLICY names (can('cases:create')
→ cases), never a noun parsed from In.http: { path } pins one action's URL whatever the style — for a webhook a vendor holds:
action({ …, http: { path: '/api/webhooks/wompi' } }). Static, lowercase, no params, never
/_x (X_ACTION_HTTP_PATH_INVALID). A pin colliding with another path is
X_ACTION_PATH_DUPLICATE at boot.toRoute, toOpenApiOperation, describeAction,
derivePath(name) (pin-aware) and actionHttpPath(action | name). A pinned action is called
through action.client(), which holds the pin.<meta name="ultimate-path-style">) and rpc({ baseUrl: '' }) derives under it — as do
useMutation and the outbox replay. 'resource' writes no tag.rpc({ baseUrl, pathStyle: 'readable' }) in a script or
another service. The wrong one is answered by the server — 404 X_CONTRACT_DRIFT, naming the
style it serves and the path the action lives at — never a bare X_ROUTE_NOT_FOUND
(explainActionPathMiss, wired as @ultimat3/http's hooks.explainMiss).apps/web/api/index.ts before any other module, so a page or admin view already sees the declared
style. A module the index itself imports runs before its defineApi call: a module-level
const SIGN_OUT = derivePath('signOut').path there captures the default style, and the
declaration refuses the switch with X_ACTION_PATH_DERIVED_EARLY, naming it. Derive inside the
component or function instead.import { defineApi } from '@ultimat3/action';
import type { BearerResolver } from '@ultimat3/http';
declare const resolveToken: BearerResolver; // the app's MCP token resolver
declare const MCP_SCOPES: Readonly<Record<string, readonly string[]>>;
export const api = defineApi({
// actions, queries, jobs, tasks — as always
http: {
pathStyle: 'readable',
// A second door onto a cut of the SAME routes: Authorization: Bearer only (a session cookie
// authenticates nothing there), 404 for a primitive outside the token's scopes, and a
// per-token allowance answered with RateLimit-* and 429 + Retry-After.
mounts: [{
prefix: '/v1',
scopes: MCP_SCOPES, // scope -> [primitive names]; the map defineAppMcp takes
resolveToken, // the MCP resolver: token -> { actor, scopes } | null
rateLimit: { limit: 120, windowMs: 60_000 },
openapi: 'openapi.v1.json', // x manifest writes only the cut here; x verify checks it —
// each operation `security: [{ bearer: ['<its scope>'] }]`,
// the bearer scheme lists every scope (x-ultimate.scopes)
}],
},
// Declaring it (any key) makes openapi.json COMPLETE: info, servers, securitySchemes
// (cookie; bearer when a mount exists), 401 + 429 (Retry-After) on every authenticated
// operation, RateLimit-* on rate-limited ones. Absent, the bytes are what they were.
openapi: {
title: 'Notificado API',
version: '1.0.0',
servers: [{ url: 'https://www.notificado.co' }],
sessionCookie: 'session',
},
});
toPostBinding(action, path) is the same projection bound to a PAGE's URL for POST, the URL's
query merged over the posted fields (query wins): what defineRoute({ post: '<action>' }) mounts,
for an RFC 8058 one-click unsubscribe.
The MCP tool name is not derived at all — it is the export name verbatim, because
that is what defineAppMcp's scopes: and a tools/call have to spell.
Under the default style:
| Action | Route | MCP tool |
|---|---|---|
publishPost | POST /api/posts/publish | publishPost |
updateUserProfile | POST /api/user-profiles/update | updateUserProfile |
likePost | POST /api/posts/like | likePost |
checkout (single word) | POST /api/checkouts/invoke | checkout |
One name, three surfaces — openapi.json's x-ultimate.mcpTool,
describeAction().mcp.tool (what x actions show --json, x actions list --json, the
actions.describe dev MCP tool and the /_x Routes panel show) and the catalog @ultimat3/mcp
serves. It was two until 2026-08: a toToolName() here snake_cased the published ones to
publish_post while the server answered only publishPost, so an agent that read the published
contract called a tool that does not exist. toToolName is deleted, not deprecated — a second
derivation is a second name. mcp-surface.test.ts here and @ultimat3/mcp's
cross-surface.test.ts are what keep it that way.
x.manifest.json is not one of the four: ActionFact.mcp is { expose, description? }, so
the manifest never carried a tool name and was never wrong about one.
invoke() is the only execution path: policy's actor half → parse input → evaluate
policy → handle → parse output. HTTP, MCP, jobs and direct server calls differ only in the
surface they hand to enforce() from @ultimat3/policy, which selects how a
denial renders (problem+json / tool error / failed job) — never whether authz runs.
Enforced structurally, not by convention: the declaration is held in a private
store inside invoke.ts, so handle is reachable from nowhere else. An action has
no .def. A second authz path cannot be written without deleting that store.
| Stage | Failure |
|---|---|
policy, actor half (guardActionBeforeInput) | X_UNAUTHENTICATED / X_FORBIDDEN for a caller the policy refuses whatever the input — never a 400 describing the schema of an operation they may not call |
| parse input | X_INPUT_INVALID |
| evaluate policy | the policy's own code — X_UNAUTHENTICATED (401), X_FORBIDDEN (403) |
| handle | whatever the handler throws |
| parse output | X_OUTPUT_INVALID — and fields the schema never declared are dropped |
Who before what (As of 2026-09). The actor half is the part of the policy that reads no
input: can(p) with no predicate, allow(), deny(), their combinators, and can(p, pred) for
an actor without p. It is decided before the parse — decideBeforeInput in @ultimat3/policy,
exact rather than a heuristic — so can('refund:issue') answers a customer's {} with 403, and a
staff caller's {} with the 400 naming each missing field. A predicate over input or row still
runs after the parse, on the parsed value. Same on HTTP, MCP (including the invalid-args
path) and .as()/jobs. An audited action's denied attempt still records the parsed input.
cache: { invalidates } fans out after the handler commits, so it never fails it: a
fan-out that refuses — an undeclared tag, X_CACHE_TAG_UNKNOWN — is one
action.invalidate.failed log line and the entries expire by TTL. A replayed idempotent
call busts nothing; the first call already did.
After the COMMIT, not after the handler (As of 2026-10-02). An action invoked inside a
withTransaction — an app's own unit of work, or another action's — hands its bust to the ROOT
transaction's onCommit: it fires once the commit is answered and never for a rollback or an
aborted transaction (X_DB_TRANSACTION_ABORTED). Busting at the handler's return let a
concurrent read refill the cache from the pre-commit rows, which then stayed for the TTL. With no
transaction open the bust runs when the handler returns, as it always did.
Registering an action without policy: throws X_ACTION_POLICY_MISSING; there is
no bypass flag. A look-alike that never came out of action() is X_ACTION_FOREIGN.
mutator() is built on top of action(). A mutator IS an action, so it gets every
projection an action has; it adds local(tx, input) for the optimistic write and a conflict
strategy for the rebase.
export const likePost = mutator({
input: t.object({ postId: t.uuid }),
output: PostLikes,
policy: can('post:like'),
idempotent: true, // REQUIRED — a replayed write answers the first result (below)
// Convergent, not incremental: `local` replays on every rebase, so applying it N times has to
// equal applying it once — `likedByMe` is what makes the second application a no-op.
local(tx, { postId }) {
tx.posts.update(postId, (p) =>
p.likedByMe ? {} : { likedByMe: true, likeCount: p.likeCount + 1 });
},
async server(ctx, { postId }) { return ctx.posts.like(postId); },
conflict: 'server-wins', // | 'last-write-wins' | custom((localRow, serverRow) => row)
});
conflict is @ultimat3/core's ConflictPolicy — the one vocabulary realtime's rebase reads.
custom(merge) receives the local row and the server row (the store is row-shaped) and
returns the row that survives; resolveConflict(policy, local, server) is core's, not this
package's.
The projected surface carries the same three names the declaration used, on top of every action member above:
likePost.local(tx, { postId }) // the optimistic write, replayed on rebase
await likePost.server(ctx, { postId }) // the authoritative write
likePost.conflict // the declared strategy
.server() is not a shortcut past invoke — it calls the action's own callable, so
the input parse, the policy and the output parse all still run: an actor the policy
denies is denied there exactly as over HTTP. .local() is the only half that skips
the core, because it never leaves the client; keep it a pure function of (tx, input)
— no I/O, no clock, no randomness — since every rebase replays it.
idempotent: true is required on every mutator — a compile error without it, and
X_MUTATOR_NOT_IDEMPOTENT at declaration for a caller the compiler never saw. A mutator's write
is replayed by construction: useMutation retries, and the offline queue drains after a dropped
response, under ONE Idempotency-Key. The server reads that key only for an idempotent action, so
without the declaration the replay ran server a second time — a toggle flipped back. It is
declared, never defaulted, because the replay is answered from the idempotency store and which
store that is (below) is the app's to decide. transition() declares it for you.
LocalTx is the client write surface, implemented by @ultimat3/realtime over the page's
record store — the SAME shape as that store's tx. Every table is addressed by key:
get(key), all(), insert(key, row), upsert(key, row), update(key, patch | fn),
delete(key). The key is the caller's because the browser holds no entity schema and cannot
derive a primary key — an optimistic insert names the key its server twin will answer under.
Type your tables once: declare module '@ultimat3/action' { interface LocalTables { posts: PostRow } }.
transition() — a mutator factory over a state machineAs of 2026-08-24. A move through an entity column's state machine is a server-authoritative write
with an input schema, an output schema and a policy — which is what a mutator already is. So
transition() returns one, and the move inherits the route, the OpenAPI operation, the typed
client, the MCP tool, the job handle and its PRIMITIVE_FACTORIES row. It is not a ninth primitive
and it declares no error code of its own.
import { t, transition, type TransitionTarget } from '@ultimat3/action';
import type { Ctx } from '@ultimat3/core';
import { can } from '@ultimat3/policy';
const ORDER_STATES = ['pending', 'paid', 'shipped'] as const;
type OrderState = (typeof ORDER_STATES)[number];
const OrderView = t.object({ id: t.uuid, status: t.enum(ORDER_STATES) });
// `@ultimat3/entity`'s `orders(ctx)`: a real `Table` satisfies the seam as written.
declare function orders(ctx: Ctx): TransitionTarget<{ id: string; status: OrderState }, OrderState>;
declare const id: string;
declare const ctx: Ctx;
export const moveOrder = transition({
table: (ctx) => orders(ctx), // the request's table — tenant-scoped like every write
column: 'status', // the column whose enumerated().transitions() IS the machine
states: ORDER_STATES, // typed against the row: a state it cannot hold is a compile error
localTable: 'orders', // what the optimistic twin patches
output: OrderView,
policy: can('order:move'),
});
await moveOrder({ id, from: 'pending', to: 'paid' }, { ctx });
| Rule | Why |
|---|---|
from is required, and never defaulted or inferred | it rides in the UPDATE's own predicate, so the state observed and the state written are one decision under the row's lock. Measured on the mechanism underneath: twenty concurrent moves at one row gave 14 winners with a read-then-check-then-write and 1 winner plus 19 refusals with from in the predicate. Anything that supplies from for the caller is the lost update coming back |
the states are the input schema, not a t.string | the union survives into InferOutput, so the typed client refuses a typo at compile time, the MCP tool's inputSchema and the OpenAPI component both publish the legal set, and a bad state is X_INPUT_INVALID before a database is touched |
conflict: 'server-wins', not overridable | the server is the half that REFUSED the move; a local twin winning the rebase would leave the client showing a state the database rejected |
id is the entity's key, read off output.id | a uuid key stays t.uuid (a malformed one is X_INPUT_INVALID before a database reads it); a text() key keeps its length and a bigint() key its digits pattern — never a fixed t.uuid, which refused every move on an entity keyed by anything else. An output that does not carry the key as id passes the key's schema as id:; with neither, the id is a uuid |
idempotent: true, always | a move replayed under its Idempotency-Key answers the first outcome, not an X_STATE_CONFLICT for a move that already happened |
audit is off unless the app says so | audit: true with no sink installed is X_AUDIT_SINK_MISSING, raised before the input parse — an on-by-default audit would make every transition() refuse until an unrelated decision was made. What the row is kept for, and for how long, is the same compliance question that kept a purge out of postgresAuditSink |
a row-level policy gets its row from row | the action's own loader, handed through: row: ({ input, ctx }) => posts(ctx).find(input.id), then can<TransitionValues<S>, Post>('post:move', ({ actor, row }) => row !== null && row.authorId === actor?.id). Loaded once, after the input parse, before the guard and the statement; the factory reads no row itself. Omitted, the rule sees row: null and must fail closed |
| what the rule read rides in the move (#702) | the rule is handed a view of the loaded row that records which properties it reads; the move passes the row and that list to Table.transition as observed, and entity pins each one that is a plain column of the moved row beside id and from. An authorId reassigned between the read and the move — committed first, or in flight and waited on by Postgres — matches no statement: X_STATE_CONFLICT naming the column, and the row does not move. A column the rule never read may change freely. Not pinned: a row carrying another record's key (a loader reading a parent record; a keyless projection such as select({ authorId: true }) is this row), a property that names no column, and a timestamptz, jsonb, array, bytea, money or sealed column — none has an = that means one thing in both drivers. Nor a column read inside a getter or method of a class-shaped row: only the accessor's name is recorded, so return a plain row from the loader |
X_STATE_TRANSITION_ILLEGAL, X_STATE_CONFLICT and X_STATE_UNDECLARED propagate untouched | they are @ultimat3/entity's. A second error class over one failure is a second path |
table is typed structurally (TransitionTarget), not imported — a real Table satisfies the
seam as written.
serializeOpenApi(buildOpenApi()) sorts keys at every depth, iterates the registry
name-sorted, and reads no clock, env or random source — same registry ⇒ same bytes ⇒
x verify can diff the spec and fail on X_CONTRACT_DRIFT.
rateLimit: is the enforced limitimport type { ActionRateLimit } from '@ultimat3/action';
// The `rateLimit:` key of an `action()`: 5 held, one back every two minutes.
const rateLimit: ActionRateLimit = { limit: 5, windowMs: 600_000 };
One declaration, one bucket, every surface. invoke spends it — so the HTTP route, the MCP tool,
the in-app agent's tool call and .job() draw on the same bucket, and the third call to a
limit: 2 action is refused X_RATE_LIMITED whichever door it came through. A direct in-process
call (surface: 'server' — a service, a seed, a test) is app code, not a caller, and spends nothing.
| Fact | Answer |
|---|---|
| whose bucket | the caller's: actor → org → connection address (@ultimat3/http's rateLimitSpends). An anonymous caller with no address (MCP, a job) shares one bucket |
| where it is counted | @ultimat3/http's installed store (installRateLimitStore()) — the slot @ultimat3/query spends from too; the boot fills it with the SAME instance it gives httpServer({ rateLimitStore }), on every role. Default: one process' memory |
| what HTTP answers | RateLimit-* from the bucket closest to refusing, and on a refusal 429 + Retry-After — the pipeline's own shape |
| a retry | an admitted attempt spends one token; a refused one spends nothing. A job refused X_RATE_LIMITED is rescheduled for its Retry-After WITHOUT counting an attempt, so a backlog is delayed, never dead-lettered |
| an idempotent replay | spends nothing: the stored answer is served, never a 429 |
| who a job run is | its actor, else its tenant (org:<orgId>); a run with neither shares one job:unattributed bucket, never the anonymous callers' |
| OpenAPI | x-ultimate.rateLimit, and with defineApi({ openapi }) the RateLimit-* headers and 429 |
import type { PgExecutor } from '@ultimat3/core';
import { installRateLimitStore, postgresRateLimitStore } from '@ultimat3/http';
declare const executor: PgExecutor; // the pool this boot already opened
// Boot, before the first call — the one store every surface and every replica counts in.
// `x dev` and a container boot do this themselves, and `httpServer({ rateLimitStore })` adopts
// the store it is handed; a process with no server (a worker of your own) installs it here.
const rateLimitStore = postgresRateLimitStore({ executor });
installRateLimitStore(rateLimitStore);
toBucket is the only conversion — capacity: limit, refillPerSecond: limit / (windowMs / 1000)
— so the published numbers and the enforced ones cannot differ. It lives in @ultimat3/http,
beside Bucket and the limiter maths, and is imported from there — never re-exported here (25.0.0
dropped that second path): @ultimat3/query needs the same conversion and is the same tier, so a
copy in either package would be a second answer for the other. A pair the limiter cannot run on is X_RATE_LIMIT_INVALID, at projection. The route says
rateLimitedBy: 'handler', so the pipeline's stage spends no caller bucket for it — not default,
which would cap an action declaring more than it — and only the tenant allowance. An
http.rateLimit.buckets.<actionName> entry is a different table: it neither conflicts with nor
loosens what the action declared.
idempotent: — and where its records liveidempotent: true + an Idempotency-Key header replays the first outcome
(x-ultimate-replayed: 1); a duplicate still in flight, or a reused key with a new payload, is
X_IDEMPOTENCY_CONFLICT.
A record belongs to one caller. The key is namespaced by action and by actor
(idempotencyKeyFor), so two callers sending the same header value hold two records — the same
value under one action used to be one shared record, which replayed one caller's response to
another. A blank Idempotency-Key: is X_IDEMPOTENCY_KEY_INVALID, never read as "no key":
Headers.get() answers '' and not null, so a blank header was itself a shared key, and the
quiet reading — run without idempotency — loses the retry protection exactly when a client's key
interpolation broke. Omit the header to run un-keyed; the published maxLength: 255 is enforced
by the same refusal. An anonymous caller has no identity to narrow to, so anonymous callers of a
public idempotent action still share a key space: a UUID key is what keeps them apart.
A failed first attempt is replayed too, not re-run. guardAction() and the input parse both happen
before the idempotency gate, so everything it can see throw is post-authorization and possibly
post-commit: a handler that took the money and then failed its own output: schema is the case.
The reservation is settled as a FAILURE and the retry re-throws it under the first attempt's own
code. Releasing it there is what made idempotency the cause of a double charge.
A credential in the answer is never kept at rest. The record lives a day, and a mutator is
always idempotent — so a "create an API key" mutator would otherwise leave the plaintext key in
x_idempotency.value. On settle, every value under a key @ultimat3/core's isRedactedKey names
(the table the logger and the audit row use, extended by defineEnv({ secret: true })) and every
Secret box is stored as [redacted], at any depth, and the record is flagged redacted. The
first caller still gets the whole answer. A replay of a flagged record is
X_IDEMPOTENT_REPLAY_REDACTED (409) rather than [redacted] served as the answer: the first call
RAN, so read its effect with a query — a fresh key runs the mutation again. An answer with no such
key is stored and replayed exactly as before — plain data (plain objects, arrays, Date,
primitives) comes back as the same reference. What the walk cannot vouch for is redacted too:
a value with a toJSON method, a Map, a Set, a class instance or a Date subclass, because
JSON.stringify would run app code the walk never saw; an own getter is read once and the stored
copy holds that read. A custom IdempotencyStore receives the redacted copy, must declare
keepsRedaction: true (a store without it is a compile error), and must keep settle's required
fourth argument and hand it back as IdempotencyRecord.redacted:
import type { IdempotencyStore } from '@ultimat3/action';
// A store that wraps another — a metrics or tracing decorator — passes the flag through.
export function counted(inner: IdempotencyStore, onSettle: () => void): IdempotencyStore {
return {
scope: inner.scope,
keepsRedaction: true,
reserve: (key, requestHash) => inner.reserve(key, requestHash),
settle: (key, value, reservationId, redacted) => {
onSettle();
return inner.settle(key, value, reservationId, redacted);
},
release: (key, reservationId) => inner.release(key, reservationId),
get: (key) => inner.get(key),
};
}
The stored payload fingerprint is keyed. requestHash is keyedFingerprint(input, …) from
@ultimat3/core: h1:<key id>:<HMAC-SHA-256/128> under a key derived from the app's signing
secret (ULTIMATE_CURSOR_SECRET / configureCursorSigning), never a bare hash — the row lives a
day, and an unkeyed hash of an input holding a short account or ID number is brute-forced offline
from a table read. A row fingerprinted by a pre-22.5.1 build (16 bare hex characters) is still
compared exactly until it ages out. A row keyed under a secret this process does not hold (a
rotation inside the window) is answered on its status alone — replay, never re-run — and logs
action.idempotency.fingerprint-unverifiable: a mismatched body under that key is not diagnosed
for that window, and no honest retry gets a false 409.
Where the records live is declared, and refused at registration. The default store is process
memory — bounded, swept on a 24h window, and scope: 'process'. An app on more than one replica
must say so and bring a store that can keep it, or the retry that lands on another replica finds
no record and runs the handler again:
// boot, before registerActions()
import {
configureIdempotency,
postgresIdempotencyStore,
requestDeadlineMs,
setIdempotencyStore,
} from '@ultimat3/action';
import { db } from '@ultimat3/db';
const client = db();
// NOT `executor: Bun.sql` — `Bun.sql.query` is `undefined` `As of 2026-08` (it is a tagged
// template whose positional form is `unsafe`), so that line compiles and throws on the first
// reservation.
// The framework boot installs this store for you; reach for it by hand only from a host that
// boots the framework itself, and wrap the client that host already opened.
setIdempotencyStore(
postgresIdempotencyStore({
executor: { query: (text, values) => client.query({ text, values }) },
// The client transactions on THIS database open on: a settle rides one of those and no other.
origin: () => client,
// The app's `requestTimeoutMs` (`configureHttp`), read per reservation — never a number here.
reclaimAfterMs: requestDeadlineMs,
}),
);
configureIdempotency({ scope: 'shared' });
configureIdempotency({ scope: 'shared' }) over a per-process store — or over a store that
declares no scope at all — is X_IDEMPOTENCY_NOT_SHARED at registerAction, before the socket
opens. The table is SQL_IDEMPOTENCY_TABLE, applied the way SQL_JOBS_TABLE is: x db up in
development, the release-phase ROLE=migrate in production. postgresIdempotencyStore(...) .purgeExpired() is the sweep — Postgres forgets nothing on its own, so run it from a task.
A plain mutating route can use the same gate. withIdempotency, idempotencyKeyFor and
getIdempotencyStore are public here and IDEMPOTENCY_HEADER is @ultimat3/core's (imported from
core — 25.0.0 dropped this package's re-export, and BUILD_ID_HEADER's with it), so a route that is
not an action reserves and replays through the one implementation rather than growing a second:
const key = req.header(IDEMPOTENCY_HEADER);
// No header is the caller declining idempotency; a BLANK one is not, and
// `idempotencyKeyFor` refuses it below rather than filing a record everyone shares.
if (key === null) return jsonResponse(await refund(input));
const outcome = await withIdempotency(
getIdempotencyStore(),
// Namespaced by action AND actor: otherwise two routes share one caller's key, and two
// callers share one record.
idempotencyKeyFor('refundCharge', key, req.ctx.actor),
input,
() => refund(input),
);
settle and fail take the reservation's own id — outcome's reservation, never the key alone.
Both stores fence on it AND on in-flight As of 2026-08, the way @ultimat3/jobs' SQL_ACK fences on
id = $1 and state = 'running': a reservation whose window lapsed is reclaimed by the next caller,
so a straggler from the first attempt satisfied a status-only fence exactly and overwrote a live
reservation.
release takes it too (As of 2026-10-07): it is the pre-handler cleanup when beforeRun refuses,
and keyed alone it DELETED whatever record the key held — a replacement's reservation, or its
settled answer, so the next retry re-ran a handler that had already committed. A release that
matches nothing does nothing (idempotency-release.test.ts, both stores). A custom store's
release(key, reservationId) must delete only the record whose id is reservationId and whose
status is in-flight — one that ignores the second argument reopens the double-run window, and
type-checking cannot catch it: a one-parameter function still satisfies the signature.
Inside a transaction the settlement commits with the write (As of 2026-10-02, BOTH stores —
idempotency-parity.test.ts runs one set of cases over memory and Postgres). An action invoked
inside withTransaction settles with that transaction: on its own connection (Postgres), or at
its onCommit (memory).
| The unit of work | The record | A retry |
|---|---|---|
| commits | settled, by the same COMMIT | replays the first result |
| rolls back after the handler returned | still in-flight — never settled for rows nobody stored | X_IDEMPOTENCY_CONFLICT until the request deadline, then it runs |
| dies mid-flight | still in-flight | the same |
| outlives the deadline and a retry takes the key | the retry's | the slow attempt's settle is refused X_IDEMPOTENCY_RESERVATION_LOST, which rolls its transaction back — one write |
| Rule | Why |
|---|---|
the deadline is reclaimAfterMs: () => number, required on the Postgres store | it is the app's requestTimeoutMs, declared (configureHttp) after the boot built the store — so it is read per reservation, and requestDeadlineMs is the reader. 0 is "no deadline": nothing is reclaimed before the window. The memory store defaults to the same reader |
it frees only a record whose settle was BOUND to a transaction (x_idempotency.tx_bound) | a handler with no transaction around it keeps the old rule — reserve, run and settle are three commits, "died before the write" and "wrote, then died before the settle" are one row, and that row answers 409 for the whole window rather than risk the second charge |
| the reservation and a recorded failure always go through the pool | a duplicate sees the first at once, and a failure outlives its rollback |
origin: () => client is required on the Postgres store | a transaction opened on ANOTHER database (withTransaction(fn, { client: shard })) has no x_idempotency; its record is settled on the pool, as an autocommit handler's is |
| a finished transaction binds nothing | a promise chain the body forgot to await still finds the scope's handle; the settle then runs at once (liveTxConnection), never as a commit hook that a rollback already dropped |
A query has none and never will: a read has nothing to be idempotent about.
deprecated: — a compat window, not a versionimport type { Deprecation } from '@ultimat3/core';
// The `deprecated:` key of an `action()`.
const deprecated: Deprecation = {
since: '2026-08-01T00:00:00Z',
sunset: '2026-12-31T23:59:59Z',
replacedBy: 'searchOrders',
};
Four things at once: Deprecation: @1754006400 (RFC 9745) and Sunset: Wed, 31 Dec 2026 …
(RFC 8594) on every response including the failures, link: </api/orders/search>; rel="successor-version", deprecated: true plus x-ultimate.deprecation in the OpenAPI
operation, and a deprecated_calls_total{primitive,name} counter — which is the only way to
answer "is anyone still calling it?" before deleting it. A date that cannot be rendered is
X_ACTION_DEPRECATION_INVALID at projection, not on the first request.
Deprecation, renderDeprecation and recordDeprecatedCall are @ultimat3/core's and are
imported from there — this package does not re-export them (As of 25.0.0).
Versioning itself is deliberately absent, and will stay absent. Running v1 and v2 of one
action side by side is two deployments behind one ingress — axiom 7's answer, costing this package
no router feature, no path prefix and no second registry. What ships is the window: a date, a
successor, and a number.
audit: true on any action or mutator sends every attempt — allowed, denied and failed
— to the installed AuditSink. Opt-in per declaration, never a global switch: a login and a
price change are not the same event, and the framework is not the thing that knows which of
them your business has to keep.
import { setAuditSink } from '@ultimat3/core';
setAuditSink({
async write(record) { await record.ctx.db.auditRows.insert(myRow(record)); },
});
What the framework supplies is what it genuinely knows:
| Field | |
|---|---|
at | when the attempt began, from ctx.now() — an instant, never a rendering |
name / primitive / mutator | the registered name; 'action' (or 'query' for an audited read); and whether mutator() built it. name and primitive are required, and the action alias of name is gone (As of 25.0.0) — postgresAuditSink still files name in the x_audit.action column |
surface | server | http | mcp | job (and live on a read) — the same price change over MCP is not the same event |
ctx | the whole context: actor, requestId, traceId, locale, and the services a sink needs to write a row |
input | the parsed input, or undefined when the parse is what failed — never the raw payload |
idempotencyKey / replayed | the namespaced key, and whether this was a call rather than a write |
outcome | allowed | denied | failed |
failure | the X_* code and the thrown value, on every outcome but allowed |
One contract, one sink, shared with reads. AuditRecord, AuditSink and the installed slot
are @ultimat3/core's, and so is the one way to install it: setAuditSink from @ultimat3/core
(this package re-exports the two types, never the slot — As of 25.0.0) installs the sink query({ audit: true }) writes to as well (@ultimat3/query's README). A sink
tells the two apart by record.primitive.
What it does not supply: an audit entity, a retention policy, a hash chain, a subject index, or an opinion on what "who" means under impersonation. Four apps model those four ways; shipping one would make three of them wrong.
| Sink | Keeps | Use it for |
|---|---|---|
memoryAuditSink({ maxRecords }) | the newest DEFAULT_MAX_AUDIT_RECORDS (1,000) records, verbatim. It DROPS — dropped counts what it discarded | x dev, tests |
postgresAuditSink({ executor }) | one append-only x_audit row per attempt. Drops nothing | anything that has to keep its trail |
The memory sink is bounded because a record pins a whole Ctx: at 50 audited writes a second an
unbounded array is 4.3M immortal records a day and the pod dies holding the trail it was
retaining. The trap it names out loud is that the shortest edit clearing X_AUDIT_SINK_MISSING
is setAuditSink(memoryAuditSink()), and nothing at that call site says the result is amnesiac.
// apps/web/server.ts — the app owns the connection, so the app installs the sink
import { postgresAuditSink } from '@ultimat3/action';
import { setAuditSink } from '@ultimat3/core';
import { db } from '@ultimat3/db';
const client = db();
setAuditSink(
postgresAuditSink({ executor: { query: (text, values) => client.query({ text, values }) } }),
);
The table is applied by the boot; the sink is not. startQueue runs SQL_AUDIT_TABLE on
every start — x dev, the container's web/worker, and the release-phase ROLE=migrate — the
way SQL_IDEMPOTENCY_TABLE is applied, because a package holding no database dependency cannot
apply its own schema. Installing a sink stays your one line, deliberately: there is no default, so
audit: true with none installed keeps refusing with X_AUDIT_SINK_MISSING instead of recording
into a ring. executor is a client that already speaks (text, values) — never Bun.sql, whose
.query is undefined.
x_audit carries the framework's own facts as columns — the action, the surface, the outcome,
the actor, the correlation ids, the idempotency key — and the parsed input as jsonb, redacted
through core's own isRedactedKey table, the one defineEnv({ secret: true }) extends. So a
value that renders [redacted] in a log line cannot be plaintext in the audit table, and a
boxed Secret is redacted by value wherever its key sits. What never reaches a column: the Ctx
itself (ctxOf spreads every installed service onto it, and an HTTP surface's is a
RequestContext carrying the caller's Authorization and Cookie), and the thrown value behind
a failure — the row keeps failure.code, never the throwable.
The table has no purge, deliberately, and it is the one framework table that does not: a
stale idempotency row is meaningless while a stale audit row is the record, and "how long" is a
legal answer that differs per app. Pruning or partitioning x_audit is yours.
A denial is recorded because invoke wraps the whole path — guardAction throws before handle,
so nothing you could write around your own handler would ever see one. That is the reason this
lives in the framework and the row does not.
Failure is loud, both ways. audit: true with no sink installed is X_AUDIT_SINK_MISSING,
raised before the input parse — the one audit failure with no committed write behind it. A sink
that refuses a successful record is X_AUDIT_SINK_FAILED: the deliberate opposite of the
cache tier's bestEffort, because a dropped cache entry expires by TTL and the stack heals
itself while nothing ever re-derives an audit row that was never written. It is post-commit all
the same, and the error says so.
Its fix: branches, because only one of the two is ever true. Retrying is safe exactly when
this invocation went through the idempotency store — then the settled record replays and the
audit row is re-attempted without re-running the handler. It did not when the action is not
idempotent, and it did not when the action is idempotent but the caller sent no
Idempotency-Key: invoke reads def.idempotent === true ? (options.idempotencyKey ?? null) : null, so both collapse to the same null. In that case the error says do not retry and
names the edit — telling a caller to re-run a committed mutator is worse than saying nothing.
meta.replayable carries the same fact to --json.
A sink that refuses a denied or failed record is logged as
audit.sink.failed and the original error still reaches the caller: answering
X_AUDIT_SINK_FAILED there would hide the X_FORBIDDEN from whoever has to act on it.
Your house rule goes in a wrapper, not in a config option — the same shape as tenantEntity():
// apps/web/shared/base/audited-mutator.ts — the app's convention, written once
import { mutator, type MutatorDef } from '@ultimat3/action';
import type { StandardSchemaV1 } from '@ultimat3/schema';
/** Every write in this app is recorded and retryable — declared once, not at forty call sites. */
export const auditedMutator = <I extends StandardSchemaV1, O extends StandardSchemaV1>(
def: MutatorDef<I, O>,
) => mutator({ ...def, audit: true, idempotent: true });
Nothing downstream can tell the difference: isMutator() is structural and registerActions
names the object in place, so every projection, the manifest and admin CRUD work on it exactly
as on a hand-written one.
The row it produces is the app's, and so is every question the framework refused to answer — which fields, whose tenant, chained or not, kept how long:
setAuditSink({
async write(record) {
const { ctx } = record; // the services a sink needs to write a row
const prev = await chainHead(ctx); // hash-chained: the app's choice
await ctx.db.auditRows.insert({
orgId: orgOf(ctx.actor), // tenancy: derived from the actor
subjectId: subjectOf(record.name, record.input), // queryable by subject: the app's index
actorId: impersonatorOf(ctx.actor) ?? ctx.actor.id,
at: record.at, outcome: record.outcome, code: record.failure?.code ?? null,
prevHash: prev, hash: await sha256(prev, record),
});
},
});
publishPost.contract() returns three assertions. Run them; they throw X_CONTRACT_DRIFT.
| Assertion | Holds when |
|---|---|
| input schema rejects garbage | the invocation fails X_INPUT_INVALID — that code, not any failure |
| policy denies an anonymous actor | the invocation fails with an ActionDeniedError |
| OpenAPI document contains its operation | the action's path, in the document built from the WHOLE registry (plus this action when a test drives an unregistered .named() twin), is this action's operation — a second registered action deriving the same route fails it |
The denial assertion sends an input synthesized from input:'s own schema — required keys
only, formats included — because a payload the schema rejects never reaches a policy. It
asserts the denial, not X_FORBIDDEN: a denial carries the policy decision's own code, and
can() answers a null actor with X_UNAUTHENTICATED.
publishPost.contract({
garbage: 42, // what the input schema must reject
input: { postId, orgId }, // when the synthesized one cannot fit
ctx: myCtx, // default: an anonymous context
})
Pass input: when the schema carries a constraint the IR cannot invert (a bare pattern) or
when row: needs an id that resolves. Anything thrown before the policy decides is drift,
never a pass — the assertion says which code got in the way and names input: as the fix.
| Code | When | Fix |
|---|---|---|
X_ACTION_DUPLICATE | two actions registered under one name | rename one export |
X_ACTION_PATH_DUPLICATE | two actions derive one HTTP path (archiveOrder / archiveOrders) | rename one export |
X_ACTION_POLICY_MISSING | registration without policy: | add policy: can('…') |
X_RATE_LIMIT_INVALID | rateLimit: with a non-positive or non-finite half — windowMs: 0 refills infinitely. Owned by @ultimat3/http, which owns the conversion | make both positive, or delete the block |
X_ACTION_DEPRECATION_INVALID | deprecated: with a since/sunset that is not a date | use an ISO-8601 instant |
X_INPUT_INVALID | input failed the Standard Schema. Carries the rejections twice: the flattened line in cause, and the structured list in meta.issues — one value rendered two ways, As of 2026-08-24 | x actions show <name> --json |
X_IDEMPOTENCY_CONFLICT | key reused with a new payload / still in flight | new key, or retry later |
X_IDEMPOTENCY_KEY_INVALID | Idempotency-Key: sent blank (Headers.get() answers '', not null) or past 255 characters | send one unique value per request, or omit the header |
X_IDEMPOTENCY_NOT_SHARED | configureIdempotency({ scope: 'shared' }) over a per-process (or scope-less) store | install postgresIdempotencyStore({ executor, origin, reclaimAfterMs }) at boot |
X_IDEMPOTENCY_REPLAYED_FAILURE | a retried key replays a first attempt that failed and carried no framework code of its own | read the first attempt, then send a fresh key |
X_IDEMPOTENCY_RESERVATION_LOST | a settle inside the handler's transaction matched no record: the attempt outlived the request deadline and a retry took the key. This attempt's transaction rolls back | resend with the same Idempotency-Key |
X_MUTATOR_NOT_IDEMPOTENT | mutator() declared without idempotent: true | add idempotent: true to the definition |
X_IDEMPOTENCY_STATUS_UNKNOWN | x_idempotency.status holds a word this build has no branch for — written by a newer deploy | finish the rollout onto the build that writes it, then reconcile those requests — never DELETE the rows, which frees the key to run an already-committed action a second time |
X_CONTRACT_DRIFT | client/server build skew, missing spec entry | reload / x verify --only contract |
X_RPC_FAILED | registered, thrown by nothing since 21.0.0 — a non-problem+json failure is core's X_CLIENT_TRANSPORT_FAILED now, as for a query | match X_CLIENT_TRANSPORT_FAILED instead |
X_ACTION_UNREGISTERED | projected before registerActions() ran | register at boot |
X_AUDIT_SINK_MISSING | audit: true and no sink installed — raised before the input parse | setAuditSink(yourSink) at boot |
X_AUDIT_SINK_FAILED | the sink refused the record for an attempt that succeeded | fix the sink — then retry the same Idempotency-Key if this call carried one, else reconcile by hand |
Denials re-throw the policy layer's own codes (X_FORBIDDEN, X_UNAUTHENTICATED) —
this package never invents an authz code.
The client does the same with the server's: a problem+json failure comes back as a
RemoteActionError keeping the code the server sent, marked meta.origin: 'remote' because
the browser bundle may never have registered it, and linked only to a page that exists — the
server's own docs/type when it sent an http(s) one, this build's registered link when it
knows the code, otherwise the error index. A per-code URL is never synthesized for a code
nothing here declares.
A document carrying an issues member arrives parsed as well: meta.issues, read by
issuesFromWire — a wire value, so the list is rebuilt member by member and a list this build
cannot read is dropped whole rather than half-kept, leaving cause (which still holds every
rejection) as the answer. It is exported for the island that posts with a plain fetch and holds
the body itself.
Every error class src/index.ts exports, for instanceof inside one process. Across a wire or
a job boundary the class is gone and the code is what survives — match on that.
| Class | Code | Declared in |
|---|---|---|
ActionDeniedError | the policy denial's own code (X_FORBIDDEN, X_UNAUTHENTICATED, …), kept on .denial | src/errors.ts |
ActionDeprecationInvalidError | X_ACTION_DEPRECATION_INVALID | src/errors.ts |
ActionDuplicateError | X_ACTION_DUPLICATE | src/errors.ts |
ActionForeignError | X_ACTION_FOREIGN | src/errors.ts |
ActionPathDuplicateError | X_ACTION_PATH_DUPLICATE | src/errors.ts |
ActionPolicyMissingError | X_ACTION_POLICY_MISSING | src/errors.ts |
ActionUnregisteredError | X_ACTION_UNREGISTERED | src/errors.ts |
AuditSinkFailedError | X_AUDIT_SINK_FAILED | src/errors.ts |
AuditSinkMissingError | X_AUDIT_SINK_MISSING | src/errors.ts |
ContractDriftError | X_CONTRACT_DRIFT | src/errors.ts |
IdempotencyConflictError | X_IDEMPOTENCY_CONFLICT | src/errors-idempotency.ts |
IdempotencyKeyInvalidError | X_IDEMPOTENCY_KEY_INVALID | src/errors-idempotency.ts |
IdempotencyNotSharedError | X_IDEMPOTENCY_NOT_SHARED | src/errors-idempotency.ts |
IdempotencyReplayedFailureError | X_IDEMPOTENCY_REPLAYED_FAILURE | src/errors-idempotency.ts |
IdempotencyReservationLostError | X_IDEMPOTENCY_RESERVATION_LOST | src/errors-idempotency.ts |
IdempotencyStatusUnknownError | X_IDEMPOTENCY_STATUS_UNKNOWN | src/errors-idempotency.ts |
InputInvalidError | X_INPUT_INVALID | src/errors.ts |
MutatorNotIdempotentError | X_MUTATOR_NOT_IDEMPOTENT | src/errors-idempotency.ts |
OutputInvalidError | X_OUTPUT_INVALID | src/errors.ts |
RemoteActionError | the code the server sent, verbatim — meta.origin: 'remote' | src/errors.ts |
RpcFailedError | X_RPC_FAILED | src/errors.ts |
Tier 3. Imports @ultimat3/core, schema, cache, db, entity, policy, http. Never imports
query, jobs, realtime (same tier) or anything above it — those import this.
FAQs
The action primitive: one declaration projected to route, OpenAPI, client, MCP tool, job handle, tests
The npm package @ultimat3/action receives a total of 3,264 weekly downloads. As such, @ultimat3/action popularity was classified as popular.
We found that @ultimat3/action demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.

Research
/Security News
A new GhostAction wave hits hundreds of GitHub repos, expanding CI/CD secret theft to cloud and AI credentials in source code and git history.

Research
/Security News
Tensorlake npm SDK version 0.5.144 was compromised in a ChainDrop / Shai-Hulud attack, delivering credential-stealing malware.