@phosra/connect — PhosraConnect
@phosra/connect is the parent-facing connect surface — a calm, branded, in-app modal that
never navigates away, shows exactly which child and which rules are being granted, and ends on
an explicit success. It is the Plaid Link of OCSS: the same recognizable surface everywhere,
which is the trust mechanism.
The component is presentation + state machine only. Every network call goes through a
ConnectTransport you implement on your own backend — the parent never sees a secret,
token, or signature, and your provider key never reaches the browser.
npm install @phosra/connect| Import | Use |
|---|---|
@phosra/connect | Web (React) — PhosraConnect, ConnectFlow, useConnect |
@phosra/connect/native | React Native — same components |
@phosra/connect/core | Shared types + the headless useConnect controller |
The transport contract
The component calls your backend through a ConnectTransport. Three calls are required; the
fourth (provision) is optional and unlocks in-modal create-and-link. Each maps 1:1 to a
createLink connect leg on your BFF.
import type { ConnectTransport } from "@phosra/connect/core"
const transport: ConnectTransport = {
// → { authorizeUrl, state, sessionId } (wraps link.connect.start)
async init(req) { return post("/api/phosra/connect/init", req) },
// → { sessionId, childProfiles, provisioningForm? } (wraps link.connect.resume)
async complete(req) { return post("/api/phosra/connect/complete", req) },
// → { grant_id, verified?, receiptFingerprint? } (wraps link.connect.finish)
async bind(req) { return post("/api/phosra/connect/bind", req) },
// OPTIONAL → { grant_id, verified, provisioned } (wraps link.provision)
async provision(req){ return post("/api/phosra/connect/provision", req) },
}| Transport call | Returns | BFF wraps |
|---|---|---|
init | { authorizeUrl, state, sessionId } | link.connect.start(...) |
complete | { sessionId, childProfiles, provisioningForm? } | link.connect.resume(...) |
bind | { grant_id, verified?, receiptFingerprint? } | link.connect.finish(...) |
provision (optional) | { grant_id, verified, provisioned } | link.provision(...) |
verified drives the sandbox test cue — never fake it. Success shows
"Verified test signature" only when verified === true for the documented synthetic context.
It must not imply accreditation, certification, or a production platform. Otherwise the modal shows
<Platform> connected — confirming enforcement… (where <Platform> is the platform name).
Return verified honestly; a green badge with no verified receipt is the one thing the
branding rule forbids.
provisioningForm is sandbox-server-advertised. If complete returns
provisioningForm: "batch", that overrides the component prop — the platform told the census
it accepts batch provisioning, and the modal offers the create-and-link step. Your bind /
provision handlers exercise the signed sandbox path; there is no connect secret surfaced in
the component.
Web quickstart
import { PhosraConnect } from "@phosra/connect"
import type { ConnectTransport } from "@phosra/connect/core"
const transport: ConnectTransport = { init, complete, bind, provision }
export function ConnectNotflix() {
return (
<PhosraConnect
platform={{ did: "did:ocss:notflix", name: "Notflix" }}
rules={[{ category: "dm_restriction", label: "Restrict direct messages" }]}
grantedScope={["dm_restriction"]}
childId="child:a11ce0fa-..."
ageHint="13_15"
childName="Ava" // names the child in the grant preview
redirectUri="https://custo.app/phosra/callback"
transport={transport}
onSuccess={(r) => {
// r.grant_id, r.verified, r.receiptFingerprint
router.push("/family")
}}
onExit={() => router.back()}
/>
)
}The sandbox component renders three precise evaluation cues: "OCSS sandbox" (intro), the
concrete rules preview (the rules you pass), and Verified test signature only when
bind returned verified === true for the documented sample context. It does not display an
accreditation or production-platform claim. No secret, token, or signature is surfaced.
Props
| Prop | Type | Notes |
|---|---|---|
platform | { did, name } | Synthetic target identity in sandbox evaluation; name co-brands the modal. |
rules | { category, label }[] | The concrete grant preview — the exact rules the parent is consenting to. |
grantedScope | string[] | The rule categories bound in the grant (drives bind). |
childId | string | The OCSS child ref (child:<uuid>). |
ageHint | AgeHint | "under_13" | "13_15" | "16_17". |
childName | string (opt) | Names the child in the preview and success copy. |
provisioningForm | "batch" | "single" (opt) | Enables create-and-link; a server-advertised provisioningForm from complete overrides it. |
provisionChildren | ProvisionChild[] (opt) | Ages/names to create when there are no existing profiles. |
redirectUri | string | Your OAuth callback. |
transport | ConnectTransport | Your BFF calls. |
onSuccess | (r: BindResult) => void | { grant_id, verified?, receiptFingerprint? }. |
onExit | () => void | Parent dismissed the modal. |
ConnectFlow takes the identical props if you want the flow inline rather than in a modal
shell. React Native is the same API from @phosra/connect/native.
Create-and-link in the modal
When there are no existing profiles on the platform and you pass a provisioningForm
plus a transport.provision, the modal shows a no_profiles step with a "Create &
connect" button. The parent creates the banded child profiles and binds them in one contained
step — no new tab, no dead-end.
<PhosraConnect
platform={{ did: "did:ocss:notflix", name: "Notflix" }}
rules={[{ category: "content_rating", label: "Age-appropriate titles only" }]}
grantedScope={["content_rating"]}
childId="child:a11ce0fa-..."
ageHint="under_13"
childName="Leo"
provisioningForm="batch" // + transport.provision → in-modal create
provisionChildren={[{ ageHint: "under_13", displayName: "Leo" }]}
redirectUri="https://custo.app/phosra/callback"
transport={transport}
onSuccess={(r) => router.push("/family")}
onExit={() => router.back()}
/>Under the hood the modal advances through two new statuses: no_profiles (the create step is
offered) and creating (the signed provision delivery is in flight). Your transport.provision
returns { grant_id, verified, provisioned } — provisioned is the count created.
Headless: useConnect
For a fully custom UI, drive the state machine directly with useConnect and render your own
chrome (branding still required — see below).
import { useConnect } from "@phosra/connect/core"
function CustomConnect() {
const c = useConnect({
platform: { did: "did:ocss:notflix", name: "Notflix" },
grantedScope: ["dm_restriction"],
childId: "child:a11ce0fa-...",
ageHint: "13_15",
provisioningForm: "batch",
provisionChildren: [{ ageHint: "13_15", displayName: "Ava" }],
redirectUri: "https://custo.app/phosra/callback",
transport,
})
// c.status: … | "no_profiles" | "creating" | …
if (c.status === "no_profiles") {
return <button onClick={() => c.createProfiles()}>Create & connect</button>
}
// render per c.status; c.state carries provisioningForm and the child profiles
}createProfiles() is valid only while status === "no_profiles"; it runs the signed
transport.provision and advances the machine.
Branding is mandatory
The drop-ins render the Phosra Link branding — the phosra · OCSS lockup, explicit
sandbox/test labels, and the never-a-fake-green honesty rule (the test-verification badge
appears only after the documented sample receipt is checked).
This is required, not optional: any Phosra Link connect/consent surface MUST use the
published kit, a provider may not hand-roll or restyle the branded consent, and the platform
auth leg MUST co-brand Phosra Link · <Platform>. Consistency is the trust mechanism, and it
is part of the design-partner review. See the
Phosra Link Branding Requirement.
Next
- Provider Quickstart · createLink — the BFF your transport wraps
@phosra/link→ Reference BFF — a copy-pasteable BFF for the three legs- PhosraLinkKit (iOS) — the native equivalent
- Phosra Link Branding Requirement