# Browser and server tagging

Manage consent-aware events and approved destinations without arbitrary script execution.


## The service boundary

Tags connect browser ttag, request-scoped server astag, trigger definitions and approved destinations. The default internal analytics tag records matching events; GA4 Measurement Protocol and approved HTTPS webhooks execute server-side so destination credentials do not enter public container JSON.

## Browser collection

Load the configured /\_tags/ttag.js route after establishing an intentional consent policy. Use ttag event/set/get/consent operations. Denied consent discards queued events. Browser events are delivered asynchronously and best effort; a generated request ID does not prove delivery or legal consent.

## Server context

Create astag for a request with a trusted site and approved identity context. Do not share one mutable astag singleton across users. Use a shared database request-ID store for cross-replica uniqueness and siteByOrigin for public tenant binding.

## Studio and releases

Admin routes require host authorization; writes need same-origin checks and optimistic versions. Publish/preview controls and bounded release snapshots do not replace transactional control-plane storage at large scale. No arbitrary browser JavaScript/HTML tags are executed.

## Egress

Destination timeouts isolate failures, but HTTPS hostname validation alone does not remove DNS rebinding risk. Deploy outbound DNS/IP allowlists and firewall rules. Google HTTP acceptance is not proof of GA4 processing. Email marketing consent is independent of telemetry consent.

## Install and select the integration

```sh
npm install @zeruxjs/tags@0.0.4
```

This package participates in the runtime's **tags** integration. Start with [tags configuration](/zeruxjs/docs/0.0.4/configuration-tags.md), which documents `zerux.config.js`, its exact values and the matching TypeScript setup. Runtime setup and standalone factory input are different contracts; do not paste a factory argument under a config key without checking the adapter layer.

## A practical integration workflow

Configure analytics first, then server-only credentials, a shared request-ID store and a versioned tag container. Browser tags refer to approved credentials/destinations by identifier rather than embedding secrets. Choose consent defaults explicitly; granted is the package fallback, so privacy-sensitive applications should set denied and wire the host consent interaction.

## TypeScript: inputs, results and ownership

```ts
import { createTagManager } from '@zeruxjs/tags';

type Inputs = Parameters<typeof createTagManager>;
type Result = Awaited<ReturnType<typeof createTagManager>>;
```

`Inputs` is the function's complete argument tuple, including optional parameters. `Result` removes a Promise when the function is asynchronous. These aliases follow the installed declarations; no internal `src`/`dist` import is required. For a generic factory, keep the concrete call's inferred type when an application needs a narrower result than `ReturnType` preserves. Use `satisfies` or a public option type for configuration literals, and validate unknown request/file data separately.

In a JavaScript application the equivalent editor annotation is `/** @type {Parameters<typeof createTagManager>} */` on a matching argument tuple. This provides diagnostics only when JavaScript checking is enabled. It does not validate network data or create a resource automatically. See [strict application setup](/zeruxjs/docs/0.0.4/typescript.md) for the complete NodeNext checker configuration.

## Failure modes and resource lifetime

Administrative editing/publishing requires host session callbacks and origin/version checks. A dashboard hostname is not authentication. Memory reservation/container fallback does not coordinate replicas. Verify that public container JSON never exposes tokens or destination authorization headers, and distinguish server events from untrusted browser claims.

## Verify before deploying

- Exercise the example with the actual owning runtime/browser, not only an imported module.
- Test one invalid input and the relevant unavailable/denied dependency path; do not turn those into a silent success.
- Confirm teardown leaves no owned listeners, timers, connections or stale per-user state.
- Check the production entry point and installed export map; a workspace checkout can contain features not yet published to npm.

## Public option and data contracts

These tables list fields declared directly on the named public contract. Referenced/inherited types retain their own declarations; optional does not imply a default value. Use the lifecycle and configuration chapters above for actual initialization behavior.

### TagRuntimeOptions

