JavaScript Middleware Example¶
This is the copyable JavaScript integration shape before Planisphere has a
packaged public SDK. The helper lives at
docs/examples/javascript/planisphereGate.mjs and uses only fetch-compatible
platform primitives.
Use it at the side-effect boundary: before an AI workflow files, sends, exports, posts, deploys, buys, deletes, or calls a tool that mutates a customer system.
Install Shape¶
Copy docs/examples/javascript/planisphereGate.mjs into the service that owns
the AI workflow. Configure:
baseUrl: Planisphere API host.apiKey: tenant API key.lawContext: matter/tool/doctrine metadata for the action.sourceKey: stable source key from your workflow state.
Generic Gate¶
import {
PlanisphereClient,
exampleLawContext,
} from "./planisphereGate.mjs";
const client = new PlanisphereClient({
baseUrl: "https://api.planisphere.ooo",
apiKey: process.env.PLANISPHERE_API_KEY,
});
export async function guardedAction() {
const result = await client.gateAction({
surface: "node-service",
sourceKey: "matter-safe:motion-draft-123",
proposedAction: "File an AI-drafted motion with unverified citations.",
lawContext: exampleLawContext(),
redactedText: "AI-drafted filing contains citations requiring review.",
});
if (result.status === "allowed") {
await executeSideEffect();
}
return result;
}
What The Helper Preserves¶
- Raw prompt/document content stays out of durable evidence by default.
needs_reviewbecomes a pause/resume object instead of a dead-end block.blockedreturnsexecute: falsefor workflow runners that prefer state over exceptions.evidencePacketgives the workflow a durable receipt link.
Verify The Receipt Seal¶
The helper can also submit the raw-safe packet fields to Planisphere's public verifier. That proves the returned seal matches the decision metadata without shipping Planisphere's private scoring, policy, or signing logic into the customer service.
const verification = await client.verifyEvidenceSeal(result.decision);
if (!verification.verified) {
throw new Error("Planisphere evidence seal verification failed.");
}
Bind Law Review Grades To The Mirror¶
For law review decisions, the helper carries a pinned public mirror rubric. It
computes the same digest as GET /law/grading-mirror, normalizes categorical
grades locally, and posts the full mirror tuple with the review decision. Raw
legal text does not go into the mirror tuple.
import {
canonicalLawMirrorDigest,
lawMirrorReviewFields,
} from "./planisphereGate.mjs";
const mirrorGrades = {
citation_support: "PARTIAL",
privilege_boundary: "PASS",
supervision_route: "PASS",
client_confidentiality: "PASS",
filing_readiness: "PARTIAL",
};
const fields = await lawMirrorReviewFields(mirrorGrades);
if (fields.mirror_digest !== await canonicalLawMirrorDigest()) {
throw new Error("Planisphere law mirror digest drifted.");
}
const review = await client.reviewDecision({
actionKey: result.decision.action_key,
reviewer: "partner-or-gc",
decision: "escalated",
reason: "Citation support needs partner review.",
mirrorGrades,
});
if (review.mirror_seal.mirror_digest !== fields.mirror_digest) {
throw new Error("Planisphere mirror seal did not bind the posted digest.");
}
const mirrorVerification = await client.verifyLawMirrorSeal(review);
if (!mirrorVerification.verified) {
throw new Error("Planisphere law mirror seal verification failed.");
}
Checked Test¶
The helper has a Node test:
node --test docs/examples/javascript/planisphereGate.test.mjs
The normal pytest suite runs that Node test through
tests/test_javascript_example.py.
Slack Review Modal Bridge¶
The copyable Slack bridge lives at
docs/examples/javascript/slackReviewServer.mjs. Use it for a tenant-owned
Slack app that renders Planisphere law review modals.
Slack interactivity sends application/x-www-form-urlencoded requests with a
JSON payload field. block_actions include the short-lived trigger_id
needed for Slack views.open; Slack expects the app to acknowledge within a
few seconds. The bridge preserves that shape:
- Slack posts a signed
block_actionsrequest when the reviewer clicksplanisphere_open_law_mirror_review. - The tenant app forwards Slack's exact raw form body and signature headers to
POST /integrations/callbacks/{tenant_id}/slack/{target}/open-review-modal. - Planisphere returns a raw-safe
views_open_requestcontaining the prepared modal view. - The tenant app calls Slack
views.openwith its bot token. - Slack posts a signed
view_submissionrequest; the tenant app forwards the exact form body toPOST /integrations/callbacks/{tenant_id}/slack/{target}/review-decision.
import { createSlackReviewServer } from "./slackReviewServer.mjs";
const server = createSlackReviewServer({
planisphereBaseUrl: "https://api.planisphere.ooo",
tenantId: "tenant-example-legal-ops",
target: "legal-review-channel",
slackBotToken: process.env.SLACK_BOT_TOKEN,
slackSigningSecret: process.env.SLACK_SIGNING_SECRET,
});
server.listen(3000);
The bridge does not use a Planisphere tenant API key for these callbacks. The
security boundary is Slack's signed request body, which Planisphere verifies
against the tenant integration's server-side signing secret reference. The bot
token is used only by the tenant app when calling Slack views.open; it is not
sent to Planisphere and not stored in Planisphere route metadata.
Keep this bridge close to the Planisphere API or set low timeouts. The example defaults each interaction call to a short timeout so the request can complete inside Slack's acknowledgement window.
Primary Slack docs used for this example:
Payload Retention: Recording Completed Acts¶
POST /v1/record (and /v1/record/batch) attests acts that already happened —
an output was marked, a disclosure was shown. Planisphere stores hash
commitments only — retain your payload to re-prove WHAT was recorded. Send
digests in source_payload (for example content_sha256); raw content is
rejected at the edge and is never stored. Every record response repeats this in
its payload_retention field. Wire your integration to archive the payload
bytes (or a durable pointer to them) alongside the returned action_key: the
sealed receipt proves a payload with that hash was recorded, when, and for whom
— only your retained copy proves what the payload said.
For a period export, page GET /tenant/exports/records?from=&to=&cursor= (the
signed recorded_at is the filter; unknown query parameters are rejected, never
silently ignored), then POST the action keys to
/evidence-packets/seal-bundle/batch as {"action_keys": [...]} (a wrapped
object — unlike POST /v1/record/batch, which takes a bare JSON array of
record bodies) for one offline-verifiable evidence package, and fetch
GET /v1/records/{action_key}/inclusion-proof for the canonical roll-up
inclusion proof. Records anchor on the hourly roll-up epoch, so a
just-sealed record returns an honest 409 with an estimated_next_epoch_at
hint until the next epoch.