Skip to content

Latest commit

 

History

History
311 lines (235 loc) · 10.4 KB

File metadata and controls

311 lines (235 loc) · 10.4 KB

postguard-js

GitHub · TypeScript · JavaScript/TypeScript SDK

The official PostGuard SDK for browsers and Node.js. Published as @e4a/pg-js on npm.

PostGuard encrypts data for recipients based on their identity attributes (email address, phone number, etc.) rather than traditional public keys. Recipients prove their identity via Yivi to decrypt. No key exchange or certificates needed.

Installation

npm install @e4a/pg-js

Optional peer dependencies for Yivi web UI integration:

npm install @privacybydesign/yivi-core @privacybydesign/yivi-client @privacybydesign/yivi-web

These are not needed when using API key signing or custom session callbacks.

Quick Start

import { PostGuard } from '@e4a/pg-js';

const pg = new PostGuard({
  pkgUrl: 'https://pkg.staging.postguard.eu',
  cryptifyUrl: 'https://storage.staging.postguard.eu', // optional, for file upload flows
  headers: { 'X-My-Client': 'v1.0' },                // optional
});

Client version header

The SDK stamps an X-POSTGUARD-CLIENT-VERSION header onto every PKG and Cryptify request so the servers can attribute traffic by SDK and version. The value has four comma-separated parts: host,host_version,pg-js,<version>, where host is the detected runtime (browser, node, bun, or deno).

You do not need to set it. If you pass your own X-POSTGUARD-CLIENT-VERSION in headers (any casing), it wins. Embedding hosts use this to report their own identity, for example the Outlook add-in reports pg4ol.

Source: encryption4all/postguard-js#90.

Architecture

The SDK uses a lazy builder pattern. pg.encrypt() and pg.open() return builder objects that capture parameters but do no work. The actual operation runs when you call a terminal method.

// Encrypt: nothing happens until .upload() or .toBytes()
const sealed = pg.encrypt({ files, recipients, sign });
await sealed.upload();                                       // silent: stream to Cryptify, no emails
await sealed.upload({ notify: { recipients: true } });       // + email each recipient a link
const bytes = await sealed.toBytes();                        // encrypt + buffer in memory

// Decrypt: nothing happens until .inspect() or .decrypt()
const opened = pg.open({ uuid });
const info = await opened.inspect();                  // peek at recipients and sender
const result = await opened.decrypt({ element: '#yivi-web' });
result.download();

Encryption

Encrypt files and upload to Cryptify

const sealed = pg.encrypt({
  sign: pg.sign.apiKey('your-api-key'),
  recipients: [pg.recipient.email('bob@example.com')],
  files: fileList,
  onProgress: (pct) => console.log(`${pct}%`),
  signal: abortController.signal,
});

// Silent upload — no Cryptify-sent emails. Returns UUID for custom delivery.
const { uuid } = await sealed.upload();

// Or opt into Cryptify-sent emails. `recipients: true` emails each recipient
// with a download link; `sender: true` adds a confirmation back to the
// sender. Both default false.
const { uuid } = await sealed.upload({
  notify: {
    recipients: true,
    sender: false,
    message: 'Here are your documents.',
    language: 'EN',
  },
});

The size of each chunk sent to Cryptify is configurable via uploadChunkSize on the PostGuard constructor. The default is 5 000 000 bytes (5 MB), matching the chunk_size default in Cryptify. Override only if the Cryptify deployment has been reconfigured to accept a different chunk size.

const pg = new PostGuard({
  pkgUrl: 'https://pkg.staging.postguard.eu',
  cryptifyUrl: 'https://storage.staging.postguard.eu',
  uploadChunkSize: 10_000_000, // 10 MB chunks
});

Source: src/types.ts

Encrypt raw data (no upload)

For email clients and custom integrations that handle their own transport:

const sealed = pg.encrypt({
  sign: pg.sign.apiKey('your-api-key'),
  recipients: [pg.recipient.email('bob@example.com')],
  data: myDataBytes, // Uint8Array or ReadableStream
});

const encrypted = await sealed.toBytes();
// encrypted is a Uint8Array: attach it, send it, store it however you want

Decryption

Inspect before decrypt

const opened = pg.open({ uuid: 'the-file-uuid' });
const info = await opened.inspect();
// info.recipients: ['bob@example.com']
// info.sender: { email: 'alice@example.com', attributes: [...] }

Decrypt from Cryptify UUID (Yivi web)

const opened = pg.open({ uuid: 'the-file-uuid' });
const result = await opened.decrypt({
  element: '#yivi-popup',
  recipient: 'bob@example.com', // optional hint
  enableCache: true,            // cache JWT for repeated decryptions
});

result.download('decrypted-files.zip');
console.log('Sender:', result.sender);

The example above assumes the payload was uploaded with Sealed.upload({ files }). When the upload used Sealed.upload({ data }), the same call returns DecryptDataResult with result.plaintext (a Uint8Array) instead of files and blob. Narrow with 'plaintext' in result. See Decrypt from Cryptify UUID for details.

Decrypt raw data

const opened = pg.open({ data: encryptedBytes });
const result = await opened.decrypt({
  element: '#yivi-popup',
  recipient: 'bob@example.com',
});
// result.plaintext is a Uint8Array
// result.sender contains verified sender identity

