Skip to content

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_review becomes a pause/resume object instead of a dead-end block.
  • blocked returns execute: false for workflow runners that prefer state over exceptions.
  • evidencePacket gives 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:

  1. Slack posts a signed block_actions request when the reviewer clicks planisphere_open_law_mirror_review.
  2. The tenant app forwards Slack's exact raw form body and signature headers to POST /integrations/callbacks/{tenant_id}/slack/{target}/open-review-modal.
  3. Planisphere returns a raw-safe views_open_request containing the prepared modal view.
  4. The tenant app calls Slack views.open with its bot token.
  5. Slack posts a signed view_submission request; the tenant app forwards the exact form body to POST /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.