npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@phosra/gatekeeper

v0.8.73

Published

Platform-side SDK for Phosra Link and the OCSS (Open Child Safety Specification). It owns the signed linking ceremony, Trust List verification, durable retries, enforcement-profile verification, read-back evidence, and confirmation events.

Readme

@phosra/gatekeeper

Platform-side SDK for Phosra Link and the OCSS (Open Child Safety Specification). It owns the signed linking ceremony, Trust List verification, durable retries, enforcement-profile verification, read-back evidence, and confirmation events.

The golden path has one Phosra secret: PHOSRA_CREDENTIAL. You do not wire raw signing keys, service URLs, fetch, trust resolvers, delivery transports, or ACK verification. V3 credentials also carry the public environment-manifest and root authority, so applications do not copy pins or service coordinates into source.

Install

npm install @phosra/gatekeeper

Quickstart: receive Phosra Link in a platform

// src/lib/phosra.ts — the one process-local Phosra singleton
import { createPlatform } from "@phosra/gatekeeper"
import { pool } from "@/db"

export const phosra = createPlatform({
  credential: process.env.PHOSRA_CREDENTIAL!,
  // Optional: safe for hosted logs. Diagnostics contain a stable phase code
  // and random correlation ID, never account/profile IDs or dependency text.
  onDiagnostic: (diagnostic) => console.error(JSON.stringify(diagnostic)),
  humanRecovery: {
    signInPath: "/sign-in",
    createProfilePath: "/profiles/new",
  },
  adapter: {
    database: pool,
    authorizedProviders: ["did:ocss:custo"],

    // Map the already-signed-in platform request to real child profiles.
    resolveAccount: async (request) => {
      const session = await sessionFrom(request)
      if (!session) return null
      return {
        accountId: session.accountId,
        profiles: await childrenFor(session.accountId),
        // Each profile is { id, displayName, displayHint? }.
        // displayHint (for example "Age 10") safely distinguishes same-name children.
      }
    },

    // Apply is a command. Its return value is deliberately not trusted as evidence.
    apply: async ({ selectedProfileId, profile, idempotencyKey }) => {
      await applyParentalRules({ selectedProfileId, profile, idempotencyKey })
    },

    // Observe independently reads back the real persisted platform effect.
    // The SDK reporter owns OCSS rule refs, hashes, timestamps, and reason codes.
    observe: async ({ selectedProfileId, profile }, evidence) => {
      const controls = await readAppliedRules(selectedProfileId)
      return profile.rules.map((rule) => {
        const control = controls.get(rule.category)
        if (!control) return evidence.refused(rule, "unsupported")
        if (!control.enabled) return evidence.degraded(rule, "platform-conflict")
        return evidence.applied(rule, {
          method: "gate",
          sideEffectId: control.id, // a stable row/control ID; never sent in cleartext
        })
      })
    },

    // Disconnect is also command + independent read-back. A command return
    // alone can never make Phosra claim that controls were removed.
    release: async ({ selectedProfileId, idempotencyKey }) => {
      await removePhosraOverlay({ selectedProfileId, idempotencyKey })
    },
    observeRelease: async ({ selectedProfileId }, evidence) => {
      const removal = await readOverlayRemoval(selectedProfileId)
      return evidence.released({ sideEffectId: removal.id })
    },
  },
})

onDiagnostic reports both startup failures (operation: "platform_ready") and bounded enforcement failures (operation: "materialization"). Materialization subcodes distinguish profile retrieval, profile verification, shared-target reservation, platform enforcement, confirmation, and outcome recording. Hook errors are isolated and never change retries or enforcement.

Put the HTTP route and worker in separate files. The route is safe for Next.js to import during a build: constructing the singleton and exporting its handlers performs no network, database, migration, or readiness work. The first real, recognized protocol request lazily runs one SDK-owned readiness gate. Concurrent requests coalesce; success is reused; a failure returns a redacted 503 and is retried only after bounded backoff. The route never starts the long-running worker:

// app/api/phosra/[...phosra]/route.ts — the entire HTTP integration.
import { phosra } from "@/lib/phosra"

export const { GET, POST } = phosra.next.handlers()

