@xyo-network/dapp-kit-vercel
v2.1.4
Published
Vercel target analysis and host adapters for headless XL1 dApps
Readme
@xyo-network/dapp-kit-vercel
Vercel target analysis and host adapters for portable XL1 dApps.
The package verifies an exact dapp-kit Deployment Lock, checks every locked
requirement against one versioned Vercel target policy, and returns either a
deterministic target plan or stable rejection issues. Its offline CLI can then
generate or lint the complete vercel.json; its read-only audit command can
compare an existing Vercel project with that plan. An explicit provision
--apply reconciles the committed project/resource topology through an already
authenticated Vercel CLI and can deploy both runtime and external signer.
This package does not make every dApp Vercel-compatible. In particular,
resident execution, managed/local XL1 nodes, private-network datalakes,
required-for-liveness side channels, and unfenced effect writers fail closed.
The trigger ledger provides fenced, at-least-once delivery metadata for one admitted actor. The separate event adapters below add a bounded durable stream and independent fan-out subscriptions over private Blob CAS transactions. Event Kit owns signed admission and its leased wake boundary; dapp-kit owns event synthesis and consumer progress. A deployment may also bind its website to Vercel while binding its stream, consumers, or R2 resources to Cloudflare through target composition.
Event Kit admission and bounded consumers
VercelBlobTransactionalStorage implements an atomic multi-key transaction in
one private, origin-consistent Blob envelope. Opening a store performs no
ownership transfer: an operator must explicitly activate epoch 1, or transfer
from the exact prior writer to its next epoch. Every later transaction checks
the same deployment fence and uses an origin ETag CAS. A transaction callback
may run again after contention and MUST NOT perform external effects.
VercelEventKitInboxQueue co-commits replay identities and leased wake work.
Its required profileId and parseWake bind persisted state to the selected
admitted-wake contract. Queues use Event Kit's canonical AdmittedWakeZod
plus the deployment's exact source binding. Ordinary
claims reclaim expired leases. Concurrent Function startup MUST NOT call the
exclusive-owner recoverInflight() operation, which would revoke another
invocation's live lease.
handleVercelEventKitIngress composes an Event Kit admission gate with an
awaited scheduling hint. Public ingress only admits and schedules; it does not
run reducers. A lost scheduling send returns 503 while retaining the admitted
wake. An exact publisher retry schedules it again. A managed Queue callback or
separate authenticated repair invocation calls runVercelEventKitDriver.
The driver acknowledges a wake only after durable event append and every
declared consumer wake request.
VercelQueueEventWorkScheduler is the production Queue implementation of that
scheduler port. It awaits @vercel/queue acceptance before ingress returns,
uses a fresh disposable hint for every repair attempt, and authenticates that
internal hint with a deployment/stream-scoped HMAC. The HMAC protects only the
private Queue callback; it is not an alternative Event Kit admission profile.
Public wake admission still requires the Event Kit wallet JWT and grant view.
import { QueueClient } from '@vercel/queue'
import {
runVercelEventKitDriver,
VercelQueueEventWorkScheduler,
verifyVercelEventWorkHint,
} from '@xyo-network/dapp-kit-vercel'
const queue = new QueueClient({ region: 'sfo1' })
const scheduler = new VercelQueueEventWorkScheduler({
queue,
topic: 'application-events',
deploymentId: 'production',
streamId: 'source-events',
secret: process.env.EVENT_WORK_SECRET!,
})
export const POST = queue.handleCallback(async (value) => {
verifyVercelEventWorkHint(value, {
deploymentId: 'production',
streamId: 'source-events',
secret: process.env.EVENT_WORK_SECRET!,
now: Math.floor(Date.now() / 1000),
})
await runVercelEventKitDriver(/* bounded application dependencies */)
})The Queue callback MUST use a dedicated entrypoint, separate from public HTTP
admission, control, and probe handlers. Vercel makes the entire Function private
when queue/v2beta is attached; public rewrites and service bindings cannot
expose it. See Queue consumer security.
The host artifact declares an exact consumer for each host/topic:
{
"queueConsumers": [
{ "host": "node", "topic": "application-events", "entrypoint": "dist/node/queue.mjs" }
]
}Consumer paths are relative to the artifact's root. The generator keeps HTTP
services trigger-free and emits one private service per host/topic, with one
queue/v2beta trigger and no public rewrite. Actors sharing a host/topic share
one consumer; different host/topic pairs use different entrypoint files and
independent consumer groups. Consumer service names depend on host/topic, not
artifact content hashes. Missing, duplicate, unused, or aliased bindings fail
closed. Existing Queue targets must add these bindings and regenerate their
target/configuration; non-Queue targets need no new field. The consumer build
must attest every dedicated bundle in its artifact manifest. The adapter checks
declared bindings and hashes, not file existence or application behavior.
Public HTTP handlers must reject Queue callbacks rather than dispatch them to
a reducer; the private callback still verifies internal work hints and writer
fencing before doing work. This configuration correction is not hosted Queue
delivery, redelivery, or ACK qualification.
A Workflow driver implements the same VercelEventWorkScheduler interface: request() starts one durable application-owned Workflow run and
resolves only after Vercel accepts that run. The Workflow entrypoint then calls
the same bounded driver and may start a successor only while durable work is
still pending. dapp-kit does not create an application Workflow because its
arguments, deployment, and generated route are application artifacts. Do not
use a detached promise after the ingress response for either driver.
VercelDappEventStore retains ordered events, per-consumer cursors and fences,
outcome proofs, and wake outboxes. runVercelSubscription opens one bounded
reducer session and always closes it. XL1 finality, Ethereum finality, and
completed UTC intervals are independent sources. A wake is a scheduling hint:
the reducer MUST read its source independently and make external outputs
idempotent before returning a committed outcome. An authenticated repair path
must resume durable pending work even when no new source wake arrives.
These are bounded qualification partitions, not an unbounded event database. The object byte limit, maximum records, maximum subscriptions, and retained admission identities fail closed when full. Configure retention/rotation before using an indefinitely running deployment; these adapters do not silently discard history or grant a new writer ownership.
Managed event-stream and event-subscription resources are target-qualified
only with exact vercel-blob adapters and qualified private Blob features.
External event resources require exact external adapters. Queue or Workflow
qualification remains attached to the event-driven actor trigger, so storage
admission cannot silently grant an execution driver.
Local tests cover wallet-signed admission, duplicate and collision handling, CAS contention, writer transfer, independent cursors, scheduling failure, and actual process death before/after admission and acknowledgement. The process tests use an fsynced filesystem Blob facade. They do not establish credentialed Vercel Blob, managed Queue, preview, or public-network qualification. The canonical source-aware contract requires the coordinated Event Kit release; older registry packages do not supply it.
Configuration CLI
The CLI reads three strict JSON inputs:
--locksupplies the verified Deployment Lock;--policysupplies the dated feature/plan/adapter admission policy; and--targetsupplies hash-bound artifact descriptors, exact actor triggers, the analysis date, requested duration bounds, and optional allowlistedconfigurationExtensions. It defaults tovercel.target.json.
xl1-dapp-vercel check \
--lock .xl1/locks/vercel.json \
--policy vercel.target-policy.json \
--target vercel.target.json \
--json
xl1-dapp-vercel generate \
--lock .xl1/locks/vercel.json \
--policy vercel.target-policy.json \
--target vercel.target.json \
--output vercel.json
xl1-dapp-vercel lint \
--lock .xl1/locks/vercel.json \
--policy vercel.target-policy.json \
--target vercel.target.json \
--config vercel.json \
--json
# validate only; performs no Vercel mutation
xl1-dapp-vercel provision \
--lock .xl1/locks/vercel.json \
--policy vercel.target-policy.json \
--target vercel.target.json \
--provision vercel.provision.json \
--json
# reconcile resources, inject configuration, and deploy when the manifest opts in
xl1-dapp-vercel provision \
--lock .xl1/locks/vercel.json \
--policy vercel.target-policy.json \
--target vercel.target.json \
--provision vercel.provision.json \
--apply \
--json
# with VERCEL_TOKEN already present in the process environment
xl1-dapp-vercel audit \
--lock .xl1/locks/vercel.json \
--policy vercel.target-policy.json \
--target vercel.target.json \
--project my-runtime-project \
--team-id team_Xy0AbCdEfGhIjKlMnOpQrStU \
--signer-project my-signer-project \
--jsonplan-promote reads deploy/inventory.json (or --inventory) plus an
injected-or-process git snapshot and reports whether a named --environment
(default beta) may be promoted. It does not require --lock or --policy,
never calls Vercel, and --apply fails closed because promote execution is
planner-only.
deploy is the git-linked Vercel planner extracted from the Crypto Cards
operator flow. It reads the same inventory, refuses a dirty tree, requires the
requested SHA to be on origin/<sourceBranch>, and will only move
origin/<environment.gitBranch> by fast-forward. It never force-pushes and
never spawns vercel deploy. Git-linked Vercel projects rebuild from that
branch pointer. Default is dry-run. --apply requires --yes, filled project
IDs, and a green GitHub Actions workflow named by vercel.ciWorkflow (default
Build) unless --skip-ci-check is explicit. With VERCEL_TOKEN it can watch
READY; with optional origin and readinessPath it can probe. A successful
apply writes deploy/last-vercel.json (local operator state, not a secret).
inventory-audit is read-only. It reports inventory placeholders, last-deploy
receipt drift, and optional live project facts (git link, root directory,
custom-environment branch) when VERCEL_TOKEN is present. Drift exits 3
unless --report-only. It never mutates Vercel.
deploy and inventory-audit check the inventory's vercel.teamId and project
id against the team_ and prj_ formats that audit accepts. A malformed
team ID is invalid data (exit 1, vercel.cli.file-invalid). A project id
that is not a valid prj_ ID, or a team ID left as a placeholder such as
UNFILLED, is reported as an unfilled placeholder and is never sent to Vercel:
deploy --apply refuses to run, and inventory-audit makes no live read.
Optional inventory fields (origin, readinessPath,
requireClosedPublicSurface, teamId, ciWorkflow, deployBranchFilter) are
generic. Product copy, game policy, and Coming Soon text stay in the consumer.
check and lint never write. generate refuses an existing destination
unless --force is explicit and uses a synced temporary file plus atomic
rename. Machine results use stable exit codes: 0 clean, 1 incompatible or
drifted data, 2 invalid CLI use, and 3 for a failed online project probe or
inventory-audit drift. audit accepts its scoped credential only through
VERCEL_TOKEN; it does not echo the token or retain raw project/team
identities. --project and --signer-project take a Vercel project name or
mixed-case prj_ ID; --team-id takes only a mixed-case team_ ID, never a
team slug, and is sent as the REST teamId parameter. A malformed reference
exits 2 (vercel.cli.option-invalid) before any request. Offline or
API-inaccessible project facts are reported as vercel.project.unverified,
never as passed.
vercel.provision.json is strict, secret-free JSON bound to the analyzed
targetPlanId. It declares the GitHub repository and branch environment,
runtime/signer projects, deployment-protection posture, every target-managed
Blob resource, generated cross-project secrets and their Vercel visibility,
signer variable names, and
whether the apply should deploy. Provisioning fails before mutation if the
manifest omits or invents a Blob/signer resource required by the target plan.
Without --apply, provision only validates and emits its deterministic hashed
plan. With --apply, it uses the logged-in vercel executable, creates missing
projects and the branch-tracked custom environment, validates existing project
and Git bindings, reconciles deployment protection, creates/connects public and
private Blob stores, and targets the store IDs to the custom environment. A new
Blob connection is bound to that custom environment only, because Vercel issues
Blob OIDC per environment; an existing connection that lacks it gains it
without losing any environment it already binds. Custom-environment-only
connection creation is covered by deterministic local tests and is not yet
confirmed by a credentialed apply. The apply generates shared credentials only
when any declared destination is missing, derives the expected signer address
from the declared address index, injects the signer URL, and deploys signer
before runtime when requested by the manifest. Existing generated credentials
and signer identity are retained on a repeat run.
Generated secrets declared as sensitive are non-readable after creation;
encrypted generated secrets may be pulled by an authorized operator for a
credentialed validation client. The signer seed is the only user-authored
secret. It is read from the
manifest-declared environment variable (normally XL1_SEED_PHRASE) or from a
hidden terminal prompt. API request bodies, including all secrets, are passed
to the authenticated Vercel CLI over standard input; no secret is placed in a
command argument, manifest, action report, or result JSON.
Test-only provisioning may explicitly pass --allow-well-known-test-seed.
When the seed is not already present in the process environment, that option
labels the hidden prompt accordingly and maps an empty response to the public
test test test test test test test test test test test junk seed. The option
is rejected by non-provisioning commands and MUST NOT be used for an identity
that may hold Mainnet funds.
The versioned policy declares the exact Node major, Fluid-compute setting, maximum audit age, preview-protection policy, and production-route state. The REST probe currently normalizes reviewed project fields such as Node version, Fluid settings, default region/failover, rolling-release presence, OIDC, and deployment protection. Managed-resource semantics or beta enrollment absent from the response remain unverified until a qualified deployment/operator probe supplies them.
The renderer is pinned to the supported subset of the official Vercel schema
and Vercel CLI 58.1.0 behavior reviewed on 2026-08-18. It emits the current
services model: each Node service owns its Function settings, each static
service owns its output directory and SPA fallback, and exact top-level
rewrites select the public service for each path. Static SPA services use
Vercel's /(.*) catch-all so the bare / route and non-root client routes both
resolve to index.html; /:path* is not used because hosted Services leaves the
bare root unmatched. The linter rejects the removed legacy
experimentalServices/routing shape instead of accepting a locally invented
field. Single-region Fluid compute, exact Cron schedules, and Queue
queue/v2beta triggers are emitted only when those capabilities are present and
qualified in the target plan. Queue triggers belong only to the dedicated
private services described above. Workflow/project/resource facts that cannot be
represented in vercel.json stay explicit external requirements.
The target's optional configurationExtensions section is a strict,
hash-bound pass-through for Vercel settings that the adapter does not derive.
It currently accepts response headers, cleanUrls, and trailingSlash.
Header sources, names, values, sizes, and duplicate names are validated before
admission, including rejection of CR/LF injection. Unknown extension keys fail
closed. The section cannot replace generated services, rewrites, Functions,
regions, Cron entries, environment variables, resource ownership, or build
authority. Adding another pass-through field therefore requires an adapter
release that reviews and types that field rather than silently weakening exact
configuration linting.
renderVercelRuntimeDescriptor produces a separate, non-secret runtime
descriptor containing the exact generated-configuration hash, lock ID, plan
ID, target-plan ID, and writer epoch. Applications SHOULD generate this
descriptor beside their Vercel configuration and bundle it into every runtime
and external signer that must agree on writer authority. These immutable values
do not need to be duplicated as Vercel project environment variables, and the
configuration hash is not inserted into the configuration that it hashes.
defineVercelFetchHandler exposes a request handler using Vercel's Fetch
Standard { fetch(request) } export. Vercel entrypoints SHOULD use this shape
so the handler receives a standard absolute-URL Request, rather than relying
on the legacy Node request/response invocation contract.
Vercel Blob object and projection storage
VercelBlobObjectStore implements the neutral object-store surface with a
mandatory repository-style prefix, bounded objects, explicit public/private
access, explicit cache policy, create-only writes, ETag replacement, origin
read-back, deterministic local paging, and a computed SHA-256 value on every
body read. Production construction requires an explicit OIDC/store pair or
read-write token; the adapter does not intentionally fall back to ambient SDK
credential discovery. An injected transport keeps every remote request
boundary deterministic and fault-testable.
Mutable control state must use origin-consistent-control; only immutable
content may select cache-eligible-immutable. Custom object metadata and
content encodings are rejected because the Blob adapter cannot preserve those
neutral-store semantics. The special zero-age head directive is mapped to
Vercel Blob's 60-second cache floor while correctness-critical reads still go
directly to origin.
VercelBlobProjectionStore requires a private origin-consistent object store.
It writes immutable generations, verifies their canonical hash, advances the
head with the previously observed ETag, and permits checkpoint advancement only
after that generation is the verified visible head. The neutral public
projection publisher also runs unchanged over the object adapter.
V3 evidence is deterministic local/facade testing, including failures before and after remote writes. It has not made a credentialed Vercel Blob call, and does not by itself qualify Blob as an XYO datalake.
Effect journal and writer fence
VercelBlobEffectJournalPartition stores the complete bounded recovery
inventory, public receipt surfaces, privileged preparation sidecars, revision,
and active writer authority in one private Blob envelope. This avoids treating
Blob listing or a non-transactional second inventory object as recovery truth.
Preparation admission is atomic; every append and compaction replaces the
envelope with its previously observed ETag.
The authority binds the deployment ID, monotonically increasing epoch, lock ID, plan ID, target-plan ID, and generated-config hash. Fence transfer and effect mutation update the same object, so an already in-flight old deployment loses its CAS race and all later reads or writes from that journal fail as stale. The administrative partition surface, including epoch transfer, belongs only in a trusted deployment control boundary and is not an application port.
V4 conformance forces simultaneous writers, failures before and after remote
writes, lost responses, every receipt stage, preparation compaction, and fence
transfer. A separate SIGKILL matrix recreates the client process before every
retry. This remains a fsynced deterministic Blob facade, not a credentialed
Vercel Blob or Vercel preview result.
Invocation, datalake, and trigger adapters
The UC-01 and UC-02 invocation handlers construct a fresh admitted Node runtime for each request. Authority remains outside command data, status/readiness are exact, cleanup uses the invocation abort boundary, and signer/runner calls revalidate the deployment fence. UC-02 adds immutable public Blob payloads, origin verification, public cached reads, and by-ID reconstruction after private preparation compaction.
VercelTriggerDeliveryDriver maps admitted Workflow or Queue input to a stable
delivery identity. A separate private CAS ledger records claims, payload
fingerprints, completion receipts, sequence cursors, and its writer fence.
Duplicate completion skips execution; retries retain the same identity;
out-of-order input follows the hashed hold/reject policy; completion is durable
before acknowledgement. This metadata never replaces the effect journal or
canonical XL1/datalake state.
Promotion, rollback, and operations
VercelDeploymentTransferCoordinator is the trusted, replayable fencing step
around an injected Vercel promotion or rollback. It closes state-changing
ingress, transfers every effect-journal and trigger-ledger writer epoch,
invalidates outgoing attachments, activates and recovers the incoming
deployment, verifies its exact deployment/target identity and writability, and
only then reopens ingress. A partial transfer remains closed and can be retried
because already-transferred partitions accept the exact incoming authority
idempotently.
Operational events use a closed schema with stable phases and error codes; they
have no arbitrary message, command body, project ID, credential, signer
material, or preparation sidecar field. See
docs/runbooks/VERCEL_OPERATIONS.md
for the hosted qualification and incident procedure.
All current runtime, audit, and promotion evidence is deterministic local evidence over injected request boundaries. No credentialed Blob call, Workflow or Queue run, Vercel preview, external-user test, or production qualification has been performed.
