Show an ASX-listed company by ticker
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
npx comms-id initmakes a development key in.env.localand 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.localasCOMMS_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.- When it works in TEST,
npx comms-id key --worker <your worker>makes the production key straight into your server’sCOMMS_ID_PRIVATE_JWKsecret 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:
<!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):
// 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:
// 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. The attribution (ASX) the element shows is part of the data licence: do not hide it.