That single catch-all owns /api/phosra/par, /api/phosra/authorize, /api/phosra/token, /api/phosra/delivery, /api/phosra/retry, and /api/phosra/status. The retry endpoint accepts only the exact signed-request media type (maximum 64 KiB) and returns only the exact platform-signed result bytes persisted with the durable retry decision. A duplicate still being processed receives 202 with Retry-After; crossed idempotency receives 409; no HTTP success or adapter return is ever promoted into enforcement evidence.

After a valid retry is durably accepted, Gatekeeper runs one bounded worker pass before returning the persisted response. This fast path lets an interactive Phosra Link disconnect normally apply and observe the platform release, then dispatch its signed release event, without waiting for the standalone worker's next polling cycle. It grants no new authority and applications do not add a special disconnect endpoint. If the bounded pass fails, the committed retry remains queued for the standalone worker and the original signed retry response is returned unchanged.

Gatekeeper 0.8.19 also handles a disconnect that was signed against the last known revoked generation while the platform's current lifecycle has advanced into an exhausted degraded or stale generation. Recovery is allowed only when the delivery, delivery attempt, authority digest, provider, and platform still match exactly. Gatekeeper records the decision in both immutable retry journals and reopens only that profile lifecycle; every other historical-stage request remains fail-closed.

GET /api/phosra/status — which precondition is outstanding (0.8.68)

