@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.

bash
npm install @phosra/connect
ImportUse
@phosra/connectWeb (React) — PhosraConnect, ConnectFlow, useConnect
@phosra/connect/nativeReact Native — same components
@phosra/connect/coreShared 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.

ts
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 callReturnsBFF 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

tsx
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

PropTypeNotes
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.
grantedScopestring[]The rule categories bound in the grant (drives bind).
childIdstringThe OCSS child ref (child:<uuid>).
ageHintAgeHint"under_13" | "13_15" | "16_17".
childNamestring (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.
provisionChildrenProvisionChild[] (opt)Ages/names to create when there are no existing profiles.
redirectUristringYour OAuth callback.
transportConnectTransportYour BFF calls.
onSuccess(r: BindResult) => void{ grant_id, verified?, receiptFingerprint? }.
onExit() => voidParent 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.


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.

tsx
<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).

tsx
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 &amp; 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