Skip to content
PacSpace
Talk to us

TypeScript SDK

@pacspace-io/sdk: the client, its settings, the records calls, fingerprint, webhooks, and errors, with the signatures as published in 0.12.0.

bash
npm install @pacspace-io/sdk

Node.js 20.19 or later. The package has one dependency and ships its own verifier, so records.check runs as installed.

The client

Create the client where your system already logs what an agent did, such as the harness or the tool router, so the key stays with your code and the agent never holds it.

typescript
import { PacSpace } from '@pacspace-io/sdk';

const pac = new PacSpace({
  apiKey: process.env.PACSPACE_API_KEY!,
  webhookSecret: process.env.PACSPACE_WEBHOOK_SECRET, // only if you verify webhooks with this client
});
SettingDefaultWhat it does
apiKeyrequiredYour workspace's key. pk_test_ reaches your Sandbox, pk_live_ your Production; the host is https://app.pacspace.io for both.
webhookSecretnoneTurns on pac.webhooks.
maxRetries2Sends a request again after a network error or a timeout, or a 408, 429, 500, 502, 503, or 504. A 429 or a 503 that names its wait in retryAfterSeconds is sent again after exactly that; otherwise the wait grows between tries. records.history and records.disclose are sent once.
timeout30000Milliseconds per request.
bufferWritestrueA write that still fails after its retries is held in memory and sent again in the background, with a growing wait between tries, so the records.emit promise settles only when the write lands or the tries run out. Set false to have records.emit throw instead. pac.close() rejects any write still held.
bufferMaxAttempts100How many times a held write is tried before records.emit rejects with the last error.
fetchthe runtime'sA fetch of your own, for a proxy or a test.
baseUrl, sandboxUrl, productionUrlhttps://app.pacspace.ioOverrides for a private host of your own. You do not set these.

fingerprint

typescript
import { fingerprint, writeBlindingFile, readBlindingFile, blindingFileText } from '@pacspace-io/sdk';

fingerprint(
  input: File | Blob | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array> | Iterable<Uint8Array> | AsyncIterable<Uint8Array>,
  options?: { blind?: boolean; alg?: 'sha-256' | 'hmac-sha-256'; blinding?: Blinding },
): Promise<{ ref: PayloadRef; blinding: Blinding | null }>;   // ref: { alg: 'hmac-sha-256' | 'sha-256'; digest: string; byteLength: string }
writeBlindingFile(filePath: string, blinding: Blinding): Promise<string>;   // writes <filePath>.pacspace.json and returns that path; Node only
readBlindingFile(path: string): Promise<Blinding>;                       // Node only
blindingFileText(blinding: Blinding): string;                            // the blinding file's text

Blinded by default: blinding is null only with { blind: false } or { alg: 'sha-256' }. Pass { blinding } to fingerprint the same file again and get the same ref. A stream or an iterable, such as a Node stream, is read in chunks; a File, a Blob or an ArrayBuffer is read whole. A Blinding shows no value when printed, and JSON.stringify throws on it, so log the ref instead. See Fingerprint a file.

records.emit

typescript
pac.records.emit(input: {
  record: string;                 // your id for the record (wire: entityId)
  title: string;                  // at most 120 characters
  occurredAt: string;             // RFC 3339, UTC, ending in Z
  actorId: string;
  payloads: PayloadRef[];         // at least one
  kind?: string;                  // lower case with hyphens
  instructedBy?: string;
  description?: string;
  note?: string;
  references?: Array<{ recordKey: string; entry: number }>;   // entries count from 1
  amends?: { recordKey: string; entry: number };              // the entry this one corrects; send lifecycle 'amended'
  recordType?: string;            // default 'machine-action-record'
  lifecycle?: 'recorded' | 'amended' | 'closed';   // default 'recorded'
  idempotencyKey?: string;        // one key is one write; wire: referenceId
}, options?: RequestOptions): Promise<RecordEmitResponse>;

RecordEmitResponse carries intakeId, receiptId, recordType, entityId, recordKey, lifecycle, status (QUEUED), referenceId, receivedAt, and message. A repeat of a finished key returns the original answer; the wire's idempotent field is not passed on. On a repeat of a key whose first write is still running it carries code: 'IDEMPOTENCY_PENDING' and retryAfterSeconds, and does not throw. See Write an entry and, for amends, Correcting an entry.

Every entry the SDK takes counts from 1, as the pages do: entry: 1 is the record's first entry. The SDK sends seq = entry - 1 on the wire and throws ValidationError for an entry below 1 before anything is sent. A payload that holds a blinding, a blinding file, or a whole fingerprint result is refused with RECORD_BLINDING_INCLUDED before anything is sent.

records.history

typescript
pac.records.history(input: {
  record: string;
  fromEntry?: number;             // counted from 1
  toEntry?: number;
  recordType?: string;
}, options?: RequestOptions): Promise<RecordHistory>;

Returns the history file whole, with entries[] (seq, status, code, sentence, receiptId), receipts[], bundleDigest, and nextFromSeq read from the paging header. The file is the wire document the check verifies by its own fingerprint, so its seq values stay the wire's, counted from 0; nextFromSeq is a wire number too, and the next page's fromEntry is nextFromSeq + 1. See History.

records.receipt

typescript
pac.records.receipt(input: {
  record: string;
  entry: number;                  // counted from 1
  recordType?: string;
}, options?: RequestOptions): Promise<RecordReceiptResponse>;   // { recordType, entityId, receipt, verify }

