# 1-z-2 integration spec

You are generating or modifying an app that should connect to 1-z-2 (say it
"one-z-two"). 1-z-2 lets independently made apps exchange useful items even
when they organise the information differently. It adapts between app formats,
not human languages. DO NOT design for anyone else's schema; use this app's own
natural data shapes everywhere.

Relay URL: https://relay.1-z-2.com

Not a JavaScript or TypeScript app? If this app is written in Python, Swift,
Kotlin, Go, PHP or anything else, there is no SDK file to vendor: read this
page for WHAT to build, then fetch https://relay.1-z-2.com/integrate-http.md
and follow it for HOW — it gives every step below as a plain web request.

Data flow — the facts, stated before anything is wired in:

- Nothing transmits automatically. Data leaves this app only when its user
  explicitly sends a specific item to a specific person (step 6). No sync,
  no background upload, no analytics; the SDK contacts the relay only when
  a call in this spec runs.
- Recipients are consented contacts only. A first message to anyone requires
  that person to accept a contact request first (step 7). Strangers cannot
  receive, look up, or discover this app's data through the relay.
- Identity is a keypair generated on this device — not an account. The
  private key never leaves the machine; the relay holds the public half.
  No email, password, or personal profile is required.
- The relay does read message payloads: translation between differently-
  shaped apps requires the plaintext — that is the product. Retention is
  bounded, and the relay accounts for itself in machine-readable form:
  GET /retention (authenticated) returns exactly what it holds for this
  handle and when each part expires; message bodies have an end date.
- Published capability examples (step 3) are stored durably — they are what
  transforms are compiled from. Make them faithful to the app's real shape
  with representative values, not rows lifted from real people's records.
