# PULSE-1 offline QR transport, version 1

Status: reference implementation and adversarial project tests. The separate
[mobile browser lab](pulse-qr.html) exchanges real QR pixels between separately
provisioned phone and executor roles. Physical phone cameras, scan latency and
cross-device clocks have **not** been field-validated. No independent audit or
proximity, identity, biological-presence or remote-payment-settlement claim.

This is transport only. Existing HAP-1, WA-1, PULSE-1 policy and pulse signed bytes
are unchanged. A scan cannot issue a ticket, produce an action permit, execute an
action, satisfy WebAuthn, reset a counter, restore budget or undo revocation. All
execution goes through the existing dedicated PULSE gate and its final dispatch
capability, including a separate fresh HAP challenge and exact-action owner permit.
The lab provisions test tickets only through its explicit setup control and has
no payment provider or execution route.

## Pins and wire

Trusted provisioning supplies the **exact signed ticket and policy**, independently
compared issuer, executor and delegate public keys, and durable owner/executor
stores. Never derive a pin from an incoming scan. The existing ticket/policy
verifiers check the issuer signature and hashes; fingerprints must match the
pinned ticket's issuer, audience and delegate. The executor authenticates its
challenge with its existing Ed25519 key. The phone authenticates the unchanged
pulse with its existing issuer key. No QR signing authority or remote grant server
is introduced. Private keys, private heartbeat bands and raw readings stay local.

`parsePulseQr()` limits ASCII canonical JSON to 2,048 bytes and calls the existing
`parseWire()` before any cryptographic or state operation. Duplicate keys, numeric
aliases, unknown/extra members, unknown versions, noncanonical encodings and
oversized payloads refuse. Scans are data, never URLs, scripts, keys to import or
compressed objects. The image codec makes no trust decisions.

| Message | Exact members |
| --- | --- |
| Executor challenge | `schema, issuer, executor, delegate, ticketHash, policyHash, session, issuedAt, expiresAt, signature` |
| Phone response | `schema, challengeHash, pulse` |

Challenge schema is `PULSE-1/qr-challenge/1`. Bindings use existing HAP key IDs and
canonical hashes. `session` is the gate's fresh 32-byte CSPRNG nonce. Every issued
QR challenge calls `gate.connect()` and uses a **new session**, including repeated
challenges on the same devices. One session names exactly one immutable challenge
and deadline; challenges are never reissued with a different deadline. Times are
nonnegative safe Unix millisecond integers. `expiresAt` is
`min(ticket.expiry, issuedAt + 5000)`, exclusive. The executor signs
`UTF8(schema + "\n" + canonical(bodyWithoutSignature))` with Ed25519; signature is
128 lowercase hex characters, with no trailing newline in the signed bytes.

Response schema is `PULSE-1/qr-response/1`. `challengeHash` hashes the complete signed
challenge. `pulse` is the **unchanged** PULSE-1 signed object, including its original
five-second duration. The envelope is not a second authority signature. The gate
retains the challenge and compares its hash; the pulse's signed fresh session
binds it to that exact challenge. Phone and executor check ticket/policy/key pins,
time and session. The pulse may not predate the challenge. Changing envelope
metadata cannot turn another session's pulse into a response to this challenge.

## Consumption, deadlines and cancellation

The phone verifies the executor signature before reserving the session with durable
CAS in a dedicated record. Duplicate and concurrent scans cannot elicit a second
pulse, including across normal phone restarts/tabs. Reservations persist until their
challenge deadline; at most 128 simultaneously unexpired sessions are retained.
A failed/uncertain signing attempt burns the reservation and may burn a source
counter. The executor must create a new challenge. Missing previously provisioned
state refuses; `initialize:true` is only for first provisioning with a separate
retained marker. Clock rollback refuses. Source counters and executor counter
high-water marks remain the existing durable PULSE mechanisms.

The executor retains one ephemeral pending challenge, claims it synchronously
before awaiting acceptance, and never retries an uncertain acceptance. Concurrent
scans have at most one successful reception. Authentic rollback/equal counters
still refuse in the gate's CAS. Restart, reconnect or another challenge discards
the old pending exchange and conservatively invalidates old leases. Invalid scan
inputs never modify a valid accepted lease or lower a counter.

`gate.accept(ticket, pulse, {deadline: challenge.expiresAt})` stores an optional
trusted transport deadline. This argument can only **shorten** the signed lease.
Acceptance, status, consumption and the final dispatch check all enforce the
minimum of this deadline, the pulse deadline, HAP action deadline, ticket expiry
and any required heartbeat deadline. A pulse signed a second after its challenge
cannot buy a sixth second. Delayed signing or storage acknowledgements cannot
extend the window. Omitting the option preserves the WebRTC adapter behavior.
The raw pulse's `expiresAt` is unchanged; gate status reports the effective deadline.
Read-only signature/interop inspection of a raw pulse does not recover private
transport state and never authorizes execution.

