
Security News
Open VSX Unblocks Extension IDs Used in Malware Campaign
Open VSX has removed three extension IDs from its malicious-extension list as the legitimate publishers they impersonated move to claim the names for themselves.
@ultimat3/action
Advanced tools
The action primitive: one declaration projected to route, OpenAPI, client, MCP tool, job handle, tests
One declaration → six artifacts.
| # | 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 | publishPost.tool() | identical policy evaluation to the route |
| 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.tool() // MCP descriptor
publishPost.openapi() // OpenAPI operation
publishPost.client({ baseUrl }) // typed RPC method
publishPost.job() // durable-work handle
publishPost.contract() // the three generated assertions
publishPost.tool().policy === publishPost.policy — the same 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. 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.
First camelCase word is the verb; the rest is the resource, last word pluralized,
kebab-cased. 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.
| 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, four surfaces — .tool().name, openapi.json's x-ultimate.mcpTool,
describeAction().mcp.tool (what x actions describe --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 first three 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-tool.test.ts's "one name per action, on every surface" is what
keeps 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: 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 |
|---|---|
| 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 |
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.
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 all
six projections; 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'),
// 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(merge)
});
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.
LocalTx is the client write surface (@ultimat3/realtime implements it over OPFS
SQLite). Type your tables once: declare module '@ultimat3/action' { interface LocalTables { posts: PostRow } }.
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 limitrateLimit: { limit: 5, windowMs: 600_000 }, // 5 held, one back every two minutes
One declaration, three places it lands: the bucket the limiter runs on (named after the action,
registered by @ultimat3/http's withRouteBuckets when the route is mounted), the
ratelimit-limit header the caller reads, and x-ultimate.rateLimit in the OpenAPI operation.
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 re-exported here: @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. An action that declares nothing
stays on the default bucket. An app that also configures http.rateLimit.buckets.<actionName>
with different numbers is X_RATE_LIMIT_BUCKET_CONFLICT at boot: neither source wins, because
the loser would go on being read as enforced.
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. guard() 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.
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,
setIdempotencyStore,
} from '@ultimat3/action';
// 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 }) },
}),
);
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, IDEMPOTENCY_HEADER,
idempotencyKeyFor and getIdempotencyStore are all public, 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 json(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.
A query has none and never will: a read has nothing to be idempotent about.
deprecated: — a compat window, not a versiondeprecated: { 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.
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/action';
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 |
action / mutator | the registered name, and which primitive it was |
surface | server | http | mcp | job — 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 |
What it does not supply: an audit entity, a schema, a retention policy, a storage backend, 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.
A denial is recorded because invoke wraps the whole path — guard 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.action, 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 derived path is in buildOpenApi() |
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 | x actions describe <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 }) 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_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 --contract |
X_RPC_FAILED | non-problem+json failure, or a body naming no X_ code | check the gateway |
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.
Tier 3. Imports @ultimat3/core, schema, cache, 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
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
Open VSX has removed three extension IDs from its malicious-extension list as the legitimate publishers they impersonated move to claim the names for themselves.

Product
Socket’s PHP and Composer support is now in Beta for all customers, with PHP reachability analysis generally available.

Product
Socket is bringing experimental protection to Firefox, scanning 97,000+ extensions in Mozilla's official directory for malware and risky updates.