Signing Methods

// Business API key (server-side). The SDK forwards this key to Cryptify
// uploads as `Authorization: Bearer <apiKey>`, which unlocks the higher
// upload-quota tier (see /repos/cryptify#upload-limits).
pg.sign.apiKey('your-api-key')

// Yivi web session (browser, inline QR code)
pg.sign.yivi({
  element: '#yivi-popup',
  senderEmail: 'alice@example.com',
  attributes: [                    // optional: request extra attributes
    { t: 'pbdf.gemeente.personalData.fullname', optional: true },
    { t: 'pbdf.sidn-pbdf.mobilenumber.mobilenumber', optional: true },
  ],
  includeSender: true,             // optional: also encrypt for the sender
})

// Custom session callback (email addons, mobile apps, etc.)
pg.sign.session(
  async ({ con, sort }) => {
    // Show your own Yivi UI, return the JWT
    return await myCustomYiviFlow(con, sort);
  },
  { senderEmail: 'alice@example.com' }
)

Recipients

// Encrypt for an exact email address
pg.recipient.email('bob@example.com')

// Encrypt for anyone with an email at a domain
pg.recipient.emailDomain('bob@example-org.com')

// Require extra attributes (fluent chaining)
pg.recipient.email('bob@example.com')
  .extraAttribute('pbdf.gemeente.personalData.surname', 'Smith')
  .extraAttribute('pbdf.sidn-pbdf.mobilenumber.mobilenumber', '0612345678')

Email Integration

For email clients (Thunderbird, Outlook, etc.) that need to encrypt/decrypt email bodies:

import { buildMime, extractCiphertext, injectMimeHeaders } from '@e4a/pg-js';

// Build inner MIME message
const mime = buildMime({
  from: 'alice@example.com',
  to: ['bob@example.com'],
  subject: 'Hello',
  htmlBody: '<p>Secret message</p>',
  attachments: [{ name: 'doc.pdf', type: 'application/pdf', data: pdfBuffer }],
});

// Encrypt and create email envelope
const sealed = pg.encrypt({
  sign: pg.sign.yivi({ element: '#yivi-popup', senderEmail: 'alice@example.com' }),
  recipients: [pg.recipient.email('bob@example.com')],
  data: mime,
});

const envelope = await pg.email.createEnvelope({ sealed, from: 'alice@example.com' });
// envelope.subject → "PostGuard Encrypted Email"
// envelope.htmlBody → Placeholder HTML with PostGuard branding
// envelope.plainTextBody → Plain text fallback
// envelope.attachment → File("postguard.encrypted") | null  (null in tier 3, see /sdk/js-email-helpers)
// envelope.tier → "tier1" | "tier2" | "tier3"
// envelope.uploadUuid → Cryptify UUID (string) when uploaded, otherwise null

// --- On the receiving side ---
const ciphertext = extractCiphertext({
  htmlBody: emailBodyHtml,
  attachments: [{ name: 'postguard.encrypted', data: attachmentBuffer }],
});

const opened = pg.open({ data: ciphertext });
const result = await opened.decrypt({ element: '#yivi-popup', recipient: 'bob@example.com' });
// result.plaintext → decrypted MIME content
// result.sender → verified sender identity

Standalone email helpers (buildMime, extractCiphertext, injectMimeHeaders) can be imported directly without creating a PostGuard instance:

import { buildMime, extractCiphertext, injectMimeHeaders } from '@e4a/pg-js';

Error Handling

import {
  PostGuardError,
  NetworkError,
  YiviNotInstalledError,
  DecryptionError,
  IdentityMismatchError,
} from '@e4a/pg-js';

try {
  await sealed.upload();
} catch (err) {
  if (err instanceof IdentityMismatchError) {
    // Yivi attributes didn't match the encryption policy
  } else if (err instanceof YiviNotInstalledError) {
    // Yivi peer dependencies not installed
  } else if (err instanceof NetworkError) {
    console.log(err.status, err.body);
  }
}

Server-side usage

The SDK runs on browsers, Node.js (20.3+), Bun, and Deno. The lower bound is set by AbortSignal.any, listed in engines.node of the package.

Encrypt and upload calls work identically across all four runtimes. Decryption with pg.sign.yivi(...) and opened.decrypt({ element }) is browser-only — both render the Yivi QR widget into a DOM element. Calling pg.sign.yivi(...) from a server runtime throws YiviSessionError upfront before any session starts; pick pg.sign.apiKey(...) or pg.sign.session(...) instead.

See the pg-node example for a runnable Node.js script using pg.sign.apiKey.

Development

Prerequisites

  • Node.js 20.3+ (or Bun / Deno)

Building

npm install
npm run prebuild  # Generate WASM base64
npm run build     # TypeScript build via tsdown

Testing

npm run test      # Vitest
npm run typecheck # tsc

Releasing

This repository uses semantic-release for automated npm publishing. Conventional commits on main trigger version bumps and releases automatically.

CI/CD

Workflow Trigger What it does
integration.yml Push/PR Type checking, build, tests
delivery.yml Push to main Integration checks then semantic-release