@demystify/id-contracts
v0.4.0
Published
The Demystify ID token & client-registration contract: what a Demystify ID token means, and what makes an OIDC client registrable. Consumed by every product in the One Suite.
Readme
@demystify/id-contracts
The Demystify ID contract: what a Demystify ID token means, and what makes an OIDC client registrable. One source of truth, shared by the issuer, the control plane and every product in the One Suite.
Full integration guide: docs/47 — Client Integration
npm install @demystify/id-contractsTo verify tokens you want @demystify/id-verify,
which builds on this package and binds the correct audience per token kind.
Constants you compile against
The issuer URL, the JWKS URL and the suite resource are minted once and never change, so it is safe to hold them as constants rather than strings scattered across your config.
import {
DEMYSTIFY_ISSUER, // "https://id.demystifysystem.com"
DEMYSTIFY_SUITE_RESOURCE, // "https://api.demystifysystem.com" ← the ACCESS token audience
DEMYSTIFY_JWKS_PATH, // "/.well-known/jwks.json"
DEMYSTIFY_DISCOVERY_PATH, // "/.well-known/openid-configuration"
DEMYSTIFY_ENDPOINTS, // { authorization: "/authorize", token: "/token", userinfo: "/userinfo", … }
DEMYSTIFY_SCOPES, // openid profile email access entitle dpa offline_access
} from "@demystify/id-contracts";The ID token is audienced to your
client_id; the access token toDEMYSTIFY_SUITE_RESOURCE. Mixing those up is the most common integration failure — see docs/47 §5.
Token claims
import { TokenClaims, DemystifyClaims } from "@demystify/id-contracts";
const claims = TokenClaims.parse(verifiedPayload);
claims.sub; // demystify_id → core idp.identities.id (the person)
claims.org_id; // demystify_org_id → core access.orgs.id (the business)
claims.roles; // ["finocket.ledger.read", …] — <product>.<module>.<action>, scoped to org_id
claims.products; // ["finocket", "asher"]Client registration
OidcClient is the schema the issuer and the control plane both register through, so a client that parses
here is a client the issuer will accept.
import { OidcClient, checkRedirectUri } from "@demystify/id-contracts";
OidcClient.parse({
clientId: "finocket",
name: "Finocket",
redirectUris: ["https://app.finocket.com/auth/callback"],
scopes: ["openid", "profile", "access", "entitle"],
});The redirect-URI allow-list is what stops token theft, so the rule is strict and non-negotiable:
| Rejected | Why |
|---|---|
| http://app.finocket.com/cb | HTTPS only (except http://localhost for an explicit dev client) |
| https://*.finocket.com/cb | no wildcards — the allow-list is exact |
| https://app.finocket.com/cb# | no fragment delimiter, not even an empty one |
| HTTPS://app.finocket.com/cb | one canonical spelling; the allow-list is matched by exact string |
| https://app.finocket.com/c b | no whitespace |
| [] | a client must register at least one callback |
checkRedirectUri(uri) returns null when a URI is registrable, or the reason it is not. The same rule is
mirrored as a database constraint in the core, and a parity test proves the two never diverge.
Event envelope
SuiteEvent is how a product reports activity and metering back to the core. Identity, never data: send
counters and references, and keep the business record in your own database.
import { SuiteEvent } from "@demystify/id-contracts";
SuiteEvent.parse({
orgId, product: "finocket", module: "ledger", kind: "invoice.created",
meta: { count: 1, invoiceRef: "inv_123" }, // not the invoice itself
});