`disconnect()` immediately fences pending reception and durably closes it. Only an
already accepted lease remains usable once, at its original effective deadline,
with a separately issued valid action permit. `stop()` immediately fences reception
and pending dispatch, clears the gate session/lease, and persists the stop. Phone
`stop()` prevents further responses and fences in-flight sensor/signing work; it
cannot notify a disconnected executor, whose accepted lease expires normally.
Phone `revoke()` also durably stops that source ticket. Use the existing authority
to sign the normal HAP revoke and deliver it to the executor.

Executor `revoke(canonicalBundleText)` uses strict HAP wire parsing, stops the local
gate and invokes the existing authenticated revoke verifier. Only successful durable
completion is acknowledgement. A received revoke wins at dispatch. Large history
bundles use a local text/file transfer, not truncation, chunk reassembly or a new
revoke grammar. A sender tap is not a receiver acknowledgement. No transport can
undo an effect already dispatched or instantly revoke an unreachable executor.

One live transport owns each gate, and all effects/revokes use that same gate.
Independent workers need the existing coordinated dispatch boundary or separate
executor identities/tickets; shared storage alone does not broadcast local fences.
The lab holds an exclusive Web Lock per role and stops on page exit. Backgrounding
closes reception and camera; resuming requires an explicit fresh exchange.

## Captured-image limitation

A captured *still-valid* QR can be the first delivery in its exact original
session. An optical copy and the original contain identical bits; this protocol
cannot tell them apart. It refuses duplicates, stale scans, other tickets, policies,
keys, executors, sessions and deadlines. A cooperating real-time relay remains
possible. A scan proves neither physical distance nor biological presence. Do not
market this as a proof of proximity or a screenshot-proof human-presence ceremony.

Clock integrity, durable storage, uncompromised endpoint code and independently
retained pins are assumptions inherited from PULSE. Restoring an old database,
malicious OS clocks or deleting all identity/provisioning markers require stronger
external checkpoints. No signed timestamp alone proves physical signing time.

## Integrating

```js
const receiver = await createPulseQrExecutor({
  ticket, policy, issuerPublicKey, delegatePublicKey,
  identity: executorIdentity, executor: gate,
});
const phone = await createPulseQrPhone({
  ticket, policy, issuerPublicKey, executorPublicKey, delegatePublicKey,
  source, store: durableQrReservations, initialize: firstProvisioningOnly,
});
const challengeText = await receiver.challenge(); // show executor QR
const responseText = await phone.respond(challengeText); // scan, then show phone QR
await receiver.accept(responseText); // scan; transport result only
// Obtain the existing exact-action HAP/WA permit separately, then gate.execute().
```

The self-hostable lab has phone and executor links, public setup data and explicit
fingerprint comparison, canvas QR rendering, local camera/image decoding and exact
text import. The decoder is bundled locally (jsQR); it does not depend on native
BarcodeDetector availability, remote image services or CDNs. HTTPS/localhost is
required for the browser cryptography, camera and durable state. Load both pages
before going offline or explicitly save the public tool with its service worker.
Five seconds includes both scans; missed windows refuse instead of offering grace.
The lab fixes heartbeat to OFF for its newly created test tickets; the transport
API continues to enforce pre-provisioned required-heartbeat policies.

QR codec dependencies are pinned in `vendor/qr/package-lock.json`, with licenses
in `vendor/qr/LICENSES.txt`. They only turn bytes into pixels and back; trust logic
is in `pulse-qr.mjs`. Rebuild offline after installing pinned build dependencies:

```sh
npm ci --prefix hap1/vendor/qr --ignore-scripts
node scripts/build-hap1-qr-codec.mjs --check
node --test scripts/hap1-pulse-qr-regressions.mjs scripts/hap1-pulse-regressions.mjs
HAP1_PLAYWRIGHT_MODULE=/absolute/path/to/playwright/index.mjs \
  node scripts/hap1-pulse-qr-browser.mjs
python3 scripts/build-hap1.py --check
```

`pulse-qr-cases.json` enumerates hostile scenarios; the regression runner generates
actual signed fixtures with isolated keys and stores. Additional regressions cover
strict parsing, every binding, shortened dispatch deadlines, asynchronous stop,
revoke, clock rollback, required heartbeat, durable reservation races and missing
state. The Chromium test uses separate browser contexts, actual PNG QR decoding,
network-offline operation and reload, durable IndexedDB, delayed camera permission,
mobile layout and received revocation. Its clock and camera cancellation are test
fixtures; it is not physical-device field validation. CI runs both the QR and
existing authority/PULSE suites and checks the packaged public files and archive.
