createLink() is the ergonomic front door to @phosra/link. It derives the whole
hand-built LinkConfig (writer DID, router DID, router payload key, the HKDF-derived
household and parent keys) from one Ed25519 writer key you already hold, ships and
migrates its own product tables, and delivers rules to platforms by signing each
delivery to the OCSS root — so there is no shared HMAC secret to mint, hand off, store,
sync, or rotate.
This page is the ergonomic quickstart. The lower-level primitives it wraps —
createLinkSession, completeLink, runConnectCeremony, directive,
provisionProfiles, deliverLabelToPlatform — are unchanged and still exported; see
the @phosra/link reference. New code should start here.@phosra/link is ESM-only. Import it from an ESM module ("type": "module", a .mjs
file, or TypeScript compiled to ESM). From CommonJS, load it with a dynamic
await import('@phosra/link') — not require.Integrate in ~15 lines
writerSeed is the only secret, and it is shared with no
platform — the same key signs your census writes and your platform deliveries.
What each field does
| Field | Required | Meaning |
|---|---|---|
census | yes | Census base URL. If it is a pinned host, the SDK ships the matching trust root — no TOFU, no trustRoot. |
writerSeed | yes | Your Ed25519 signing key: a base64url seed, raw bytes, or a SenderKey { seed, keyID }. |
writerKeyId | yes¹ | Your full key id, did:ocss:<slug>#<kid>. ¹Omit only when writerSeed is already a SenderKey carrying its keyID. |
db | yes | A pg.Pool or a connection string. The SDK owns and migrates its tables here. |
trustRoot | conditional | Required for a census host that is not in the SDK’s pin set (no trust-on-first-use); overrides a pin otherwise. |
developerOrgId | no | Billing attribution. Omit and the family lands in the anonymous/self-host bucket — no invoice, no SLA. Never gates the safety path. |
verifyAccreditation | no | Default true; gates writes on a Trust-List self-check. Set false to skip it (not recommended). |
Trust root: pinned vs. explicit. For a census in
PINNED_TRUST_ROOTS
(resolvePinnedTrustRoot(census) returns it), the SDK verifies against the bundled pin and
you pass nothing. For any other host — a self-hosted or one-off census — pass trustRoot
explicitly. Root verification exists precisely so you don’t fetch the root from the census
you’re about to verify.link.ready() runs ensureSchema (the idempotent product DDL) and verifyAccreditation
(root-verify + writer-pub match + active enforcement-agent role). Prefer to control timing?
Call link.migrate() and link.verifyAccreditation() yourself.
The connect flow
The connect flow links a parent’s account on a platform to a child’s OCSS policy. It is three legs on thelink.connect sub-object. The parent authenticates with your auth —
Phosra never sees parent credentials; parentSessionRef is the server-derived binding
between that login and the ceremony (never accept it from the client).
connect.finish does everything the old four-step ceremony did — derives the unlinkable
household hash, mints the grant, ingests the §8.3.2 parent-consent attestation, binds the
enforcement endpoint, and delivers the label to the platform, signed to the OCSS root.
deliveryScheme tells you which lane carried the label. On a secret-free platform it is
"ed25519-did" (signed). It is "hmac" only if you passed a legacy __legacyConnectSecret
during a migration drain window — see Migrating HMAC → Signed.
delivered + deliveryStatus report the receiver’s HTTP result.Two connect vocabularies, one flow. The factory’s legs are
start / resume /
finish; the embeddable @phosra/connect transport names the
same three legs init / complete / bind. They map 1:1 — start↔init,
resume↔complete, finish↔bind — so your BFF wraps the factory to serve the modal.Create-and-link (one parent action → N banded profiles)
When the platform advertises batch provisioning, a single signed call creates the child profiles on the platform and binds them — the create-and-link path (ingestConsentAttestation → mintEnforcementEndpoint(standingRef) → signed
provisionProfiles, all wrapped):
Write a rule — link.enforce
Once a grant is active, link.enforce signs and posts the rule write to the census.
granted_scope
throws before any network call. The standing (consent:attestation:<ref>) is supplied
automatically from the grant.
Manage grants
Typed errors
@phosra/link throws typed LinkError subclasses so you can turn a raw census status into
an actionable message. Guard with isLinkError(e) and branch on e.code.
DeliveryFailedError with .status === 401 almost always means the platform has not yet
added your provider DID to its createConnectReceiver({ authorize }) allowlist — the one
trust config on the platform side. That is the signed-delivery analogue of “wrong HMAC
secret,” and the fix is a one-line allowlist edit on the platform, not a secret exchange.
What the SDK owns (and never sends)
| Operation | Where it runs |
|---|---|
| Ed25519 signing (every write + every delivery) | Your process — the key never leaves |
| Household hash derivation | Your process |
| Scope pre-check | Your process |
| Product schema (grants + sessions) | Your db — link.migrate() |
| Trust List fetch + verify-to-root | Census: GET /.well-known/ocss/trust-list |
| Platform connect-config fetch | Census: GET /api/v1/providers/{did}/connect |
| Consent attestation ingest | Census |
| Endpoint binding | Census: POST /api/v1/enforcement-endpoints |
| Rule write | Census: POST /api/v1/policies/{policyId}/rules |
| Label delivery to the platform | Direct provider → platform, Ed25519-signed (census has no role) |
What the platform (your partner) must do
The signed model shifts almost all of the work to your side. To receive your deliveries, the platform implements one route and adds one allowlist line — no shared secret:- Install
@phosra/gatekeeperand addPOST /api/ocss/connectviacreateConnectReceiver({ env, did, seed, authorize: ["did:ocss:<you>"], store, onBound })— see Platform Quickstart. - Put your provider DID in that
authorizeallowlist. That is the entire trust configuration; there is nothing to mint, store, sync, or rotate. - Nothing else. The platform never holds a Phosra secret; your signature to the pinned root is the auth.
Next
- Platform Quickstart · createConnectReceiver — the one-route receiver
- Migrating HMAC → Signed — retire the shared connect secret
- PhosraConnect component — the Plaid-grade in-app parent modal
@phosra/linkreference — the low-level primitives and the reference BFF