# Show an ASX-listed company by ticker

What you build: a company card. The visitor types a ticker or the start of a name; the element suggests listed companies, looks the chosen one up and hands the page the record; the page renders the name, ticker, industry, address, website and description, and says when the ASX source was observed. An outage keeps the last card and says so.

## Before you start: a development app, then a production app

1. `npx comms-id init` makes a development key in `.env.local` and prints its public key. In the Companion App, register it as a development app, enable **ASX** for it, and put the app id in `.env.local` as `COMMS_ID_CLIENT_ID`. This key reaches only the TEST service (`https://api-test.comms.id`): the same API, errors and authentication as LIVE, with synthetic replies.
2. When it works in TEST, `npx comms-id key --worker <your worker>` makes the production key straight into your server's `COMMS_ID_PRIVATE_JWK` secret and prints the public key; register it as a production app with ASX enabled. Going live changes only the environment and the key: nothing in the code below.

A `.env.local` never holds a production key, and a browser never holds any key.

## The page

`company-card.html`:

```html
<!doctype html>
<!-- A card that shows an ASX-listed company. The element suggests companies as the visitor
     types a ticker or a name, looks the chosen one up, and this page renders the record. -->
<comms-id-asx label="ASX ticker or company name" relay="/api/comms-id/asx"></comms-id-asx>

<article hidden id="company">
  <h2 data-field="companyName">Company</h2>
  <p><strong data-field="ticker"></strong> · <span data-field="industry"></span></p>
  <p data-field="address"></p>
  <a data-field="website" href="#company" rel="noopener" target="_blank">Website</a>
  <p data-field="description"></p>
  <small data-field="observed"></small>
</article>
<p id="asx-status" role="status"></p>

<script src="https://cdn.comms.id/asx/v1.js"></script>
<script>
"use strict";
const field = document.querySelector("comms-id-asx");
const card = document.getElementById("company");
const status = document.getElementById("asx-status");
const show = (name, value) => {
  card.querySelector(`[data-field=${name}]`).textContent = value ?? "";
};

field.addEventListener("comms-id-select", (event) => {
  const { record } = event.detail;
  const company = record.observation.company;
  show("companyName", company.companyName);
  show("ticker", `ASX:${company.ticker}`);
  show("industry", company.industryGroup);
  show("address", company.address);
  show("description", company.description);
  const website = card.querySelector("[data-field=website]");
  website.textContent = company.website ?? "";
  website.href = company.website ?? "#";
  show(
    "observed",
    `${record.source} data, observed ${new Date(record.observation.observedAt).toLocaleDateString("en-AU")}`
  );
  card.hidden = false;
  status.textContent = "";
});

// An outage is not "no company": keep the last card and say so.
field.addEventListener("comms-id-error", (event) => {
  const { outcome, message } = event.detail.error;
  status.textContent = outcome === "source-unavailable" ? `ASX data is unavailable right now: ${message}` : message;
});
</script>
```

`comms-id-select` carries the whole lookup reply: `observation.company` (`ticker`, `companyName`, `address`, `website`, `description`, `people` and more), `source` (`live` or `snapshot`) and `observation.observedAt`.

## Your relay: one route, one signer

The browser calls your own server at `/api/comms-id/<product>/<operation>`; your server signs each call as your registered app with `@comms-id/relay`, so the page holds no credential. The relay forwards only the operations the product descriptors list, and never the user's cookies.

`pnpm add @comms-id/relay @comms-id/address @comms-id/asx @comms-id/charity @comms-id/logo @comms-id/sanctions`

`app/api/comms-id/[product]/[operation]/route.ts` (Next.js; `@comms-id/relay/hono` is the same for Hono):

```ts
// app/api/comms-id/[product]/[operation]/route.ts (Next.js). One route serves every product
// your pages use; the relay forwards only the operations these descriptors list.
import { addressProduct } from "@comms-id/address/descriptor";
import { asxProduct } from "@comms-id/asx/descriptor";
import { charityProduct } from "@comms-id/charity/descriptor";
import { logoProduct } from "@comms-id/logo/descriptor";
import { createNextRelay } from "@comms-id/relay/next";
import { sanctionsProduct } from "@comms-id/sanctions/descriptor";
import { createSigner } from "./signer";

export const { POST } = createNextRelay({
  products: [addressProduct, asxProduct, charityProduct, logoProduct, sanctionsProduct],
  // Your rule: who may use these routes. Return true, false (401) or your own Response.
  allow: (request) => request.headers.get("cookie") !== null,
  signer: createSigner(),
  // TEST: a development app's key and the TEST service. Going live: delete this line.
  baseUrl: "https://api-test.comms.id",
});
```

The signer is your own (the relay takes any function that returns a two-minute token for an audience and scope). This one reads your key from the environment and uses WebCrypto only, so it runs in Node 20+, Workers, Deno and Bun:

```ts
// The signer your relay needs: a two-minute EdDSA (Ed25519) token for one product, signed with
// your app's private key. The key is read once from the environment and never leaves this
// module. Runs in Node 20+, Workers, Deno and Bun (WebCrypto only).
import type { RelaySigner } from "@comms-id/relay";

const PADDING = /=+$/;
const encode = (bytes: ArrayBuffer | Uint8Array): string =>
  btoa(String.fromCharCode(...new Uint8Array(bytes)))
    .replaceAll("+", "-")
    .replaceAll("/", "_")
    .replace(PADDING, "");
const json = (value: unknown): string => encode(new TextEncoder().encode(JSON.stringify(value)));

/** A signer for `@comms-id/relay` from COMMS_ID_CLIENT_ID and COMMS_ID_PRIVATE_JWK. */
export const createSigner = (env: Record<string, string | undefined> = process.env): RelaySigner => {
  const clientId = env.COMMS_ID_CLIENT_ID?.trim();
  const jwk = env.COMMS_ID_PRIVATE_JWK;
  if (!(clientId && jwk)) {
    return () => undefined; // the relay then refuses with NOT_CONFIGURED; nothing is forwarded
  }
  const key = crypto.subtle.importKey("jwk", JSON.parse(jwk), { name: "Ed25519" }, false, ["sign"]);
  return async ({ audience, scope }) => {
    const now = Math.floor(Date.now() / 1000);
    const claims = {
      iss: clientId,
      sub: crypto.randomUUID(),
      jti: crypto.randomUUID(),
      aud: audience,
      scope,
      iat: now,
      exp: now + 120,
    };
    const input = `${json({ alg: "EdDSA" })}.${json(claims)}`;
    const signature = await crypto.subtle.sign("Ed25519", await key, new TextEncoder().encode(input));
    return `${input}.${encode(signature)}`;
  };
};
```

TEST first: the route above names `baseUrl: "https://api-test.comms.id"` and your development key answers there. LIVE: delete that line and deploy with the production key. The code between does not change.

## TEST first

With the relay pointed at TEST, type `BH`: one fictional match appears; choosing it looks up `BHP` (the published example ticker, an alias for a fictional TEST record) and fills the card. `UNAVAILABLE` as a ticker gives the outage branch; `NOMATCH`, "No ASX company found."

A page with no server: `<comms-id-asx pk="pk_test_…" base-url="https://api-test.comms.id">`, then `pk_live_` in production.

## LIVE

Remove `baseUrl` from the relay route and deploy with the production key. Reference: [ASX API](/docs/reference/asx). The attribution (ASX) the element shows is part of the data licence: do not hide it.
