Integrations / Server-side TypeScript
Cloudflare Agents SDK
Record the decision from your server.
Extend the existing Agent class and await the ADR write from an authenticated server-side decision path. Read Recourse configuration from this.env.
import { Agent } from 'agents';
import { recordDecision, type DecisionMetadata, type RecourseEnv } from './record-decision';
export class DecisionAgent extends Agent<RecourseEnv> {
// Call from an authenticated, server-side decision path in your existing Agent.
// Do not expose environment values or private inputs through setState or RPC.
async recordPublicDecision(meta: DecisionMetadata) {
return await recordDecision(this.env, meta);
}
}Received: request acknowledged. Acknowledged is not durable. Durable: committed to the configured persistence layer. Sealed: included in a signed root with a receipt. The API wire values remain accepted, durable and sealed. Searchable-through is a separate index coverage watermark: not available in this preview.
Development signer means integrity under a published dev key and no independently trusted time.
Use your existing Durable Object binding and authenticated Agent routing. This example adds no public callable method and no browser state. Await the acknowledgement; schedule later receipt retrieval through the application’s existing durable workflow.
Choose what enters the record
All five SDK examples use the same recordDecision helper. Supply RECOURSE_URL, RECOURSE_TENANT_ID and RECOURSE_API_KEY from the server environment. It sends POST /v1/adr with Bearer authentication, tenant scope and the event’s idempotency key.
Public metadata uses opaque subject, actor and action tokens; token syntax alone does not make an identifier nonidentifying. The helper always sends private_fields: {}.
Read the shared transport and retry handling
/** Server-only ADR transport. Documentation example; no top-level network call. */
export type RecourseEnv = {
RECOURSE_URL: string;
RECOURSE_TENANT_ID: string;
RECOURSE_API_KEY: string;
};
export type DecisionMetadata = {
eventId: string;
occurredAt: string;
subjectId: string;
actorId: string;
action: string;
outcome: 'allow' | 'deny' | 'defer' | 'error' | 'abstain' | 'other';
};
export class RecourseHTTPError extends Error {
constructor(readonly status: number, readonly retryAfterSeconds?: number) {
super(`Recourse request failed (HTTP ${status})`);
this.name = 'RecourseHTTPError';
}
}
const token = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const outcomes = new Set(['allow', 'deny', 'defer', 'error', 'abstain', 'other']);
export function decisionPayload(meta: DecisionMetadata) {
if (![meta.eventId, meta.subjectId, meta.actorId, meta.action].every(v => typeof v === 'string' && token.test(v)) ||
!outcomes.has(meta.outcome) ||
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(meta.occurredAt) ||
!Number.isFinite(Date.parse(meta.occurredAt)) || new Date(meta.occurredAt).toISOString() !== meta.occurredAt) {
throw new Error('Invalid public decision metadata');
}
return {
schema_version: '1.0' as const,
occurred_at: meta.occurredAt,
subject_id: meta.subjectId,
actor: { type: 'agent' as const, id: meta.actorId },
decision: { action: meta.action, outcome: meta.outcome },
private_fields: {},
};
}
export async function recordDecision(
env: RecourseEnv,
meta: DecisionMetadata,
transport: typeof fetch = fetch,
pause: (ms: number) => Promise<void> = ms => new Promise(resolve => setTimeout(resolve, ms)),
): Promise<{ adrId: string; ack: 'accepted' | 'durable' | 'sealed' }> {
let base: URL;
try { base = new URL(env.RECOURSE_URL); } catch { throw new Error('Invalid Recourse environment'); }
if ((base.protocol !== 'https:' && !(base.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(base.hostname))) ||
base.username || base.password || base.search || base.hash ||
typeof env.RECOURSE_TENANT_ID !== 'string' || !token.test(env.RECOURSE_TENANT_ID) ||
typeof env.RECOURSE_API_KEY !== 'string' || !env.RECOURSE_API_KEY || /[\r\n]/.test(env.RECOURSE_API_KEY)) {
throw new Error('Invalid Recourse environment');
}
// Keep the same event ID, timestamp and serialized payload across all retries.
const body = JSON.stringify(decisionPayload(meta));
for (let attempt = 0; attempt < 3; attempt++) {
let response: Response;
try {
response = await transport(new URL('/v1/adr', base), {
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10_000),
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${env.RECOURSE_API_KEY}`,
'X-Tenant-ID': env.RECOURSE_TENANT_ID,
'Idempotency-Key': meta.eventId,
},
body,
});
} catch {
if (attempt === 2) throw new Error('Recourse transport unavailable; retain this event for retry');
await pause(250 * 2 ** attempt);
continue;
}
const retryHeader = response.headers.get('Retry-After');
const retryAfter = retryHeader && /^\d+$/.test(retryHeader) ? Number(retryHeader) : undefined;
if (!response.ok) {
await response.body?.cancel();
// Quota, auth, conflict and schema errors require caller action; do not retry them here.
if (![408, 500, 502, 503, 504].includes(response.status) || attempt === 2 || (retryAfter ?? 0) > 30) {
throw new RecourseHTTPError(response.status, retryAfter);
}
await pause(Math.max(250 * 2 ** attempt, (retryAfter ?? 0) * 1_000));
continue;
}
let result: unknown;
try { result = await response.json(); } catch { throw new Error('Invalid Recourse acknowledgement; retain this event for retry'); }
const record = (result as { records?: { adr_id?: unknown; ack?: unknown }[] })?.records?.[0];
if (!record || typeof record.adr_id !== 'string' || !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/.test(record.adr_id) ||
!['accepted', 'durable', 'sealed'].includes(String(record.ack))) {
throw new Error('Invalid Recourse acknowledgement; retain this event for retry');
}
return { adrId: record.adr_id, ack: record.ack as 'accepted' | 'durable' | 'sealed' };
}
throw new Error('Recourse transport unavailable; retain this event for retry');
}Keep the decision through retries
Keep the original metadata in a durable application outbox. Retry the ADR write after a transport failure without rerunning a completed provider action. The helper makes up to three attempts for transient errors, including the initial request; authentication, schema, conflict and quota failures return to the caller. It never logs response bodies or credentials.
After the request is acknowledged, poll the record status through the normal minute boundary. Fetch a sealed receipt and verify it with an explicit trust anchor. A Durable response means persistence, while Sealed means a receipt is available for verification.
R0 examples are source-checked and syntax-checked, with the shared HTTP transport tested using local mocks. Provider packages are not installed or executed here; full SDK type compatibility and live provider integration remain unqualified. The ADR schema is provisional pending the unavailable D-002 source.
API references checked 5 September 2026: Cloudflare Agents API. Documentation examples only; no provider call or live Collector export was executed.