| Field                     | Accepted type/value                                                                                          | Optional |
| ------------------------- | ------------------------------------------------------------------------------------------------------------ | -------- |
| `analytics`               | `AnalyticsManager`                                                                                           | No       |
| `requestIds`              | `RequestIdStore`                                                                                             | No       |
| `store`                   | `TagContainerStore`                                                                                          | Yes      |
| `container`               | `TagContainer`                                                                                               | Yes      |
| `prefix`                  | `string`                                                                                                     | Yes      |
| `administration`          | `TagAdminMount`                                                                                              | Yes      |
| `publicOrigin`            | `string`                                                                                                     | Yes      |
| `site`                    | `string`                                                                                                     | Yes      |
| `allowedOrigins`          | `string[]`                                                                                                   | Yes      |
| `serverToken`             | `string`                                                                                                     | Yes      |
| `credentials`             | `Record<string,{secret:string}>`                                                                             | Yes      |
| `destinations`            | `Record<string,{url:string;headers?:Record<string,string>}>`                                                 | Yes      |
| `fetch`                   | `typeof fetch`                                                                                               | Yes      |
| `authorizeAdmin`          | `(request:Request)=>boolean\|Promise<boolean>`                                                               | Yes      |
| `authorizePublish`        | `(request:Request)=>boolean\|Promise<boolean>`                                                               | Yes      |
| `requireBrowserConsent`   | `boolean`                                                                                                    | Yes      |
| `siteByOrigin`            | `Record<string,string>`                                                                                      | Yes      |
| `botPolicy`               | `'allow'\|'drop-known'`                                                                                      | Yes      |
| `dashboardHost`           | `string`                                                                                                     | Yes      |
| `debug`                   | `boolean`                                                                                                    | Yes      |
| `defaultConsent`          | `'granted'\|'denied'`                                                                                        | Yes      |
| `onDebug`                 | `(data:{requestId:string;event:string;matchedTriggers:string[];firedTags:string[];failures:string[]})=>void` | Yes      |
| `onWarning`               | `(message:string)=>void`                                                                                     | Yes      |
| `providers`               | `TagProviderRegistry`                                                                                        | Yes      |
| `ownProviders`            | `boolean`                                                                                                    | Yes      |
| `maxConcurrentDispatches` | `number`                                                                                                     | Yes      |
| `dispatchTimeoutMs`       | `number`                                                                                                     | Yes      |
| `allowDispatch`           | `(event: Readonly<TagEvent>) => boolean \| Promise<boolean>`                                                 | Yes      |
| `maxBodyBytes`            | `number`                                                                                                     | Yes      |
| `maxEventsPerMinute`      | `number`                                                                                                     | Yes      |

### TagContainer

| Field        | Accepted type/value | Optional |
| ------------ | ------------------- | -------- |
| `id`         | `string`            | No       |
| `version`    | `number`            | No       |
| `name`       | `string`            | No       |
| `triggers`   | `Trigger[]`         | No       |
| `tags`       | `TagDefinition[]`   | No       |
| `variables`  | `Variable[]`        | No       |
| `governance` | `TagGovernance`     | Yes      |

### TagDefinition

| Field              | Accepted type/value                         | Optional |
| ------------------ | ------------------------------------------- | -------- |
| `id`               | `string`                                    | No       |
| `name`             | `string`                                    | No       |
| `type`             | `'analytics'\|'ga4'\|'webhook'\|'provider'` | No       |
| `providerRef`      | `string`                                    | Yes      |
| `providerVersion`  | `number`                                    | Yes      |
| `publicConfig`     | `Values`                                    | Yes      |
| `triggerIds`       | `string[]`                                  | No       |
| `enabled`          | `boolean`                                   | Yes      |
| `eventName`        | `string`                                    | Yes      |
| `measurementId`    | `string`                                    | Yes      |
| `credentialRef`    | `string`                                    | Yes      |
| `destinationRef`   | `string`                                    | Yes      |
| `clientIdVariable` | `string`                                    | Yes      |
| `parameters`       | `Record<string,Scalar\|string>`             | Yes      |

### Trigger

| Field           | Accepted type/value                                               | Optional |
| --------------- | ----------------------------------------------------------------- | -------- |
| `id`            | `string`                                                          | No       |
| `name`          | `string`                                                          | No       |
| `type`          | `'event'\|'all'\|'pageview'\|'click'\|'form'\|'history'\|'group'` | No       |
| `schemaVersion` | `1`                                                               | Yes      |
| `composition`   | `{mode: 'all' \| 'any' \| 'not'; triggerIds: string[]}`           | Yes      |
| `event`         | `string`                                                          | Yes      |
| `conditions`    | `Condition[]`                                                     | Yes      |
| `enabled`       | `boolean`                                                         | Yes      |

### Variable

| Field           | Accepted type/value                           | Optional |
| --------------- | --------------------------------------------- | -------- |
| `id`            | `string`                                      | No       |
| `name`          | `string`                                      | No       |
| `type`          | `'constant'\|'event'\|'dataLayer'\|'context'` | No       |
| `schemaVersion` | `1`                                           | Yes      |
| `key`           | `string`                                      | Yes      |
| `value`         | `Scalar`                                      | Yes      |

## Explicit owners and public imports

Use `createTagManager` and `createTagProviderRegistry` from `@zeruxjs/tags` on
the server. Retain the manager and await `close()` before closing borrowed
analytics, document and request-ID stores. A registry is borrowed unless
`ownProviders:true`; close a shared registry only after every borrowing manager
drains. Cancellation stops observation, not arbitrary JavaScript or a previously
accepted external mutation. Saved drafts, preview and import never imply publish.

