# JLINC SDK [![npm version](https://img.shields.io/npm/v/@jlinc/sdk.svg)](https://www.npmjs.com/package/@jlinc/sdk) [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE) A TypeScript/JavaScript SDK for the [JLINC](https://www.jlinc.io) API enabling verifiable data-sharing agreements, cryptographically signed data exchange, and zero-knowledge auditing. ## Overview The JLINC SDK provides a unified interface to the JLINC Server API, covering: - **Identity management** - Create and resolve decentralized identifiers (DIDs) with federation-formatted short names. - **Verifiable Contractual Agreements (VCAs)** - Produce and cross-sign agreements that govern data-sharing permissions. - **Cryptographic event records** - Produce, retrieve, and process data exchange events under agreement governance. - **Zero-knowledge auditing** - Retrieve and verify tamper-proof audit records from third-party archives. - **Fine-grained access control** - Check AuthZEN-formatted permissions before executing sensitive operations. ## Installation ```bash npm install @jlinc/sdk ``` ```bash yarn add @jlinc/sdk ``` ```bash pnpm add @jlinc/sdk ``` ## Quick Start ```typescript import { init, createEntity, produceAgreement, processAgreement, produceEvent, getAudits, verifyAudit, auth } from "@jlinc/sdk"; // 1. Initialize the SDK with your JLINC Server configuration init({ apiKey: process.env.JLINC_API_KEY!, apiUrl: process.env.JLINC_API_URL!, archiveUrl: process.env.JLINC_ARCHIVE_URL!, archiveKey: process.env.JLINC_ARCHIVE_KEY!, fedidUrl: process.env.JLINC_FEDID_URL!, }, { // Optional: set a default agreement ID fallback agreementId: "00000000-0000-0000-0000-000000000000", }); // 2. Create an identity const entity = await createEntity({ shortName: `my-app@${(await getDomains({ auth: { subject: { type: "user", id: "admin" }, action: { name: "read" }, resource: { type: "data", id: "demo" } } }))[0]}`, }); // 3. Produce and process an agreement const agreement = await produceAgreement({ data: { references: ["https://sisa.jlinc.org/v1/34020c5fb59ebc6507ebca4eb38090aec1097c6aec8d2ae2250ddfed4b4aa63c"], permitted: ["data-sharing"], prohibited: ["non-delegable"], shortNames: [entity.didDoc.shortName], validRoles: ["provider", "user", "third-party"], }, shortName: entity.didDoc.shortName, role: "provider", }); await processAgreement({ agreementId: agreement.created.agreementId, shortName: entity.didDoc.shortName, role: "user", }); // 4. Exchange data under the agreement const event = await produceEvent({ type: "data", senderShortName: "sender@domain.tld", recipientShortName: "recipient@domain.tld", agreementId: agreement.created.agreementId, meta: { myCustomId: "unique-id-123" }, data: { query: "Hello, world!" }, }); // 5. Audit and verify const audits = await getAudits({ agreementId: agreement.created.agreementId }); const result = await verifyAudit({ audits: audits.auditRecords || [], shortNames: ["sender@domain.tld", "recipient@domain.tld"], }); ``` ## Configuration ### `init(config, defaults?)` Initialize the SDK with your server credentials and optional default parameters. All subsequent API calls inherit these settings. ```typescript init( { apiKey: string; // Bearer token for the Core API apiUrl: string; // JLINC Core API base URL (e.g. "https://api.jlinc.io") archiveUrl: string; // JLINC Archiver base URL for audit records archiveKey: string; // Bearer token for the Archiver API fedidUrl: string; // FedID service URL for identity resolution }, defaults?: { agreementId?: string; // Default agreement ID (fallback for all calls that accept one) } ): void ``` | Parameter | Description | |-----------|-------------| | `apiKey` | Bearer token for authenticating requests to the Core API | | `apiUrl` | Base URL of the JLINC Core API endpoint | | `archiveUrl` | Base URL of the JLINC Archiver (audit) endpoint | | `archiveKey` | Bearer token for authenticating requests to the Archiver API | | `fedidUrl` | URL of the FedID service for domain resolution and identity operations | ## API Reference All methods return the parsed response data from the JLINC API. Where responses include a `created` object, it contains the newly generated identifiers (e.g., `created.agreementId`, `created.eventId`). --- ### `auth(params)` Perform a fine-grained authorization check using an AuthZEN-formatted request. Returns `{ decision: true }` if the subject is permitted, or `{ decision: false }` if denied. ```typescript type AuthSubject = { type: string; // e.g. "user", "system", "ai-agent" id: string; // Unique identifier for the subject }; type AuthAction = { name: string; // e.g. "read", "write", "delete" }; type AuthResource = { type: string; // e.g. "data", "agreement", "event" id: string; // Resource identifier properties?: { ownerID?: string; // Optional data owner ID }; }; type AuthParams = { subject: AuthSubject; action: AuthAction; resource: AuthResource; }; auth(params: AuthParams): Promise<{ decision: boolean }>; ``` **Example:** ```typescript const result = await auth({ subject: { type: "user", id: "myuser" }, action: { name: "read" }, resource: { type: "data", id: "1234", properties: { ownerID: "owner@test.com" }, }, }); if (result.decision) { // Proceed with the operation } else { // Deny access } ``` --- ### `getDomains(params)` Retrieve the list of available federation domains configured on your JLINC Server. ```typescript type GetDomainsParams = { fedidUrl?: string; // Optional: override the configured fedidUrl auth: AuthParams; // AuthZEN-formatted authorization payload }; getDomains(params: GetDomainsParams): Promise; ``` **Example:** ```typescript const domains = await getDomains({ auth: { subject: { type: "user", id: "admin" }, action: { name: "read" }, resource: { type: "data", id: "domains" }, }, }); console.log(domains); // ["test.jlinc.io", "example.com", ...] ``` --- ### `createEntity(params)` Create a new identity (entity) with a federation-formatted short name. Returns the entity's DID document. ```typescript type CreateEntityParams = { fedidUrl?: string; // Optional: override the configured fedidUrl shortName: string; // Federation-format name: "name@domain.tld" }; createEntity(params: CreateEntityParams): Promise<{ didDoc: { shortName: string; did: string; ... } }>; ``` **Example:** ```typescript const domain = (await getDomains({ auth: /* ... */ }))[0]; const system = await createEntity({ fedidUrl: "http://localhost:8881", shortName: `system@${domain}`, }); console.log(system.didDoc.shortName); // "system@test.jlinc.io" console.log(system.didDoc.did); // "did:jlinc:..." ``` --- ### `getEntity(params)` Retrieve an existing entity's DID document by its short name. ```typescript type GetEntityParams = { shortName: string; // The entity's federation-format short name }; getEntity(params: GetEntityParams): Promise<{ data: { didDoc: { shortName: string; did: string; ... } } }>; ``` **Example:** ```typescript const result = await getEntity({ shortName: "system@test.jlinc.io" }); console.log(result.data.didDoc.shortName); ``` --- ### `produceAgreement(params)` Create a Verifiable Contractual Agreement (VCA). The agreement is cryptographically signed, audited, and delivered to the archive service. ```typescript type ProduceAgreementParams = { data: { references?: string[]; // External references (e.g. SISA policy URIs) permitted?: string[]; // Allowed operations prohibited?: string[]; // Disallowed operations shortNames?: string[]; // Entities that must sign before the agreement is valid validRoles?: string[]; // Permitted roles (e.g. "provider", "user", "third-party") public?: boolean; // If true, the agreement is publicly resolvable by any API key }; shortName: string; // Entity that produces (initially signs) the agreement role: string; // Role for the initial signature archive?: { url: string; // Optional: override the configured archiveUrl key: string; // Optional: override the configured archiveKey }; }; produceAgreement(params: ProduceAgreementParams): Promise<{ created: { agreementId: string }, ... }>; ``` **Example:** ```typescript const agreement = await produceAgreement({ data: { references: ["https://sisa.jlinc.org/v1/34020c5fb59ebc6507ebca4eb38090aec1097c6aec8d2ae2250ddfed4b4aa63c"], permitted: ["data-sharing"], prohibited: ["non-delegable"], shortNames: [provider.didDoc.shortName], validRoles: ["provider", "user", "third-party"], }, shortName: provider.didDoc.shortName, role: "provider", }); console.log(agreement.created.agreementId); // "58810f16-b976-41f0-..." ``` --- ### `processAgreement(params)` Cross-sign an agreement as a second entity, finalizing its validity. This is how the "user" party agrees to the terms set by the "provider". ```typescript type ProcessAgreementParams = { agreementId?: string; // Optional: falls back to init() defaults.agreementId shortName: string; // Entity that processes (cross-signs) the agreement role: string; // Role for this entity's signature archive?: { url: string; key: string; }; }; processAgreement(params: ProcessAgreementParams): Promise<...>; ``` **Example:** ```typescript await processAgreement({ agreementId: agreement.created.agreementId, shortName: user.didDoc.shortName, role: "user", }); ``` --- ### `getAgreement(params)` Retrieve an agreement, optionally including its signatures. Falls back to the default `agreementId` from `init()` if not specified. ```typescript type GetAgreementParams = { agreementId?: string; // Optional: falls back to init() defaults.agreementId includeSignatures?: boolean; // Whether to include signature data }; getAgreement(params: GetAgreementParams): Promise<...>; ``` **Example:** ```typescript const agreement = await getAgreement({ agreementId: "58810f16-b976-41f0-9566-2c26f174b592", includeSignatures: true, }); ``` --- ### `produceEvent(params)` Produce a cryptographically signed event record - used when two entities exchange data (e.g., user input, agent handoff, API call). ```typescript type ProduceEventParams = { type: string; // Event type (e.g. "data", "command", "result") senderShortName: string; // Sending entity's short name recipientShortName: string; // Receiving entity's short name agreementId?: string; // Optional: falls back to init() defaults.agreementId (use "nil" ID for audits only) meta?: Record; // Custom metadata for indexing and lookups data: unknown; // The actual payload being exchanged archive?: { url: string; key: string; }; }; produceEvent(params: ProduceEventParams): Promise<{ created: { eventId: string }, ... }>; ``` **Example:** ```typescript const event = await produceEvent({ type: "data", senderShortName: "alice@test.jlinc.io", recipientShortName: "bob@test.jlinc.io", agreementId: agreement.created.agreementId, meta: { myCustomId: "unique-identifier-456" }, data: { query: "Why am I making this?" }, }); ``` --- ### `getEvent(params)` Retrieve an event by its unique ID or by custom metadata keys. ```typescript type GetEventParams = { eventId?: string; // Optional: lookup by event ID meta?: Record; // Optional: lookup by meta keys (AND logic for multiple keys) }; getEvent(params: GetEventParams): Promise<...>; ``` **Example - by ID:** ```typescript const event = await getEvent({ eventId: "evt-12345" }); ``` **Example - by meta ID:** ```typescript const event = await getEvent({ meta: { myCustomId: "unique-identifier-456" }, }); ``` --- ### `processEvent(params)` Cross-sign an event to indicate the recipient has received and will use the data. This finalizes the event record. ```typescript type ProcessEventParams = { eventId: string; // The event to process shortName: string; // Receiving entity that processes the event archive?: { url: string; key: string; }; }; processEvent(params: ProcessEventParams): Promise<...>; ``` **Example:** ```typescript await processEvent({ eventId: event.created.eventId, shortName: "bob@test.jlinc.io", }); ``` --- ### `getAudits(params)` Retrieve audit records from the zero-knowledge archive. Uses the `archiveKey` for authentication. ```typescript type GetAuditsParams = { agreementId?: string; // Optional: falls back to init() defaults.agreementId eventId?: string; // Optional: lookup audits for a specific event meta?: Record; // Optional: lookup audits by meta keys (AND logic) }; getAudits(params: GetAuditsParams): Promise<{ auditRecords: AuditRecord[], ... }>; ``` **Example:** ```typescript // Get all audits for an agreement const audits = await getAudits({ agreementId: "58810f16-b976-41f0-9566-2c26f174b592" }); // Get audits for a specific event const eventAudits = await getAudits({ eventId: "evt-12345" }); // Get audits by custom meta ID const metaAudits = await getAudits({ meta: { myCustomId: "unique-456" } }); ``` --- ### `verifyAudit(params)` Verify the integrity of audit records against the local data store. Returns arrays of `valid` and `invalid` records with detailed validation results. ```typescript type VerifyAuditParams = { audits: AuditRecord[]; // Audit records to verify shortNames: string[]; // Entity short names that signed the records }; type AuditResult = { audit: AuditRecord; results: { validId: boolean; // Event ID matches local data store validSignature: boolean; // Cryptographic signature is valid validAuditHash: boolean; // Audit hash matches stored data validAuditSignature: boolean; // Audit record signature is valid validMatchingDids: boolean; // Signing DIDs match target object validEventAgreement: boolean; // Event signing DIDs signed the governing agreement validEventAgreementSignature: boolean; // Agreement signatures are valid }; }; verifyAudit(params: VerifyAuditParams): Promise<{ valid: AuditResult[], invalid: AuditResult[] }>; ``` **Example:** ```typescript const audits = await getAudits({ agreementId: agreement.created.agreementId }); const result = await verifyAudit({ audits: audits.auditRecords || [], shortNames: [ "alice@test.jlinc.io", "bob@test.jlinc.io", ], }); console.log(result.valid.length); // Number of valid records console.log(result.invalid.length); // Number of tampered/invalid records ``` ## Validation Result Fields When `verifyAudit` returns results, each boolean field means: | Field | Description | |-------|-------------| | `validId` | The event ID in the audit record matches the local data store. | | `validSignature` | The cryptographic signature on the stored data is valid. | | `validAuditHash` | The hash in the audit record is a valid representation of stored data. | | `validAuditSignature` | The cryptographic signature on the audit record is valid. | | `validMatchingDids` | The DIDs that signed the audit also signed the target object. | | `validEventAgreement` | The DIDs that signed the audit signed the governing agreement. | | `validEventAgreementSignature` | The signatures on the governing agreement are valid. | Any record with a single `false` field is placed in the `invalid` array. ## Complete Workflow Example ```typescript import { init, getDomains, createEntity, getEntity, produceAgreement, processAgreement, getAgreement, produceEvent, getEvent, processEvent, getAudits, verifyAudit, auth, } from "@jlinc/sdk"; // Step 0: Initialize init({ apiKey: "your-api-key", apiUrl: "https://api.jlinc.io", archiveUrl: "https://api.jlinc.io", archiveKey: "your-archive-key", fedidUrl: "https://fedid.jlinc.io", }); // Step 1: Create identities const domains = await getDomains({ auth: { subject: { type: "user", id: "admin" }, action: { name: "read" }, resource: { type: "data", id: "demo" }, }, }); const provider = await createEntity({ fedidUrl: "https://fedid.jlinc.io", shortName: `provider@${domains[0]}`, }); const user = await createEntity({ fedidUrl: "https://fedid.jlinc.io", shortName: `user@${domains[0]}`, }); // Step 2: Sign agreements const agreement = await produceAgreement({ data: { references: ["https://sisa.jlinc.org/v1/34020c5fb59ebc6507ebca4eb38090aec1097c6aec8d2ae2250ddfed4b4aa63c"], permitted: ["data-sharing"], prohibited: ["non-delegable"], shortNames: [provider.didDoc.shortName], validRoles: ["provider", "user", "third-party"], }, shortName: provider.didDoc.shortName, role: "provider", }); await processAgreement({ agreementId: agreement.created.agreementId, shortName: user.didDoc.shortName, role: "user", }); // Step 3: Exchange data const event = await produceEvent({ type: "data", senderShortName: user.didDoc.shortName, recipientShortName: provider.didDoc.shortName, agreementId: agreement.created.agreementId, meta: { myCustomId: "session-abc" }, data: { query: "What is the weather?" }, }); await processEvent({ eventId: event.created.eventId, shortName: provider.didDoc.shortName, }); // Step 4: Audit and verify const audits = await getAudits({ agreementId: agreement.created.agreementId }); const verification = await verifyAudit({ audits: audits.auditRecords || [], shortNames: [user.didDoc.shortName, provider.didDoc.shortName], }); ``` ## Nil Agreement If JLINC is being used for zero-knowledge auditing only (no formal agreement), the nil agreement ID can be used: ```typescript const event = await produceEvent({ type: "data", senderShortName: "alice@test.jlinc.io", recipientShortName: "bob@test.jlinc.io", agreementId: "00000000-0000-0000-0000-000000000000", // Nil agreement meta: { myCustomId: "audit-only-1" }, data: { query: "test" }, }); ``` All subsequent operations (`getEvent`, `getAudits`, `verifyAudit`) work normally under the nil agreement. ## Fine-Grained Access Control with AuthZEN Every API call can optionally include an `auth` parameter for AuthZEN-formatted authorization checks. If the subject lacks permission, the server returns `{ decision: false }`. ```typescript // Check authorization before proceeding const check = await auth({ subject: { type: "user", id: "myuser" }, action: { name: "read" }, resource: { type: "data", id: "1234", properties: { ownerID: "owner@test.com" } }, }); if (check.decision) { // Authorized - proceed with the operation } ``` ## Environment Variables The SDK does not load environment variables automatically. Use a library like `dotenv` in your application: ```typescript import dotenv from "dotenv"; dotenv.config(); init({ apiKey: process.env.JLINC_API_KEY!, apiUrl: process.env.JLINC_API_URL!, archiveUrl: process.env.JLINC_ARCHIVE_URL!, archiveKey: process.env.JLINC_ARCHIVE_KEY!, fedidUrl: process.env.JLINC_FEDID_URL!, }); ``` ## Type Exports ```typescript export type { AuthSubject, AuthAction, AuthResource, AuthParams, GetDomainsParams, CreateEntityParams, GetEntityParams, ProduceAgreementParams, ProcessAgreementParams, GetAgreementParams, ProduceEventParams, GetEventParams, ProcessEventParams, GetAuditsParams, VerifyAuditParams, AuditResult, }; export { init, auth, getDomains, createEntity, getEntity, produceAgreement, processAgreement, getAgreement, produceEvent, getEvent, processEvent, getAudits, verifyAudit, }; ``` ## Error Handling The SDK uses JavaScript `Promise` rejection for network errors and non-2xx responses. Wrap calls in `try/catch` blocks: ```typescript try { const agreement = await produceAgreement({ data: { permitted: ["data-sharing"] }, shortName: "my-entity@domain.tld", role: "provider", }); } catch (error) { if (error.response) { // Server responded with an error status console.error(error.response.status, error.response.data); } else if (error.request) { // Request was made but no response received console.error("No response from server"); } else { // Something else went wrong console.error(error.message); } } ``` ## Documentation Full usage guide: [docs.jlinc.io/usage-guide](https://docs.jlinc.io)