See Receipts.

records.check

typescript
pac.records.check(input: {
  history: RecordHistory;
  expect?: Array<{
    entry: number;                                     // counted from 1
    payloads?: PayloadRef[];                           // fingerprints you kept
    files?: Array<{ source: FingerprintInput; blinding?: Blinding | null }>;   // files you hold, each with its blinding file
  }>;
  source?: { kind: 'mirror' | 'direct'; url?: string; address?: string };
}): Promise<RecordHistoryCheck>;
// { ok: boolean; entries: Array<{ entry, status, expected?, kind?, code?, sentence? }>; failedCheck: string | null;
//   sourceChecked: boolean; reason?: string; blindingFileNeeded?: number[]; note?: string }
// each result's entry is seq + 1

Runs in your process over every committed entry's seal, the order, and the file's fingerprint. expect adds, per entry, whether the files or fingerprints you hold are among the committed ones. source also reads what was committed from outside PacSpace. Never throws on a failed check. blindingFileNeeded lists the entries whose blinded fingerprints were not checked for want of a blinding file, and note says so. See Check.

Each row of the result's entries carries kind: attempt for an entry the record's rules did not allow, failure for a failure record, and transition for any other committed entry. It is read from what was committed for the entry, and only for an entry whose seal passed the check; for any other entry it is null.

records.disclose

typescript
pac.records.disclose(
  input: {
    record: string;
    recordType?: string;
    entries: Array<{ entry: number; reveal: 'all' | string[] }>;   // entry counted from 1, as the pages count
    source: RecordCheckSource;
  },
  options?: RequestOptions,
): Promise<{ bytes: Uint8Array; schema: 'record-link-copy/v1' }>;   // write bytes to a file; nothing is uploaded

records.disclose({ record, entries, source }) returns a file that holds the record's seals and the fields you choose, for each entry 'all' or a list of fields by pointer, such as ['/title', '/payloads/0']. A lab can hand an outside evaluator the title and the tool call's fingerprint of entry 3 and nothing else of it. Hand it to whoever needs it: they check it with @pacspace-io/check, or drop it on the record's Shared Record page. The entries of an older record, one whose history file says record-history-bundle/v1, can be revealed only whole.

records.disclose reads GET /api/v1/records/{recordType}/{record}/disclosure-source with your API key: the record's seal history and its full history in one answer, which is not cached. A record with more than 5000 entries on the ledger answers 413 RECORD_HISTORY_TOO_LARGE, and records.disclose passes the error on.

Pass source, the public ledger to read, as in records.check: { kind: 'mirror' } for the public mirror of the network the record is on, or your own URL. records.disclose checks the file against the ledger there before it returns, and returns no file it cannot check.

webhooks

typescript
const pac = new PacSpace({ apiKey, webhookSecret });

pac.webhooks.verify(signature: string, timestamp: string, rawBody: string, options?: { tolerance?: number }): WebhookEvent;
pac.webhooks.verifyFromHeaders(headers: Record<string, string | string[] | undefined>, rawBody: string, options?): WebhookEvent;
pac.webhooks.middleware(options?)   // Express-style: sets req.pacspaceEvent, answers 401 on a bad signature

WebhookEvent is { event, timestamp, data }; data is typed for record.committed and record.failed. tolerance is in seconds, 300 by default. The raw body must be the bytes as received, before any JSON parsing. See Signature verification.

Errors

Every API error is a PacSpaceError with statusCode, code, message, and requestPath. A refusal with status 400, 403, 409, 413, or 422, such as RECORD_TITLE_TOO_LONG, carries the API's code exactly. A 401, 402, 404, 412, 429, or 503 arrives as its own class instead: InvalidApiKeyError, InsufficientCreditsError, NotFoundError, ContractNotDeployedError, RateLimitError (retryAfter is the seconds the API named), and ServiceUnavailableError, each with the API's message. ValidationError is a 400 with no code, or the SDK's own refusal before anything is sent, and WebhookVerificationError comes from webhooks. The full table is in Errors.

An agent harness that writes after each tool call can treat a refused entry as its own event, apart from the agent's work:

typescript
import { PacSpaceError } from '@pacspace-io/sdk';

try {
  await pac.records.emit({
    record: 'eval-run-4417',
    title,
    occurredAt,
    actorId: 'agent-7',
    payloads: [ref],
    idempotencyKey: `eval-run-4417:${eventId}`,
  });
} catch (err) {
  if (!(err instanceof PacSpaceError)) throw err;
  if (err.code === 'RECORD_TITLE_TOO_LONG') {
    // Nothing was queued: shorten the title, move the rest to description, and send again.
  } else {
    console.error(err.statusCode, err.code, err.message);
  }
}

Types

PacSpaceConfig, RequestOptions, PayloadRef, FingerprintInput, FingerprintOptions, FingerprintResult, Blinding, EntryNumber, RecordEntryRef, RecordLifecycle, RecordEntryStatus, RecordEmitInput, RecordEmitResponse, RecordHistoryInput, RecordHistory, RecordHistoryEntry, RecordReceiptInput, RecordReceiptResponse, RecordCheckInput, RecordCheckExpect, RecordCheckFile, RecordCheckSource, RecordCheckEntryResult, RecordHistoryCheck, RecordDiscloseInput, RecordDisclosureFile, WebhookEvent, RecordCommittedPayload, and RecordFailedPayload are exported from the package root.