- If this app holds personal data about people other than its user (a
  roster, someone else's travel details), show the user exactly what a send
  will contain before their first send. Sharing other people's data is the
  user's decision; the app's job is to make it an informed one.

What to build — the short version. Tell the user this list, in plain words,
before you start, so they know what they are getting:

  Every app gets the same five parts, added the first time 1-z-2 goes in —
  the user should never have to invent these. Put them together in one
  "1-z-2" / "Friends" area of the app, named in the app's own style:
  - Sharing with    — who the user is connected to; add people, accept
                      requests (step 7).
  - Shared with me  — items friends sent, kept apart from the user's own
                      (steps 4–5).
  - Send            — a "send to contact" action on each shareable item
                      (step 6).
  - Conversation    — plain notes between contacts (step 9).
  - Updates         — what friends' apps have added lately, with a
                      "would you like to add this?" button (step 10).
  Underneath: connect (steps 1–2), describe what the app sends and receives
  (step 3), and a self-test before handing over (step 8). None of it is
  optional. An app with nothing of its own to share yet still gets all five
  parts — Conversation and Updates work from day one, and the Updates feed
  is how its owner discovers what to build next.

  The user decides these — ask, don't assume:
  - WHICH kinds of item the app shares. Not every feature needs to be
    sendable; anything private stays unpublished and never leaves the app.
  - Which shareable features friends see in their Updates feed. On by
    default; pass `announce: false` when publishing one the user would
    rather not advertise (it still works, it just isn't news).
  - Whether received items can be merged into their own data (step 4).
  - Whether friends of friends can find them. Introductions are off by
    default; the app offers the switch (step 7) and never turns it on
    itself.

  Look before you invent. If the request names friends who are already on
  1-z-2, or the roster (step 7) already has contacts, look each one up
  before choosing what this app shares — the directory is public and
  needs no consent:
    const { capabilities } = await relay.lookup("sam");
  Each entry has a name, a direction ("send" or "receive") and a plain
  description. Propose one or two features that would RECEIVE what those
  friends already send — framed as "what @sam could send you", built in
  this app's own style and data shape, never a copy of their app — and
  let the user choose. With no friends on 1-z-2 yet, publish the one item
  the user most wants to share and stop there: Conversation and Updates
  work from day one, and the Updates feed brings ideas as friends arrive.

  When you finish, tell the user what to do next, in this order: open the
  app, add a friend's @handle on the contacts screen, wait for them to
  accept, then send them one item. Say that the very first send to a new
  person takes about a minute and later ones are instant.

Integration requirements:

1. Vendor the SDK — one file, zero dependencies. Download
   https://relay.1-z-2.com/sdk.js and save it into this project as `relay-client.js`
   (for example: `curl -fsSL https://relay.1-z-2.com/sdk.js -o relay-client.js`), then:
     import { RelayClient } from "./relay-client.js";
   If this project uses TypeScript, also download https://relay.1-z-2.com/sdk.d.ts
   and save it beside the SDK as `relay-client.d.ts` — it declares the
   whole public surface, so nothing below needs guessing or `any`.
   Do not modify the vendored file, and do not import the SDK from a URL:
   the vendored copy is code you can read and pin before integrating it; a
   remote import is neither. If this environment genuinely cannot add a
   file to the project, stop and tell the user — they can download
   https://relay.1-z-2.com/sdk.js themselves and add it. Import from the URL only if
   the user, told this plainly, chooses it anyway.

2. If the request for this app mentions an existing credentials or identity
   file (a `.relay-<handle>.json` — claiming a handle on the relay's website
   saves one to the user's Downloads folder, where the leading dot can make
   it hidden), that handle is ALREADY registered. Get the file into place
   BEFORE the first connect, and never register that handle fresh: in Node,
   copy it into the directory the app runs from; in a browser, offer a small
   "import identity file" file input, parse its JSON, and call
   RelayClient.importCredentials({ handle, credentials }).
   Otherwise, on first launch ask the user to pick a handle (lowercase,
   hyphens ok). Either way, connect:
     const relay = await RelayClient.connect({ handle, relayUrl: "https://relay.1-z-2.com" });
   Credentials persist automatically (localStorage in browsers, a
   `.relay-<handle>.json` file in Node); reuse silently on later launches.
   The SDK signs every request with the app's device key and, on first run,
   locks the app to signed requests — a stolen bearer token alone cannot act
   as this app. Environments without WebCrypto stay on bearer-token auth
   automatically; nothing to configure either way. One consequence: raw
   `fetch` calls against the relay with just this app's token will be
   refused — go through the SDK client. (A part of this project that is
   not JavaScript can still talk to the relay, by signing its own requests:
   https://relay.1-z-2.com/integrate-http.md says how.)
   Registration may be invite-only. If connect() fails with 403 saying so,
   ask the user for their invite code (next to the handle prompt, first
   launch only) and pass it as `invite:` alongside the handle — it is
   used once, at registration, and never stored.
   If connect() fails with "handle taken" (409) — or is refused as
   invite-only (403) for a handle the user believes is already theirs — the
   user most likely owns this handle and registered it elsewhere (for
   example on the 1-z-2 website, which downloads a `.relay-<handle>.json`
   identity file). Recover exactly as above: in Node, ask them to put that
   file in the project directory and retry; in a browser, offer the "import
   identity file" input, call
   RelayClient.importCredentials({ handle, credentials }), and retry
   connect(). Never ask the user to paste key material into a chat or a
   code file.

3. After connecting, publish one capability per kind of data this app could
   share or accept, using REAL example objects from this app's own data model
   (no schema authoring — the relay infers it from the example):
     await relay.publish({
       name: "workout",              // short kebab-case noun for the concept
       direction: "send",            // and/or a "receive" twin
       description: "a logged workout: exercises, sets, reps, duration",
       example: {...a real object exactly as this app stores it...},
     });
   Publish BOTH directions when the app both produces and consumes the
   concept. Descriptions must say what the data MEANS in plain language —
   the relay matches capabilities between apps by meaning, not by name.
   Re-publish on every app start (cheap, and it keeps the relay current if
   this app gets regenerated with different shapes).

4. Wire receiving into the UI:
     relay.onReceive(async (msg) => { /* msg.payload is ALREADY in this
       app's own shape — but it is ANOTHER PERSON'S data, not the
       user's */ });
   Received items get their own clearly separated area — a "From friends"
   section, an inbox tab, or a visually distinct group — labeled
   "from @" + msg.from_handle. Never insert them into the user's own
   collection by default, and never count them toward the user's own
   totals, stats, streaks, or charts. Two refinements:
     - Merging is the user's per-item choice: where mixing received data
       into the user's own would be useful, add an explicit "add to my
       data" action on the received item. Once imported it may join the
       user's collection and aggregates — keep the from-@handle
       attribution on the copy.
     - Only if the request for this app explicitly describes a shared or
       collaborative concept (a shared shopping list, a joint trip plan)
       may received items merge directly into the shared view — still
       attributed.
   Separate never means hidden: received items must surface promptly and
   visibly (a badge or count on their section works), with provenance per
   step 5.

5. Show provenance on received items. The SDK pre-decides what to show:
     const summary = relay.provenanceSummary(msg);
   Render by summary.kind:
     - "plain": show nothing.
     - "translated": the relay rewrote the sender's data into this app's
       shape, and the user is entitled to know. Show summary.badge quietly
       (a small note, not an alarm — translation working is the normal
       case); if summary.dropped or summary.assumed is non-empty, show them
       verbatim (dropped = what the sender's version had and this one does
       not; assumed = what this one states that the sender never said).
       Offer "see what they sent": await summary.fetchOriginal() and render
       its `original` as raw JSON, no styling — it is evidence, not UI.
     - "untranslated": graceful failure — the payload is the sender's exact
       bytes in THEIR shape, not this app's. Do not insert it like local
       data and do not drop it on the floor: show summary.note, render the
       payload generically (raw JSON is fine), and link summary.page — a
       relay-hosted page that renders it readably for a human.

6. Wire sending into the UI: wherever a data item is displayed, add a
   "send to contact" action that asks for a handle and calls:
     const r = await relay.send(handle, "workout", item);
   Sending NEVER blocks the UI. send() itself returns fast (it hands the
   message to the relay), but full delivery can take minutes when this is
   the first-ever exchange between two shapes — the relay compiles a
   translation. Let the user keep using the app while that happens, and
   show progress ON the item that was sent (a small "sending…" state on
   its card or row, or an unobtrusive toast) — never a modal, a spinner
   overlay, or a disabled screen:
     if (r.delivery === "compiling") {
       // first exchange between these two shapes; finish in the background
       relay.waitSent(r.id)
         .then((s) => { /* mark the item sent — or failed if s.status is
           "failed": show s.error and offer a retry (reuse the same
           idempotencyKey so a retry cannot deliver twice; see send() in
           the SDK) */ })
         .catch(() => { /* timed out — mark it unknown and offer "check
           again" via relay.sentStatus(r.id) */ });
     }
     if (r.delivery === "web-fallback") {
       // recipient has no app yet — r.url is a shareable web page; show it
     }
   While a "compiling" send is in flight, say why it is slow, in words like
   these on the item itself: "First time sending to @sam — setting up, about
   a minute. After this it's instant." A bare "sending…" for a minute reads
   as broken; the explanation is what makes the wait acceptable.
   Completion shows on the same surface ("sent" where "sending…" was); a
   failed send must be visible and retryable, never silent.

7. Contacts UI (small): a screen showing the full roster — mutual contacts
   plus incoming requests with an accept button (relay.acceptContact(handle))
   — and a way to send a request (relay.requestContact(handle, "hi — it's
   me")). A first message to a new person requires them to accept a request
   first. The roster lives on the relay; NEVER keep a local copy — let the
   SDK own the refresh discipline:
     const roster = relay.syncContacts((r) => { /* render r.contacts,
       r.incoming (with accept buttons), r.outgoing */ });
   It fetches the roster now, re-fetches the moment someone accepts this
   app's request (the relay pushes that event through the onReceive loop, so
   keep that running), and keeps a slow fallback poll. Call roster.refresh()
   after any accept/decline/request action and whenever the contacts screen
   is shown. Never hide or ignore a received message because the sender
   isn't in the currently rendered contact list: the relay only delivers
   consented mail, so msg.from_handle is always a real contact — display it
   and call roster.refresh().
   Introductions (friends of friends) are opt-in and off by default. Put one
   switch on the contacts screen — "Let friends of friends find me" — wired
   to relay.setIntroductions(true | false), and say beside it that it works
   only among people who have all turned it on. While it is on:
     const { open, suggestions } = await relay.introductions();
   lists people the user could be introduced to, each
   { handle, via: [mutual friends], sends: [{ name, description }] }, with
   an "ask to connect" button:
     await relay.requestContact(s.handle, note, { via: s.via[0] });
   The recipient's incoming request then carries `via` — show "introduced
   by @sam" on it. Never switch introductions on without the user, and show
   suggestions only inside the contacts screen.

8. Prove the integration end-to-end before handing the app over: run
     await relay.selfTest();
   once, at a point where the app's onReceive loop is NOT yet consuming the
   inbox (right after connect during development, or as a standalone script).
   It exercises the whole loop against the relay's built-in echo
   (@relay-echo — auto-consents, bounces payloads back, no second person
   needed): connectivity, credentials, publish, contact flow, send, receive,
   provenance. It prints a per-step checklist and resolves { ok, steps };
   it never throws. If ok is false, fix the failing step and run it again —
   DO NOT consider the integration done until it passes. Then remove the
   call or gate it behind a dev-only flag.

9. Conversation: plain notes between contacts, identical in every app, so
   they never need translating. Once, after connecting:
     const chat = await relay.enableChat();
   If chat.enabled is false, another of this user's apps already carries
   their conversations — hide this app's conversation UI and move on; that
   is not an error. Otherwise add a small per-contact thread:
     await relay.sendChat(handle, text);
   Incoming notes arrive through the same onReceive loop as everything else,
   with msg.type === "chat" and msg.payload = { text, sentAt } — route those
   to the thread for msg.from_handle, not to "Shared with me". Store threads
   locally; the relay does not keep them. Show a time on every note. Keep it
   plain: no typing indicators, no read receipts, no attachments.

10. Updates: what the user's contacts have added lately.
     const { updates } = await relay.updates();
   Each entry: { from, app, name, description, kind: "added" | "changed",
   at (ms, may be null), yours: "ready" | "missing" | "unknown", landsAs }.
   Render newest first, one line each with a time:
     "@sam added song-votes · 2h ago"
   Clicking an entry opens it: show the description, and
     - yours === "ready": "Your app already takes these."
     - otherwise: ask "Would you like to add this?" with a button that copies
       relay.featurePrompt(update) to the clipboard and tells the user to
       paste it to the AI that built this app. Copy only — never act on it.
   Refresh when the screen opens and every few minutes while it is open;
   there is nothing to store. Files are not sent over 1-z-2 — only an
   item's details. If this app's items point at a file (a song, a photo),
   send a link to it in the item and render received links as something
   the user can open or play.

Keep all relay plumbing in one module. Handle errors softly (the relay being
unreachable must never break the app's core features).
