@opsf/pct-core
v0.1.0
Published
Create, sign, and verify Privacy Claims Tokens (PCT) — reference implementation of the PCT specification v0.1
Maintainers
Readme
@opsf/pct-core
Reference implementation of the Privacy Claims Token (PCT) Specification v0.1 — create, sign, verify, and audit cryptographically bound data-obligation tokens.
PCT is a portable, tamper-evident token that encodes data obligations (lawful basis, allowed purposes, jurisdiction rules, consent status, etc.) and travels with data through processing pipelines, enabling runtime compliance enforcement.
Features
- Token creation & signing — RS256 (recommended) and HS256 algorithms
- 8-check verification — signature, expiry, purpose, jurisdiction, consent, data category, transfer, extensions
- Cryptographic data binding — SHA-2 hash of canonicalized data (RFC 8785) prevents token/data substitution
- Tamper-evident audit records — every verification generates a hashable audit trail
- Payload validation — check payloads against the PCT v0.1 schema before signing
- Extension support — pluggable checks for
x-{framework}:{field}claims (HIPAA, AI Act, DORA, etc.) - Zero runtime dependencies — uses the Web Crypto API (Node.js >= 18, browsers, edge runtimes)
- Full TypeScript types — strict types for all PCT structures
Install
npm install @opsf/pct-coreRequires Node.js >= 18 (uses the Web Crypto API).
Quick start
import {
createPCT,
verifyPCT,
generateRS256KeyPair,
createAuditRecord,
} from "@opsf/pct-core";
// 1. Generate a signing key
const key = await generateRS256KeyPair("my-key-1");
// 2. Create a PCT bound to your data
const data = { patient_id: "P-123", readings: [1, 2, 3] };
const token = await createPCT(
{
valid_from: Math.floor(Date.now() / 1000),
expires_at: Math.floor(Date.now() / 1000) + 86400 * 365,
issuer: "https://example.com",
subject_id: "dataset:patient-cohort",
subject_type: "dataset",
data_origin: "GB",
data_categories: ["health"],
lawful_basis: { bases: ["consent"], framework: "UK_GDPR" },
allowed_purposes: ["clinical_analytics"],
consent_status: true,
consent_scope: ["clinical_analytics"],
jurisdiction_rules: { permitted_regions: ["GB"] },
hash_algorithm: "sha-256",
hash_scope: "full_payload",
},
key,
data, // automatically canonicalized, hashed, and bound
);
// 3. Verify the PCT
const result = await verifyPCT(
{
pct: token,
requested_action: "process",
requested_purpose: "clinical_analytics",
processing_region: "GB",
requestor_id: "analytics-service",
request_timestamp: Math.floor(Date.now() / 1000),
request_id: crypto.randomUUID(),
},
key,
{ data }, // verify data binding integrity
);
console.log(result.decision); // "ALLOW"
// 4. Generate a tamper-evident audit record
const audit = await createAuditRecord(request, result);
console.log(audit.record_hash); // 64-char hex SHA-256See the examples/ directory for more detailed scenarios.
Architecture
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ createPCT │───>│ Compact PCT │───>│ verifyPCT │
│ (sign.ts) │ │ header. │ │ (verify.ts) │
│ │ │ payload. │ │ │
│ ┌─────────┐ │ │ signature │ │ 8-check │
│ │ hashJSON│ │ └──────────────┘ │ procedure │
│ │ (bind) │ │ │ │
│ └─────────┘ │ │ ┌─────────┐ │
└─────────────┘ │ │ audit │ │
│ │ record │ │
│ └─────────┘ │
└──────────────┘Module map
| Module | Purpose | Spec section |
|---|---|---|
| sign.ts | Token creation and signing | §5, §6 |
| decode.ts | Parse compact tokens (no verification) | §5.1 |
| verify.ts | 8-check verification procedure | §6 |
| audit.ts | Tamper-evident audit records | §8 |
| schema.ts | Payload validation against v0.1 schema | §5 |
| keys.ts | Key generation, import, export | §5.2 |
| hashing.ts | SHA-2 hashing with base64url output | §5.8 |
| canonicalize.ts | JSON Canonicalization (RFC 8785) | §5.8 |
| encoding.ts | Base64url encode/decode (RFC 4648) | §5.1 |
| types.ts | All TypeScript type definitions | Full spec |
API reference
Token lifecycle
| Function | Description |
|---|---|
| createPCT(input, key, data?) | Create and sign a PCT. Auto-generates pct_id, issued_at, and data_hash. |
| decodePCT(compact) | Decode a compact PCT string into { header, payload, signature } without verifying. |
| verifyPCT(request, keys, options?) | Run the full 8-check verification. Returns { decision, checks, header, payload }. |
| validatePayload(payload) | Validate a payload object against the PCT v0.1 schema constraints. |
Verification checks
verifyPCT performs the spec-mandated 8-check procedure in order:
| # | Check | Field(s) evaluated | Blocks when |
|---|---|---|---|
| 1 | Signature | header.alg, header.kid | Signature invalid or no matching key |
| 2 | Expiry | valid_from, expires_at | Current time outside validity window |
| 3 | Purpose | allowed_purposes | Requested purpose not listed |
| 4 | Jurisdiction | jurisdiction_rules | Region not permitted or explicitly restricted |
| 5 | Consent | consent_status, consent_scope | Consent required but missing or insufficient |
| 6 | Data category | data_categories, lawful_basis | Special categories without adequate legal basis |
| 7 | Transfer | transfer_restrictions | Destination not permitted or no mechanism |
| 8 | Extensions | extensions | Any extension check fails |
A single failed check results in BLOCK. Pass data in options to also verify data binding integrity.
Verification options
const result = await verifyPCT(request, keys, {
// Override current time for testing (seconds since epoch)
now: 1743000000,
// Verify data binding — recomputes hash and compares to data_hash
data: originalPayload,
// Custom extension checker for regulatory frameworks
extensionChecker: (extensions, request) => [
{
check_name: "x-hipaa:covered_entity",
result: extensions["x-hipaa:covered_entity"] ? "pass" : "fail",
reason: "HIPAA covered entity verification",
},
],
});Key management
| Function | Description |
|---|---|
| generateRS256KeyPair(kid) | Generate an RSA-2048 key pair (recommended for multi-party). |
| generateHS256Key(kid) | Generate an HMAC shared secret (single-organisation only). |
| importRS256PublicKey(kid, jwk) | Import a public key from JWK for verification only. |
| importHS256Key(kid, rawSecret) | Import a shared secret from raw ArrayBuffer. |
| exportRS256KeyPair(keyPair) | Export key pair to JWK format for storage or distribution. |
Data hashing
| Function | Description |
|---|---|
| hashJSON(data, algorithm?) | Canonicalize (RFC 8785) and hash. Returns base64url. Default: sha-256. |
| hashString(data, algorithm?) | Hash a UTF-8 string (CSV, XML, etc.). |
| hashBytes(data, algorithm?) | Hash raw Uint8Array bytes. |
| canonicalize(value) | JSON Canonicalization Scheme (RFC 8785). Sorted keys, no whitespace. |
Audit
| Function | Description |
|---|---|
| createAuditRecord(request, result) | Generate a tamper-evident audit record with SHA-256 record_hash. |
| buildVerificationResponse(result, audit) | Build a spec-compliant verification response (§7.3). |
Schema validation
import { validatePayload } from "@opsf/pct-core";
const result = validatePayload(untrustedPayload);
if (!result.valid) {
for (const err of result.errors) {
console.error(`${err.field}: ${err.message}`);
}
}The full JSON Schema (Draft 2020-12) is also available at schema/pct-schema-0.1.json for use with standard validators like Ajv.
Supported algorithms
Signing
| Algorithm | Mechanism | Use case | |---|---|---| | RS256 | RSASSA-PKCS1-v1_5 + SHA-256 | Recommended. Multi-party / cross-organisation. | | HS256 | HMAC + SHA-256 | Single-organisation only. Must not cross org boundaries. |
Data hashing
| Algorithm | Status |
|---|---|
| sha-256 | Recommended |
| sha-384 | Supported |
| sha-512 | Supported |
| MD5 | Prohibited (collision attacks) |
| SHA-1 | Prohibited (collision attacks) |
Examples
The examples/ directory contains fully commented, runnable examples:
| Example | Description |
|---|---|
| 01-create-and-verify.ts | Complete token lifecycle: create, verify, audit |
| 02-blocked-request.ts | Demonstrates all block scenarios (wrong purpose, region, key, tampered data) |
| 03-ai-interaction.ts | AI model interaction with EU AI Act context and extensions |
| 04-cross-border-transfer.ts | Cross-border data transfer with SCCs and adequacy decisions |
| 05-schema-validation.ts | Payload validation against the PCT schema |
Run any example with:
npx tsx examples/01-create-and-verify.tsDevelopment
# Install dependencies
npm install
# Run tests (71 tests across 6 suites)
npm test
# Type-check
npm run lint
# Build
npm run build
# Format code
npm run formatSee CONTRIBUTING.md for the full development guide.
Spec conformance
This library implements the PCT Specification v0.1 including:
- Compact serialisation format (
header.payload.signature, per RFC 7519 §7.2) - All required and conditional payload fields (§5)
- RS256 and HS256 signing algorithms (§5.2)
- Data binding via RFC 8785 canonicalization + SHA-2 hashing (§5.8)
- The 8-step verification procedure (§6)
- Enforcement API request/response contracts (§7)
- Tamper-evident audit record generation (§8)
- Payload validation against the v0.1 JSON Schema
- Extension namespace support (
x-{framework}:{field}) - Conditional fields:
consent_status/consent_scope,ai_context,transfer_restrictions
License
License TBD.
