// examples/compliance-integrations/index.js
import { setTimeout as sleep } from "node:timers/promises";
import readline from "node:readline/promises";
const BASE = process.env.ORBIT_API ?? "https://api.orbit.devotel.io/api/v1";
const KEY = process.env.ORBIT_API_KEY;
if (!KEY) throw new Error("set ORBIT_API_KEY");
async function orbit(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
...(KEY ? { Authorization: `Bearer ${KEY}` } : {}),
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
throw new Error(`${method} ${path} -> ${res.status}: ${await res.text()}`);
}
return res.json();
}
const poll = async (fn, done, { everyMs = 5000, tries = 60 } = {}) => {
for (let i = 0; i < tries; i++) {
const v = await fn();
if (done(v)) return v;
await sleep(everyMs);
}
throw new Error("poll budget exhausted");
};
// --- A. DSAR intake: begin as your webform would, then verify + submit.
async function dsarIntake(email, phone) {
const begun = await orbit("POST", "/compliance/public/dsar/begin", {
email, phone, request_type: "delete",
});
const claimId = begun.data.claim_id;
const rl = readline.createInterface({ input: process.stdin });
const code = await rl.question("enter the emailed 6-digit code: ");
rl.close();
await orbit("POST", "/compliance/public/dsar/verify-email", {
claim_id: claimId, code,
});
await orbit("POST", "/compliance/public/dsar/submit", { claim_id: claimId });
const sla = await orbit("GET", "/compliance/dsar/sla");
console.log("dsar queued; sla:", sla.data);
return claimId;
}
// --- B. Ledger exports: suppression + consent now, audit-export polled.
async function exportLedgers(outDir = ".") {
const { writeFileSync } = await import("node:fs");
const to = new Date().toISOString().slice(0, 10);
const from = "2026-01-01"; // your retention start
const sup = await fetch(
`${BASE}/compliance/suppression-list/export?from=${from}&to=${to}`,
{ headers: { Authorization: `Bearer ${KEY}` } },
);
if (sup.headers.get("x-export-truncated")) console.warn("suppression export truncated");
writeFileSync(`${outDir}/suppression.csv`, await sup.text());
const job = await orbit(
"POST", `/compliance/audit-export?from=${from}&to=${to}&format=json`,
);
const done = await poll(
() => orbit("GET", `/compliance/audit-export/${job.data.id}`),
(j) => j.data.status === "complete" || j.data.status === "failed",
);
if (done.data.status !== "complete") throw new Error(done.data.error ?? "audit export failed");
console.log("audit export ready:", done.data.download_url);
}
// --- C. 10DLC: status check; on FAILED decode the rejection to a fix card.
async function tenDlcStatus() {
const status = await orbit("GET", "/compliance/10dlc/status");
for (const brand of status.data.brands ?? []) {
if (brand.status === "FAILED" && brand.rejectionReason) {
const card = await orbit("POST", "/compliance/10dlc/decode-rejection", {
code: brand.rejectionReason.slice(0, 200),
free_text: brand.rejectionReason,
});
console.log("fix card:", card.data);
if (card.data.resubmit_allowed === false) {
console.log("record locked: register a fresh brand instead");
}
}
}
return status.data.verdict ?? status.data;
}
// --- D. Pre-send posture gate.
async function canSendNow(phone, channel = "sms") {
const r = await orbit(
"GET",
`/compliance/quiet-hours/preview?phone=${encodeURIComponent(phone)}&channel=${channel}`,
);
if (!r.data.allowed_now) {
console.log(`blocked (${r.data.reason}); queue until ${r.data.next_allowed_at}`);
}
return r.data.allowed_now;
}
const phone = process.env.RECIPIENT_PHONE ?? "+14155550100";
await canSendNow(phone);
await tenDlcStatus();
await exportLedgers();
if (process.env.DSAR_EMAIL) await dsarIntake(process.env.DSAR_EMAIL, phone);
console.log("all four integrations exercised");