For replaceable UI, import `createTagAdminClient` from
`@zeruxjs/tags/admin-client` and `mountTagAdmin` from
`@zeruxjs/tags/admin-view`. These browser-safe entries do no work on import.
Explicitly load the view, destroy its owned subtree/listeners on unmount, then
close any client you own. The host still verifies every session, tenant, origin,
CSRF token and publisher grant; hiding a control grants no permission.


## Public entry points

Manifest reference for `@zeruxjs/tags@0.0.4`.

```json
{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    },
    "./admin-client": {
      "types": "./dist/admin-client.d.ts",
      "default": "./dist/admin-client.js"
    },
    "./admin-view": {
      "types": "./dist/admin-view.d.ts",
      "default": "./dist/admin-view.js"
    }
  },
  "engines": {},
  "dependencies": {},
  "peerDependencies": {
    "@zeruxjs/analytics": "0.0.4"
  },
  "bin": {}
}
```


## Package README

# @zeruxjs/tags — server and browser tag manager

Managed triggers, tags, data-layer variables, browser `ttag()` and server `astag()` APIs. Supports Zerux internal analytics, **server-side GA4 Measurement Protocol**, and preapproved HTTPS webhook destinations. Credentials are only resolved on the server. Does not execute arbitrary browser script tags.

## Zerux runtime configuration

```ts
export default {
  analytics: { analytics: 'reporting', enabled: true },
  tags: {
    enabled: true,
    site: 'storefront',
    prefix: '/_tags', // browser: /_tags/ttag.js, collector: /_tags/collect
    publicOrigin: 'https://tags.example.com', // optional for tagging subdomain
    allowedOrigins: ['https://shop.example.com'],
    dashboardHost: 'tags.example.com', // optional restrict dashboard host
    authorizeAdmin: async req => await sessionHasRole(req, 'analytics_admin'),
    credentials: { ga4: {secret: process.env.GA4_MP_SECRET!} },
    destinations: { crm: { url: 'https://hooks.example.net/events', headers: { Authorization: `Bearer ${process.env.HOOK_TOKEN!}` } } },
    debug: process.env.NODE_ENV !== 'production',
  }
}
```

The default container includes an **internal analytics tag** fired for all events. `dashboardHost` applies to admin routes only. Session/role authorization must be supplied by the host application; otherwise **no admin routes are registered**. Admin PUT requires a matching Origin and a monotonically increasing version. Credentials and destination URLs never appear in public container JSON. Direct browser-origin POSTs cannot claim `server` events.

For browser collection with cross-origin tags, put the script on the page:

```html
<script>window.dataLayer=window.dataLayer||[];window.dataLayer.push({event:'preload',properties:{section:'home'}});</script>
<script async src="https://tags.example.com/_tags/ttag.js"></script>
```

```js
const requestId = ttag('event', 'purchase', { value: 49.99, currency: 'USD' });
ttag('set', 'sessionId', 'opaque-session'); // optional stable client_id for GA4
const x = ttag('get', 'sessionId');
// ttag('config', {...}) is intentionally disabled: tenant/debug are server-controlled.
ttag('consent', 'denied');  // telemetry-only switch, unrelated to email marketing consent
window.dataLayer.push({event:'cart_add',properties:{quantity:1}});
```

`ttag('event')` returns the **16-character request ID** immediately; that ID is also attached to the collected event and passed to matching triggers. Events queue asynchronously, attempt `navigator.sendBeacon()` first (CORS-simple text/plain), then `fetch(...,{keepalive:true})` if Beacon cannot queue. Flushing also occurs on `visibilitychange` / `pagehide`. Network delivery is **best-effort**, not an exactly-once guarantee. Browsers that do not provide secure `crypto.getRandomValues()` cannot safely generate IDs and therefore do not send telemetry.

On the server, get a request-scoped function (not a cross-request global singleton):

```ts
const tagService = runtimeServices.get('tags');
const astag = tagService.createAstag({site:'storefront',sessionId:'opaque-user-id'});
await astag('set', 'tier', 'pro');
const result = await astag('event', 'checkout', {value:125});
await astag('get', 'tier');
await astag('config', {pathname:'/checkout'});
await astag('consent', 'denied'); // telemetry control only
```

A server request scope can generate **one** request ID, reserved for the UTC day, and reuse it across related events; browser deliveries use **one ID per POST**. The current UTC day is assigned on the server. The event row contains reserved `zRequestId` / `zRequestUtcDay` scalar properties for joining data. Duplicate request ID reservations are rejected (409), invalid ID (422), unavailable reservation storage (503). For guaranteed cross-replica uniqueness, use the analytics-named shared DB reservation adapter or inject an atomic shared `requestIds` store. Memory-only reservation is development-only.

### Editing tags

