# Xaere Codes publisher backend example

This Node.js 22 module is intended for a CMS, a broadcast scheduler or another
trusted publisher backend. It issues one or a bounded batch of signed XR Codes
through Xaere Core and can fetch the corresponding WAV signal for a broadcast
pipeline.

The `xak_` API key is a server credential. Keep it only in the publisher's
secret manager; never send it to a web page, a Flutter application or a device.
Create a project-bound key with `codes:write` for issuance/revocation and
`codes:read` for listings and WAV rendering. Xaere keys contain at least 32
URL-safe random characters after the `xak_` prefix.

```js
import { createXaerePublisherClient } from './server.js';

const codes = createXaerePublisherClient({
  apiBase: process.env.XAERE_CODES_API_BASE ?? 'https://api.xaere.io',
  apiKey: process.env.XAERE_CODES_API_KEY,
  organizationId: process.env.XAERE_ORGANIZATION_ID,
  projectId: process.env.XAERE_PROJECT_ID,
});

const code = await codes.issueCode({
  issuer: 'cms-production',
  url: 'https://publisher.example/experience/42',
  expiresAt: '2030-01-01T00:00:00.000Z',
  idempotencyKey: 'cms-job-20260813-content-42',
});
const wav = await codes.renderAudio(code.xr_id);

// If the response was lost, recover that exact receipt. This route cannot
// issue a replacement Code and requires the original fields and key.
const recoveredCode = await codes.recoverCode({
  issuer: 'cms-production',
  url: 'https://publisher.example/experience/42',
  expiresAt: '2030-01-01T00:00:00.000Z',
  idempotencyKey: 'cms-job-20260813-content-42',
});

const issued = await codes.issueBatch({
  count: 25,
  issuer: 'broadcast-scheduler',
  url: 'https://publisher.example/live',
  expiresAt: '2030-01-01T00:00:00.000Z',
  idempotencyKey: 'broadcast-job-20260813-live',
});

// If that exact call timed out after Core accepted it, recover its receipt.
// Keep all fields and the stable key byte-for-byte equivalent to the first call.
const recovered = await codes.recoverBatch({
  count: 25,
  issuer: 'broadcast-scheduler',
  url: 'https://publisher.example/live',
  expiresAt: '2030-01-01T00:00:00.000Z',
  idempotencyKey: 'broadcast-job-20260813-live',
});
const wavZip = await codes.renderAudioBundle(
  issued.data.map((item) => item.xr_id),
);

const firstPage = await codes.listCodes({ limit: 50 });
const secondPage = firstPage.next_cursor
  ? await codes.listCodes({ limit: 50, cursor: firstPage.next_cursor })
  : null;
await codes.revokeCode(code.xr_id);
```

`issueBatch({ count, ...template })` sends exactly one request to the bounded
Core batch endpoint (1–100 Codes); it does not create a validation Code first.
Pass a stable, job-scoped `idempotencyKey` when your scheduler can retry a
request after losing its response. Xaere retains only its hash for 24 hours and
returns the exact original Code or batch without consuming quota twice. If no
key is supplied, the client creates a fresh random key for that call.
`recoverCode()` and `recoverBatch()` are deliberately different from issuance:
they require the original stable key and exact fields, call only their
read-only recovery endpoint, and never create a replacement key or fall back
to issuance. Recovery remains possible while the 24-hour receipt exists even
if the Code's own expiry has just passed. An unknown/expired receipt fails with
404 and a changed request fails with 409. Store the job fields and key together
in the trusted publisher database until the receipt has been durably committed.
`renderAudio()` accepts at most 2 MiB, stops a streamed response as soon as it
exceeds its declared length, and verifies both that exact length and the
RIFF/WAVE signature before returning bytes to the broadcast pipeline.
`renderAudioBundle()` accepts one unique selection of at most 100 XR Code IDs,
uses the tenant- and project-scoped Core route, and accepts at most 66 MiB. It
reads within the declared length before it verifies the stored ZIP entry order,
exact `<xr_id>.wav` names, RIFF/WAVE
signatures, entry count and central-directory boundary before returning bytes;
an unexpected or foreign filename rejects the complete bundle.
`listCodes()` uses a bounded keyset page. Treat `next_cursor` as opaque and pass
it back unchanged; null means the project listing is complete.
Every Core JSON response must use `application/json`, contain an object and fit
within 1 MiB. Declared and received lengths must agree when Core supplies a
`Content-Length`; malformed, streamed-oversized and non-JSON responses are
rejected before the publisher workflow consumes them. Reflected API error text
is limited to one bounded line. Successful responses must also match their
closed operation contract: signed-frame fields must agree, batch cardinality
must equal the requested count, listed Codes must belong to the configured
project, identifiers must be unique, cursors must remain opaque and bounded,
and a revocation must name the requested XR Code.

For an XR Audience campaign, pass an active project-owned
`audienceIntegrationId` when issuing the Code. The integration HMAC secret is
not used here and must remain only on the publisher backend that forwards
minimised measurement events.

Run `node --test server.test.js` to verify tenant scoping, single batch
issuance, explicit single and batch no-creation recovery, listing/revocation, bounded WAV/ZIP
validation and URL/credential safeguards.