Every /api/phosra/* protocol route answers the same opaque 503 PHOSRA_NOT_READY for every fault. That is correct for a protocol surface and useless for an operator: a missing credential, a lapsed accreditation, and a credential whose signing key has diverged from the key the census publishes are indistinguishable from outside. status answers the question the 503 cannot.

curl -s https://your-app.example.com/api/phosra/status | jq
{
  "ok": true,
  "ready": false,
  "precondition": "PLATFORM_SIGNING_KEY_MISMATCH",
  "summary": "The credential's active signing key is NOT the key the census publishes under that key id. The credential and the trust list have diverged.",
  "remedy": "Compare signingKey.credentialPublicKey with signingKey.censusPublicKey below — …",
  "platform": { "did": "did:ocss:example", "environment": "production", "credentialVersion": 3 },
  "census": { "origin": "https://prodapi.phosra.com", "trustListIssue": 41 },
  "signingKey": {
    "kid": "did:ocss:example#link-1",
    "credentialPublicKey": "AZMIh9qxtx0nN4bXfj2f60_mGhl0cryDS5vCrKBZew4",
    "censusPublicKey": "47tjRF8D8DFYQ9Kl7F3hg7qEA9PnliCuDs4xhlSuUQM",
    "matches": false
  },
  "standing": { "did": "did:ocss:example", "status": "active", "tier": "accredited", … }
}

Named preconditions: PLATFORM_READY, CREDENTIAL_MISSING, CREDENTIAL_UNPARSEABLE, ENVIRONMENT_MANIFEST_UNAVAILABLE, TRUST_LIST_UNVERIFIABLE, PLATFORM_SIGNING_KEY_UNPUBLISHED, PLATFORM_SIGNING_KEY_MISMATCH, PLATFORM_STANDING_NOT_ACCREDITED, AUTHORIZED_PARTY_NOT_ACCREDITED, PLATFORM_DIRECTORY_UNAVAILABLE, PLATFORM_COMPOSITION_FAILED, PLATFORM_DATABASE_NOT_READY, PLATFORM_GENERATION_INACTIVE.

Three properties are load-bearing:

  • It always answers 200, including — especially — when the platform is not ready. It is a report ABOUT availability, not the thing being gated, and it is deliberately not behind the readiness gate. A diagnostic that 503s under the fault it explains is not a diagnostic.
  • It is unauthenticated. Every field is already public: DIDs are public identifiers, the trust list is world-readable at /.well-known/ocss/trust-list, and a signing key's PUBLIC half is exactly the thing that is meant to be published. Gating it would require distributing a new shared secret — the friction class Phosra Link exists to remove — and would withhold the answer from the person most likely to need it: a partner debugging from outside the deployment. It reports no seeds, private keys, or credential bytes; publicSigningKeyFacts refuses outright if the bytes it is about to publish are byte-identical to the private seed.
  • It cannot hammer the census. It reuses the same generation coordinator the protocol routes use, so a healthy platform answers with no network I/O and an unhealthy one respects the coordinator's bounded backoff. Add ?probe=0 for a strictly non-triggering read of the current state.

Return signed-out parents to Link safely

humanRecovery applies only to GET /api/phosra/authorize. A signed-out parent receives a 303 to signInPath; a signed-in account with no child profiles receives a 303 to createProfilePath. Gatekeeper accepts only static, single-origin paths with no scheme, host, query, fragment, backslash, control character, or dot segment. It appends one phosra_return_to value containing the canonical authorize path and opaque request_uri. PAR, authorize POST, token, delivery, and retry never use these redirects. Omitting humanRecovery preserves the JSON AUTH_REQUIRED and NO_PROFILE_AVAILABLE responses.

Shared profile confirmation is SDK-owned (0.8.1)

When a parent selects a platform profile that is already linked to another child in the same verified family, the Gatekeeper authorize handler owns the decision screen. Platform applications do not add a custom route, modal, or confirmation callback.

The screen recommends choosing or creating a separate profile because separate profiles keep viewing history, recommendations, screen time, and controls individual. Its actions appear in the fixed order Choose another profile, Confirm sharing, and Cancel and return. Before confirmation it explains that a shared profile receives the strictest protections required by any linked child and that activity on the profile cannot be reliably attributed to one child.

Gatekeeper renders only the authenticated platform catalog label recovered from its server-side sealed catalog and the same-family member count. A separately signed selected-profile presentation is issued later, during token exchange; it is not the source of the pre-exchange confirmation label. The confirmation form uses bounded opaque tokens but does not expose raw account or profile IDs, child commitments, family authority, aggregate IDs, or policy parameters. The initial release does not render member names or numeric ages; those require a future provider-signed member-display resolver and must never be inferred from internal authority.

If the target is changing, the handler offers an actionable retry or a return to profile choice. If family authority is missing or different, sharing is not offered and the parent is directed to choose a separate profile. Rules outside the built-in merge registry fail closed during server-side aggregate processing; 0.8.1 does not expose a dedicated public incompatibility code or capability manifest. Do not replace, restyle, or bypass these SDK-owned decisions.

The 0.8.1 handler copy and action order are fixed:

| State | Parent copy | Actions, in order | |---|---|---| | Confirmation | Use {profile} for another child? and Recommended: choose a separate profile | Choose another profile, Confirm sharing, Cancel and return to {provider} | | Target changing | This profile is finishing another link and Wait a moment and try again, or choose another profile now. | Choose another profile, Try again, Cancel and return | | Family sharing unavailable | Choose a separate profile and This profile can’t be shared for this family link. Choose another profile, or create a separate one in this platform. | Choose another profile, Cancel and return |

Your sign-in, sign-up, and profile-creation pages must preserve the parameter and validate it again before redirecting. Never pass an arbitrary query value directly to redirect():

import { redirect } from "next/navigation"

function validatedPhosraReturnTo(value: string | null): string {
  if (value === null || value.length > 2_048) return "/"
  let parsed: URL
  try {
    parsed = new URL(value, "https://phosra-return.invalid")
  } catch {
    return "/"
  }
  const entries = [...parsed.searchParams.entries()]
  const requestUri = parsed.searchParams.get("request_uri")
  if (parsed.origin !== "https://phosra-return.invalid"
    || parsed.pathname !== "/api/phosra/authorize"
    || parsed.hash !== ""
    || entries.length !== 1
    || entries[0]?.[0] !== "request_uri"
    || !/^urn:ietf:params:oauth:request_uri:[A-Za-z0-9_-]{43}$/.test(requestUri ?? "")) {
    return "/"
  }
  return `${parsed.pathname}?request_uri=${encodeURIComponent(requestUri!)}`
}

// Use the same final step after sign-in, sign-up, and child-profile creation.
export async function finishSignIn(searchParams: { phosra_return_to?: string }) {
  await completeYourSignIn()
  redirect(validatedPhosraReturnTo(searchParams.phosra_return_to ?? null))
}
export async function finishSignUp(searchParams: { phosra_return_to?: string }) {
  await completeYourSignUp()
  redirect(validatedPhosraReturnTo(searchParams.phosra_return_to ?? null))
}
export async function finishProfileCreation(searchParams: { phosra_return_to?: string }) {
  await createYourChildProfile()
  redirect(validatedPhosraReturnTo(searchParams.phosra_return_to ?? null))
}

Run the worker from its own process entry point (for example, a Railway worker service). Only this process intentionally waits for the worker until shutdown:

// phosra-worker.ts — explicit long-running worker entry point.
// Nothing starts merely because the Gatekeeper package is imported.
import { phosra } from "@/lib/phosra"

await phosra.ready()
const shutdown = new AbortController()
process.once("SIGTERM", () => shutdown.abort())
process.once("SIGINT", () => shutdown.abort())

const worker = phosra.worker.start({
  signal: shutdown.signal,
  onError: (error) => console.error("Phosra worker pass failed", error),
})
await worker.drain()

phosra.worker serializes every pass, waits when the queue is idle, applies bounded exponential backoff after an error, and never overlaps work. Direct runWorkerOnce() calls and the background loop share that same pass boundary; concurrent calls coalesce onto the in-flight result instead of applying twice. stop() requests shutdown and drain() waits for the current bounded pass to finish. status() exposes healthy, state, pass/failure counters, the next pass time, and signed-authority expiry for your health endpoint; healthy turns false as soon as that signed window expires. Calling start() again while active returns the same controller; after stop + drain it starts cleanly. If host adapter code accidentally calls runWorkerOnce() from the active pass or environment build, it rejects with PLATFORM_WORKER_REENTRANT instead of waiting on itself.

If your platform already has a durable scheduler, call await phosra.runWorkerOnce() from that scheduler instead. Do not combine both modes in one process.

The explicit composition kernel remains available at @phosra/gatekeeper/platform for advanced infrastructure, migrations, and conformance testing. Most applications should not need it.

Custom PlatformStore adapters must return BeginAuthorizationResult.redirectUri from the callback in the consumed, verified request object. Preserve that URL byte-for-byte: do not reconstruct it from browser headers, the authorization-page query string, or application defaults. Gatekeeper validates the callback again and projects only its canonical origin into the authorization page's form-action policy. The packaged Postgres store handles this automatically.

Legacy v1/v2 credentials remain supported with an explicit public bootstrap.preset. Download a v3 credential to use the one-variable quickstart above.

Environment refresh is anti-rollback: a replacement manifest must have a signed expiresAt strictly later than the active generation. When rotating a credential, signing key, or environment manifest, issue a later expiry too; a different same-expiry manifest is deliberately rejected as rollback/equivocation.

Legacy local decision engine

createGatekeeper(...) remains available for applications using the earlier poll-and-check API. New Phosra Link integrations should start with createPlatform(...) above.

Real signed profiles in unit tests

@phosra/gatekeeper/testing builds a real in-process root, Trust List, router key, and router-signed enforcement profile without external network access. Production profiles always require a valid, unique rule_ref on every category. For fixture convenience only, setProfile() deterministically fills an omitted rule_ref with a valid opaque 64-character reference before signing; an explicit reference is preserved exactly. This does not relax production profile verification.

import { makeSignedStack } from "@phosra/gatekeeper/testing"

const stack = makeSignedStack()
stack.setProfile([{
  category: "content_rating",
  decision: "warn",
  fail_mode: "closed",
  rule_slug: "content_rating",
  params: { max_allowed: 10 },
  // rule_ref may be omitted in this testing helper only.
}])

The connect receiver — @phosra/gatekeeper/next (one route, no secret)

A platform's entire /api/ocss/connect route is one export. It dispatches the three delivery shapes (signed provision batch / signed §3.6 connect bind / legacy HMAC) to the right verify primitive, and fires your materialize hook once the census-verified binding lands.

// app/api/ocss/connect/route.ts — the whole receiver
import { createConnectReceiver } from "@phosra/gatekeeper/next"
import { store }   from "@/lib/ocss/store"   // a ConnectSessionStore over YOUR child table
import { onBound } from "@/lib/ocss/apply"    // materialize: pull the profile / create the profile

export const { POST } = createConnectReceiver({
  env:  "production",                 // → census URL + PINNED trust root (bundled, no TOFU)
  did:  "did:ocss:your-platform",
  seed: process.env.OCSS_SENDER_SEED_B64URL!,  // your Ed25519 seed (base64url-raw, 32 bytes)
  authorize: ["did:ocss:custo"],      // REQUIRED provider allowlist — revoke = remove a DID
  store,
  onBound,                            // (label, childRef, provisionCtx?) => materialize
  onError: (err, ctx) => log.warn("ocss ingest failed", ctx, err), // optional
})

Signed by default. The provider's sender-DID-signed-to-root signature IS the auth — there is no shared HMAC secret to mint, store, sync, or rotate (OCSS §8.1 clause 6 bans bearer secrets). Audience-binding + created freshness + the authorizedConnectDids gate are inherited from the verified envelope. assertConfig() throws the exact missing field at construction. To migrate an existing HMAC receiver, pass legacyHmacSecret (opt-in; flips the posture to accept both lanes) and drain to signed-only, then delete the secret.

onBound is fired post-2xx, once per bound label, and MUST be idempotent (create-or-adopt keyed by label):

  • connect bindonBound(label, childProfileId) — pull the signed profile + apply caps;
  • provisiononBound(label, label, { age_band, display_hint, state, adult_pin_auto_set }) — create-or-adopt a profile for the child (band → age).

Boot-time config assert — assertGatekeeperConfig (kills the AuthKit-matcher trap)

The #1 integrator footgun is a Next middleware.ts whose config.matcher excludes the OCSS connect routes (the classic /((?!…|api).*) negative-lookahead drops all of /api), so the AuthKit/proxy middleware never runs on the connect route and the ceremony returns 500. This exact gap regressed one integrator three times. Call the assert once, in middleware.ts, and a matcher gap fails loudly at boot instead of silently mid-ceremony:

// middleware.ts
import { assertGatekeeperConfig } from "@phosra/gatekeeper"

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico).*)", // your app routes (does NOT exclude api)
    "/api/phosra/:path*",                            // ← the OCSS connect family MUST be covered
  ],
}
assertGatekeeperConfig({ matcher: config.matcher }) // throws GatekeeperConfigError if uncovered

The error names the uncovered route and the one-line fix. Mount elsewhere (e.g. /api/ocss)? Pass routes: gatekeeperConnectRoutes("/api/ocss").

The OCSS protocol layer — send + receive (no bespoke lib/ocss/)

The delivery/enforcement primitives every platform used to hand-roll are exported directly, so your lib/ocss/ shrinks to your own persistence + classifier:

import {
  loadSenderKey, TrustListCache, makePhosraTrustListResolver,
  sendHarmContext, openInboundEnvelope, buildAbuseSignal, boundedExcerpt,
} from "@phosra/gatekeeper"

// Load your Ed25519 identity from env (OCSS_SENDER_SEED_B64URL + OCSS_DID + OCSS_SIGNING_KEY_ID).
const senderKey = loadSenderKey()
const trustList = makePhosraTrustListResolver("sandbox")

// SEND (monitored platform → parental app): seal to the recipient's payload key, sign the
// outer §4.2 envelope + the RFC-9421 request, POST the router-blind harm-context lane. Never throws.
await trustList.ensure()
const res = await sendHarmContext({
  senderKey,
  receiverDid: "did:ocss:household-acme",
  receiverPayloadJwk: trustList.payloadKey("did:ocss:household-acme"), // root-verified
  resource: "endpoint:mia",
  content: new TextEncoder().encode(JSON.stringify(
    buildAbuseSignal({ harmClass: "grooming", severity: "severe", sessionScope: "thread:t1",
      eventRef: "message:m1", emitterId: senderKey.keyID.split("#")[0], appealPath: "https://you/appeal" }),
  )),
  censusUrl: "https://phosra-api-sandbox-production.up.railway.app",
})

// RECEIVE (parental app): verify the outer signature to the Trust-List ROOT + open the inner
// sealed payload — one fail-closed call.
const opened = await openInboundEnvelope({
  envelope,                       // the parsed inbound { outer, inner }
  trustList,                      // resolve + root-verify the signer
  receiverPayloadJwk: myPayloadPrivateJwk, // your private P-256 key
  expectedReceiver: "did:ocss:household-acme",
})
// opened.signerDid is verified-to-root; opened.payload is the decrypted bytes.

Disconnection — the revoked tombstone (§8.3.6 / §3.1)

A connect is only as honest as its reverse. When a parent withdraws the last standing for a child, the census stops serving rules and serves one final router-signed §8.3.6 tombstone with top-level status: "revoked". This is a positive, signed, terminal state — never an absence — so a fail-closed platform cannot mistake a disconnect for an outage and keep enforcing revoked rules from cache forever.

gk.endpointState() is the first-class signal ("active" | "revoked" | "stale" | "absent"):

| endpointState | wire cause | what the platform does | |---------------|------------------------------------|-----------------------------------| | active | 200 + valid in-window profile | enforce the tighten-only overlay | | revoked | 200 + status:"revoked" tombstone | release-to-native, stop polling | | stale | 401/5xx/timeout, or aged/expired | keep last-known-good, fail-closed | | absent | 404 / never fetched / not yours | fail-closed, alert operator |

The golden createPlatform() runtime closes this loop with durable, cross-side proof. On a fresh terminal pull, Gatekeeper first records the exact accepted authority with Phosra over a signed, idempotent PUT /api/v1/enforcement-endpoints/{binding_id}/revocation-authority; it then fetches the final profile. This exchange is SDK-managed—host applications do not construct or call it. Before Gatekeeper calls the host's release adapter, the router-signed tombstone must bind all six values from the accepted connection: link_id, child_commitment, profile_commitment, lifecycle_generation, predecessor_profile_ref, and predecessor_profile_artifact_digest. A crossed value refuses the release call.

After the host independently reads back released, Gatekeeper signs a PlatformLinkReleaseEventV1. In one PostgreSQL transaction it persists those exact bytes in the release outbox, advances the lifecycle to revoked, cancels every pending ordinary Link event for the delivery, and erases the sealed lifecycle secrets. The worker retries byte-identically after a crash or timeout and records completion only after verifying the provider-signed release ACK. The signed event and ACK keep a bounded five-minute issuance shape, but terminal proof does not become invalid merely because a provider outage lasts longer; future-dated, crossed, or tampered evidence still fails closed. If the accepted environment no longer matches the running platform or the provider is removed from current policy, the pending release dispatch is cancelled before transport with LINK_AUTHORITY_REVOKED instead of retrying or contacting that provider. This is the recommended integration path for an auditable Phosra Link unlink.

Hook release-to-native with the onRevoked callback. Once revoked, the state is terminal: the tombstone is never overwritten by a later stale profile, and refreshProfile() short-circuits (polling stops).

const gk = createGatekeeper({
  ...config,
  onRevoked: ({ childRef, endpointLabel }) => {
    // Release-to-native: drop the OCSS overlay, remove the "Managed via Phosra" badge.
    // Release is safe by construction — the overlay only TIGHTENS, so lifting it never
    // loosens anything the platform's own parent set. For OCSS-created artifacts (e.g. a
    // kid profile) convert to natively-managed: keep the data, clear the managed flag.
    releaseOverlay(childRef)
  },
})

// A poller stops the moment the endpoint is revoked (do NOT release on "stale"):
if (gk.endpointState() === "revoked") stopPolling()

A withdrawn standing is never un-withdrawn — re-connect is a fresh ceremony.

Error classes

import { RuleRefRequired, NoRatingMappingError, UnmappedRatingValueError } from "@phosra/gatekeeper"

try {
  await gk.reportParentChange({ ocssCategory: "unknown_cat", nativeValue: "PG", changeScope: "platform_local" })
} catch (e) {
  if (e instanceof NoRatingMappingError) { /* no mapping declared */ }
  if (e instanceof UnmappedRatingValueError) { /* nativeValue not in crosswalk */ }
}

Pricing & keys

This SDK is free and open (MIT). It performs signing and verification locally and calls hosted Phosra services over signed OCSS protocols. The golden runtime above takes no second API-key variable: its one Phosra secret is PHOSRA_CREDENTIAL. Credentials used to operate dashboard or management REST APIs are separate control-plane concerns, not Gatekeeper runtime configuration.

The /protocol subpath

import { verifyDocument, verifyReceipt } from "@phosra/gatekeeper/protocol"

@phosra/gatekeeper/protocol is a verbatim re-export of @openchildsafety/ocss — identical object identities. It lets you verify signed documents and receipts without a separate @openchildsafety/ocss install when you already depend on this package.

Stability

This package is 0.8.5 and is still co-evolving with the OCSS spec. Under 0.x semver, minor bumps (0.8 → 0.9) may include breaking changes with a one-minor deprecation window. Exports marked @experimental are excluded from that window.