Navigate to `/_tags/admin` on the selected dashboard host with an authenticated application session. Create/edit triggers, event matches and conditions; variables can read event properties, dataLayer values, or request context; tags can fire internally, to a configured GA4 stream, or to a preapproved webhook. `/_tags/admin/preview` is a protected **dry run** that shows eligible triggers/tags without dispatching to destinations. The full JSON editor can edit uncommon fields; saving a draft and publishing it are separately authorized, version-checked operations. Registered variables can be used as exact parameter bindings such as `parameters:{revenue:'{{cart_total}}'}`; JavaScript expressions are never evaluated.

A GA4 tag needs `measurementId` (e.g., `G-ABC1234`), `credentialRef` referencing the server-side secret, and a **client_id** via explicit sessionId or a variable; Zerux does not silently invent a cookie or fingerprint. GA4 HTTP success means received, **not necessarily processed**. See [Google's Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events). Webhooks are referenced by `destinationRef` to a server allowlist (not an attacker-controlled URL). Use egress filtering/DNS rebinding protection in production.

### Deployment prerequisites

Connect `analytics.analytics` to a real named @zeruxjs/db connector. Its facade should implement `createTable`, atomic `insert`, `select`, conditional `update` and reliable `affectedCount`. The default store uses `analytics_tag_containers` with optimistic CAS; `analytics_request_ids` enforces UTC-day ID uniqueness and can optionally prune old IDs if the connector supports `delete`. Multi-server nodes must share the same DB and dashboard store; keep admin APIs protected behind HTTPS session auth, CSRF restrictions, reverse proxy limits and role controls. Supply `store`/`requestIds` adapters if your DB is not compatible. Monitor warnings, 409/429/503 rates and outgoing destination failures. No durable queue/replay is implemented yet.

### Optional automatic web triggers

Published `pageview`, `click`, `form`, and `history` trigger types cause the generated bootstrap to register corresponding browser listeners. They emit `page_view`, `click`, `form_submit`, and `history_change` events respectively. Click/form metadata intentionally excludes element text and form field values. Automatic tracking is not enabled unless a corresponding trigger is published. Set `tags.defaultConsent:'denied'` for a site's privacy policy to suppress telemetry before the application calls `ttag('consent','granted')`; this switch is independent of email marketing subscription state. `history` currently listens to `popstate` rather than monkeypatching routing APIs.

## Publishable releases and governance

**Important migration:** `PUT /_tags/admin/api` now **saves a draft**, not the live container. A separate privileged `POST /_tags/admin/publish` makes the draft live. Existing plain versioned containers are treated as live **release 1** until their first draft change; the first save persists a governance envelope in the existing CAS document. No new table is required. Two manager replicas must share the same atomic versioned document store and request-ID adapter; an in-memory store is for development only.

Configure **both** independent authorizers on the server; no publish or rollback is allowed when `authorizePublish` is absent:

```ts
const tagConfig = {
  site: 'shop',
  allowedOrigins: ['https://shop.example.com'],
  siteByOrigin: { 'https://shop.example.com': 'shop' },
  defaultConsent: 'denied',           // for browser AND astag by default
  requireBrowserConsent: true,      // drops unknown client-consent claims
  botPolicy: 'drop-known',
  dashboardHost: 'tags.example.com',
  authorizeAdmin: req => checkRole(req, 'analytics_editor'),
  authorizePublish: req => checkRole(req, 'analytics_publisher'),
  onWarning: message => logger.warn(message),
};
```

An authorizer must verify the user's authenticated session, tenant and role. Both controls are checked for publisher actions; a publisher needs editor/admin access too. By default, all POST/PUT studio writes require same-origin `Origin` checks in addition to these authorizers; the explicit administration policy below permits exact approved UI origins only with host-verified CSRF tokens. Never expose a dashboard publicly. Add CSRF tokens in the host layer if your deployment accepts unusual state-changing request patterns.

| Route                   | Method    | Permission                            | Purpose                                                                          |
| ----------------------- | --------- | ------------------------------------- | -------------------------------------------------------------------------------- | ------------- |
| `/_tags/admin`          | GET       | editor                                | Visual draft editor, dry-run, publish and rollback controls                      |
| `/_tags/admin/api`      | GET / PUT | editor                                | Read full draft / save `{version: prior+1,...}`                                  |
| `/_tags/admin/status`   | GET       | editor                                | Draft revision, live release and bounded release-history metadata                |
| `/_tags/admin/preview`  | POST      | editor                                | Pure trigger dry-run `{name,properties,data,mode:'draft'                         | 'published'}` |
| `/_tags/admin/publish`  | POST      | editor + publisher                    | `{expectedVersion}`; atomic CAS live switch and history append                   |
| `/_tags/admin/rollback` | POST      | editor + publisher                    | `{expectedVersion,releaseId}`; publish a retained snapshot as a new live release |
| `/_tags/ttag.js`        | GET       | public                                | Bootstrap **from published triggers only**, `Cache-Control: no-store`            |
| `/_tags/collect`        | POST      | allowed origin or server bearer token | Browser/server telemetry, validated and fail-soft                                |

The editor shows draft and live revision independently; **saving never fires edited tags**. Preview is pure and does not send to GA4/webhooks or internal storage. Publish rejects missing GA4 credentials or webhook destination references; credentials, actual URL/headers and OAuth tokens are always supplied by trusted server configuration, never dashboard JSON. Rollback preserves the current draft so a publisher may fix/re-publish it later. Revisions are monotonic and concurrent conflicting writers receive HTTP 409. The atomic single-document scheme supports a 12-entry release history, compacted further to keep the complete document below 58,000 bytes for portable SQL `TEXT` implementations. Oversized definitions are rejected until simplified; historical snapshots pruned by the retention policy are not available for rollback. A rollback is a new release, not mutation of old history.

`ttag('config', {site:..., debug:...})` is **no longer accepted**: an untrusted web page may not change the server-issued tenant or debug policy. Arbitrary JavaScript/HTML tags are **not supported**. The studio uses an exact SHA-256 hash for inline script CSP, with scripts otherwise denied; it also denies framing and uses no-store responses. Automatic click/form listeners avoid form field values, text contents and URLs. Browser telemetry uses opaque random request IDs, bounded scalar properties and client-side consent. `ttag('consent','denied')` discards queued events. `defaultConsent:'denied'` gates `astag` until it receives `astag('consent','granted')`, and enables consent checks on browser ingestion by default. The collector drops explicit `denied` claims and, with `requireBrowserConsent:true`, ignores unknown consent. **A browser's `granted` claim cannot prove legal consent**: host consent management and privacy policies remain essential. `siteByOrigin` should be configured for every public origin in multi-tenant deployments; collection-side binding rejects mismatched site assertions. The system never infers identity from IPs or browser fingerprints.

Built-in GA4/webhook destinations have independently bounded 1-second timeouts; provider helpers use the owning evaluation deadline. Failures isolate each tag, with sanitized warnings. Webhook destinations must use public HTTPS DNS hostnames and redirects are disallowed; this **does not remove DNS-rebinding risk**. Enforce outbound DNS/IP allowlists and egress firewall rules in your deployment. GA4 HTTP acceptance is not proof of Google-side processing. No arbitrary remote script execution, guaranteed browser delivery, distributed rate limiting, replay queue or unbounded GTM/GA4 feature parity is provided. Large production deployments should migrate release snapshots to a normalized transactional release table rather than depending on the capped JSON envelope.

## Explicit versioned provider plugins

`createTagProviderRegistry()` owns trusted **server code** registered with `register({id,version,config,destinations,dispatch,dispose?})`. There is no package scanning, remote script loading, eval, or JavaScript-string plugin type. [The public-import example](https://github.com/zeruxjs/zeruxjs/blob/version/v0.0.4/packages/@zeruxjs/tags/examples/provider.ts) creates one app-owned registry and manager. Existing analytics/GA4/webhook tags retain their APIs; custom tags use `type:'provider'`, `providerRef`, integer `providerVersion`, and scalar `publicConfig`. Provider schemas are snapshotted at registration; the same ID/version cannot be replaced. Register retained versions explicitly if old releases must remain runnable/rollbackable.

A registry permits at most 32 registrations. `config` declares at most 25 named string/number/boolean fields, optional `required`, and bounded allowed-value lists. Unknown/wrong-type fields and incompatible provider versions reject before a draft/import or publish transition. Obvious secret-bearing public field names are rejected. This does not detect a secret hidden in an innocently named literal: hosts must review all declared public fields/values. Keep secret clients/options in private server closures or existing server credential/destination configuration; never place them in container variables, publicConfig or exported definitions. Browser bootstrap still emits only server-issued collection settings and published automatic-trigger types, never plugin code, private headers, destination URLs or credential values.

A provider's `dispatch` receives a frozen event/config/parameter snapshot, an AbortSignal and `send(destinationRef, scalarPayload)`. Every send must use both the provider's declared `destinations` list (maximum 16 references) and the manager's configured HTTPS destination allowlist. Provider publish/import/preview checks reject missing declared destinations; payloads are capped at 16 KiB and four send calls per provider-tag dispatch. Requests disallow redirects and responses are cancelled after status inspection. No automatic server retry is added; ambiguous writes must not be transparently replayed. Sends begun without awaiting are still tracked/drained, and sends attempted after the provider callback completes reject. Provider callbacks should still await owned work and cooperate with cancellation. The visual editor recognizes provider references/versions/public JSON and group composition; server validation remains authoritative.

Registered code is trusted application code, **not a sandbox**: direct global fetch, timers or arbitrary side effects created by that code cannot be intercepted safely. Use the supplied bounded send helper and explicit disposer, and review additional SDK/network policy yourself. The package does not promise vendor/plugin parity or that an HTTP 2xx means provider-side processing. The existing web-stream Measurement Protocol behavior follows Google's [server-only secret and identifier contract](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events); no new vendor SDK or browser Measurement Protocol secret is introduced.

### Trigger and variable schemas

Existing trigger/variable kinds remain supported; optional `schemaVersion:1` explicitly identifies their current schema (omission means V1). Unsupported versions reject. A trigger can additionally use `type:'group'` with `composition:{mode:'all'|'any'|'not',triggerIds:[...]}`. Groups reference existing trigger IDs, accept 1–20 distinct inputs (`not` exactly one), and may have their own existing conditions. IDs are memoized within one event only. Cycles, unknown references, depth over eight and validation/evaluation work over 5000 visits reject. Disabled inputs evaluate false, so `not` of a disabled input evaluates true; disabled groups themselves remain false. These are Boolean conditions, not authorization rules.

Variables remain constant, event, dataLayer or the enumerated request context; bindings remain exact `{{variableId}}` strings. There is no expression language, nested object evaluation or arbitrary callback in a saved container. Unknown document/trigger/variable/tag/condition fields now reject instead of being silently stripped: move host-only metadata into separate private storage. Constants and names may contain inert text; text that resembles JavaScript is never evaluated. Definitions reject functions, accessors, classes, cyclic/prototype-polluting JSON, nesting beyond 32, more than 20,000 visited nodes, or over 128 KiB before reconstruction. Existing governance still bounds persisted releases to 58,000 bytes.

### Dispatch lifetime, consent and scope

Managers default to eight actual pending evaluations and a 2000 ms observation deadline (configurable maxima 64 and 30 seconds). Event JSON is bounded by `maxBodyBytes` (default 16 KiB); data layers have 40 scalar fields. A timeout returns `dispatch_cancelled`, but capacity remains occupied until native work and tracked sends actually settle. Overload returns `dispatch_overload`; reentrant calls from a provider to the same manager return `recursive_dispatch`. This is cooperative cancellation, not forced JavaScript termination, and partial external acceptance may already have occurred. Existing direct `evaluate` is a trusted server API for an already-reserved request ID; remote callers must use the collector, which retains atomic request-ID reservation and origin/site checks.

`astag` rechecks its mutable consent state before each tag and supplied send. Optional host `allowDispatch(event)` must return literal true and runs again at dispatch boundaries, letting the application enforce fresh consent/tenant policy. A change cannot recall an already-started request. Browser claims are still not proof of legal consent, and marketing subscription state stays separate. With a fixed `site`, server contexts/config calls reject another site; multi-site collector requests may use only the independently origin-validated `siteByOrigin` mapping. Direct server sites without a fixed site remain the host's authenticated request responsibility. Pathname query/hash suffixes are removed before dispatch.

Await `manager.close()` to stop new collection/dispatch, abort cooperative work, drain actual settlement, then dispose providers when `ownProviders:true`. Registries are borrowed by default; hosts sharing one close it **after all borrowing managers drain**. Database stores, analytics managers and request-ID stores remain borrowed. `status()` exposes fixed closed/pending counters without site/event payload labels. Noncooperative callbacks/SDKs can delay shutdown. Constructors/registrations are inert; if setup validation fails, the host still owns any registry/resources it created before construction.

Browser queues are now bounded to 100 pending items/16 KiB per POST, 40 state keys, the last 500 retained dataLayer entries and the last 500 initial preload entries. Oversized/overflow events drop without remote dispatch; deny still purges the queue. The existing single 409-ID-conflict retry remains capped at one and checks consent; no retry follows an ambiguous transport failure. The generated bootstrap's listeners remain document-owned, not a general SPA mount/unmount API.

### Controlled transfer and preview

`exportTagDefinition(container,registry?)` emits `{format:'zerux-tags',schemaVersion:1,container}` without governance/history. `importTagDefinition(text,registry?)` validates this envelope, a 58,000-byte transfer budget, public fields and provider compatibility before returning a definition. Provider tags require the explicit registry in these codecs and in direct `saveTagDraft`/`publishTagDraft`/`rollbackTagRelease` calls (new optional final argument). Standalone codecs never mutate storage or publish.

Authenticated managers additionally expose `GET /_tags/admin/export` and same-origin JSON `POST /_tags/admin/import` with `{expectedVersion,definition:<envelope string>}`. Imports reject incompatible schemas and unresolved active destinations, retain container identity, ignore the imported revision and CAS-save a **draft** at expectedVersion+1. Admin authorization must verify the manager's tenant/site. Publishing still requires the separate publisher callback and a second explicit transition; imports cannot carry governance or activate a draft automatically. Rollback rechecks registered provider versions and configured destinations. Server JSON/export responses are private no-store. Admin body reads are bounded/cancellable with a five-second read deadline; host authorization callbacks still require their own cooperative limits.

Preview remains a pure authorized dry-run: no provider callback, network call, analytics write or ID reservation occurs. It uses the same group matcher and provider schema checks, with default/fresh host consent and destination readiness gates. Unknown preview scope fields reject; a preview cannot supply another site or override a denied consent policy. A default-denied instance therefore previews no candidates unless the host's actual configuration permits dispatch; there is no client-supplied consent bypass.

Qualification includes existing collector/governance checks plus provider version/import failures, recursive groups, consent changes, denied destinations, outage/no-replay, overload/deadlines and actual-settlement disposal fixtures. These tests use injected transports/VM browser fixtures, not live Google/webhook or browser acceptance. Live prerequisites remain a shared durable container/request-ID store, tenant-authorized sessions, consent management, approved egress/DNS policy and dedicated configured provider credentials. No live credentials, user applications or production destinations were used.

## Configurable headless administration

The package keeps telemetry mounting (`prefix`, collection `publicOrigin`, collector `allowedOrigins`) separate from **administration**. `TagRuntimeOptions.administration` accepts `basePath` for API routes, `uiPath` for the authenticated HTML page, optional API `publicOrigin`, and `serveUi:false` for a completely separately hosted UI. Defaults preserve `/_tags/admin` and its existing endpoints. Paths must be explicit root paths; conflicting route registrations reject. The host mounts the returned routes; no application, asset compiler or listener starts automatically.

See [the server mounting example](https://github.com/zeruxjs/zeruxjs/blob/version/v0.0.4/packages/@zeruxjs/tags/examples/admin-mount.ts) and [the browser client/slot example](https://github.com/zeruxjs/zeruxjs/blob/version/v0.0.4/packages/@zeruxjs/tags/examples/admin-browser.ts). These are explicit standalone package integrations; they do not add automatic framework configuration discovery or launch another UI server.

### Exact CORS and session/CSRF ownership

Administration has its **own** `allowedOrigins` (maximum 16 exact origins), independent of collector origins. HTTPS is required except explicit localhost HTTP development. Wildcards, URL credentials, paths, queries and opaque/null origins reject. CORS responses echo only an exact approved request origin, allow credentials and vary on Origin/Cookie/Authorization; all admin responses are private no-store. Actual in-flight admin work is capped at eight by default (`administration.maxConcurrent`, maximum 64); overload rejects before starting another authorizer/store operation. Preflight permits only GET/POST/PUT and Content-Type/X-Zerux-CSRF/Authorization. Preflight performs no authenticated data operation; each real request still calls the host authorizer. Unapproved Origin requests are rejected, including reads. See the [Fetch CORS and credentials standard](https://fetch.spec.whatwg.org/#http-cors-protocol).

Configuring cross-origin administration requires `verifyCsrf(request)`. With that verifier configured, **every write**, including same-origin validate/preview, requires JSON, a nonempty `X-Zerux-CSRF` header and literal-true host verification. Keep tokens session-bound, expiring and verified against the authenticated principal; a static shared token or Origin check alone is not the supplied CSRF integration. Default deployments without this option retain the existing same-origin Origin gate. `issueCsrfToken(request)` can provide the builtin authenticated page's per-session token; external UIs obtain it through a separately authorized host flow. Token issuance never bypasses `authorizeAdmin`. The token is intentionally visible to its authorized UI, while SMTP/provider/destination secrets remain server-only.

Host session/auth callbacks must validate tenant, site and roles for this manager/store. `authorizeAdmin` gates read/save/validate/preview/import/export, and `authorizePublish` remains an additional independent publish/rollback check. CORS is not authorization. `dashboardHost` still checks the API request host, not the browser UI Origin. Neither IP/forwarded headers nor a visible UI button grant permission.

The package creates no cookies or sessions. A UI subdomain does not automatically receive API-host cookies: choose secure host-scoped HttpOnly session cookies, SameSite policy and credentialed fetch deliberately. Cross-origin and cross-site are different boundaries, and browsers may block cross-site cookies despite CORS approval. Avoid widening cookie Domain just to make an integration appear to work; use an explicitly authenticated host/BFF/token design when required. Every script on an approved UI origin is trusted to read that user's admin responses/CSRF token, so do not share that origin with untrusted applications. No browser cookie-policy qualification is inferred from request fixtures.

### Stable operations and optimistic edits

The API base exposes:

| Suffix                  | Method     | Operation                                                                                  |
| ----------------------- | ---------- | ------------------------------------------------------------------------------------------ |
| `/api`                  | GET / PUT  | Read / CAS-save full draft; create/update/delete entries through validated document arrays |
| `/validate`             | POST       | Validate schemas/provider versions without storage mutation                                |
| `/preview`              | POST       | Authorized dry-run; consent and destination policy still apply                             |
| `/status`               | GET        | Draft/live revisions and retained release metadata                                         |
| `/publish`, `/rollback` | POST       | Separate publisher-authorized CAS transition                                               |
| `/import`, `/export`    | POST / GET | Versioned draft-only definition transfer                                                   |

Saving requires `version=previous+1`; publishing/rollback require `expectedVersion`. A CAS conflict returns 409. There is no silent merge, last-writer-wins overwrite, automatic retry or implicit publish on save. Provider compatibility, destination configuration, consent and tenant checks from the execution contract remain intact. Imported definitions contain inert public data, not executable slots, credentials or callbacks. Stored labels remain text, including strings that resemble markup; notes/HTML fields are not supported expression extensions.

Optional `@zeruxjs/tags/admin-client` exports `createTagAdminClient({apiBaseUrl,csrfToken,...})`. It is DOM-free on import and makes no requests until an operation is called. Its load/save/validate/preview/status/publish/rollback/importDefinition/exportDefinition methods target fixed suffixes, always use credentialed no-store Fetch, reject redirects, bound request/response JSON to 128 KiB and default to four pending operations/10 seconds (maxima 16/30 seconds). Each accepts cancellation. `TagAdminClientError` distinguishes conflict, denial, invalid input, unavailable service, cancellation, overload and close, preserving committed/uncertain outcomes. Native pending work keeps its slot until settlement; await client.close() to abort cooperative work and drain it. Host CSRF callbacks and injected fetch implementations must cooperate with cancellation; arbitrary JavaScript cannot be forcibly stopped.

### Audit receipts and dependency failures

Durable governance history already records successful publish/rollback releases and timestamps in the same CAS document. Optional `administration.auditContext(request)` plus `audit(event)` adds host-owned durable actor auditing. Configure both together. Events are V1, with unique operationId, intent/committed/conflict phase, action, opaque tenant/actor, expected/result revision, optional rollback release and a definition hash; no names, notes, raw definitions, destination URLs, cookies or provider credentials are emitted.

A failed intent sink prevents mutation and returns 503 with `committed:false`. A postcommit sink failure returns 503 with `committed:true` and the new revision; do not retry the transition. Intent and configuration storage are separate resources, not a distributed transaction. An unresolved intent is not proof of commit. Ambiguous storage/transport failures report an unknown/uncertain outcome; reload authorized status and reconcile before any retry. A successful commit whose status read fails still returns its committed revision rather than claiming validation failure. Hosts own audit durability, idempotence/reconciliation and callback deadlines. Manager.close() drains active admin operations/audit callbacks as well as dispatch jobs before owned provider disposal; noncooperative sinks can delay close.

### Replaceable view, assets and CSS

`@zeruxjs/tags/admin-view` exports `mountTagAdmin(host,client,{slots})` and scoped `tagAdminStyles`. The optional view owns only its subtree/listeners and borrows the client. It provides labeled JSON editing, validate/save/publish/rollback controls, a live status, native keyboard interaction and conflict messages that retain unsaved edits. It does not fetch on import/mount; explicitly call view.reload(). Call view.destroy() on unmount and separately close a client you own. Host toolbar/editor slots return synchronous disposers; they are trusted UI code and cannot be stored in definitions. Partial slot setup failures remove the owned view.

Built-in CSS and headless styles expose `--tags-border`, `--tags-radius`, `--tags-focus` and `--tags-gap` where used. Preserve contrast, labels and visible focus in overrides. No positive tabindex or invented label text is used. Custom slots remain responsible for their own accessibility semantics and cleanup.

For host-built assets, `styleHref` and `scriptSrc` are independent absolute/root asset URLs with safe filenames, no query/hash/credentials or traversal. The library does not copy/build/serve them. styleHref supplements builtin styles; scriptSrc **replaces** the builtin inline client and must contain the host's actual UI entry. Authenticated HTML exposes `meta[name=zerux-tags-api]` and `meta[name=zerux-tags-csrf]` for that entry. Inline script/style hashes and explicit configured asset origins are reflected in CSP, with no unsafe-inline style exception. External module assets still require their own server's appropriate CORS/MIME policy. A separately hosted UI owns its own HTML, asset paths and CSP; do not infer its asset base from the API URL.

Qualification includes denied reads/edits/publish, exact-origin preflight/write rejection, custom mount/assets, CAS conflicts, known-release rollback, audit pre/postcommit failure, client outage/deadline ownership and jsdom stored-XSS/labels/slot cleanup fixtures. This is not live-browser, cross-domain session/cookie or assistive-technology acceptance. Those checks require a functioning browser and host-owned HTTPS session/CSRF/audit setup; no user-managed applications were started or restarted.


## Implementation sources

- [packages/@zeruxjs/tags/README.md](https://github.com/zeruxjs/zeruxjs/blob/version/v0.0.4/packages/@zeruxjs/tags/README.md)
- [packages/@zeruxjs/tags/package.json](https://github.com/zeruxjs/zeruxjs/blob/version/v0.0.4/packages/@zeruxjs/tags/package.json)
