Skip to content

Encryption

pg.encrypt() returns a Sealed builder. The builder captures encryption parameters but does no work until you call a terminal method.

Terminal methods

MethodWhat it doesReturns
sealed.toBytes()Encrypt and buffer in memoryPromise<Uint8Array>
sealed.upload()Encrypt and stream to Cryptify (silent, no Cryptify-sent emails)Promise<{ uuid }>
sealed.upload({ notify })Same, plus opt-in Cryptify-sent emailsPromise<{ uuid }>

Recipients

Before encrypting, build one or more recipients. PostGuard can encrypt with any wallet attribute. Email is the most common, but you can also target recipients by domain or custom attributes.

The SvelteKit example encrypts for a citizen (exact email) and an organisation (email domain):

ts
const sealed = pg.encrypt({
  files,
  recipients: [
    pg.recipient.email(citizen.email),
    pg.recipient.emailDomain(organisation.email)
  ],
  sign: pg.sign.apiKey(apiKey),
  onProgress,
  signal: abortController?.signal
});

Source: encryption.ts#L27-L33

Under the hood, pg.recipient.email() creates a policy with the attribute type pbdf.sidn-pbdf.email.email, while pg.recipient.emailDomain() extracts the domain from the email and uses pbdf.sidn-pbdf.email.domain.

Both methods return a RecipientBuilder that supports fluent chaining with .extraAttribute() to require additional attributes beyond the base email or domain:

ts
pg.recipient.email('alice@example.com')
  .extraAttribute('pbdf.gemeente.personalData.surname', 'Smith')
  .extraAttribute('pbdf.sidn-pbdf.mobilenumber.mobilenumber', '0612345678')

Encrypt and upload

Encrypts files, bundles them into a ZIP, and streams the encrypted data to Cryptify. Returns a UUID that recipients use to download and decrypt.

ts
const sealed = pg.encrypt({
  files,
  recipients: [pg.recipient.email(citizen.email), pg.recipient.emailDomain(organisation.email)],
  sign: pg.sign.apiKey(apiKey),
  onProgress,
  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 files',
    language: 'EN'
  }
});

Source: encryption.ts#L24-L60

WARNING

Requires cryptifyUrl to be set in the constructor.

Parameters

ParameterTypeRequiredDescription
filesFile[] | FileListYes*Files to encrypt (zipped automatically)
dataUint8Array | ReadableStreamYes*Raw data to encrypt (no zipping)
signSignMethodYesAuthentication method
recipientsRecipient[]YesOne or more recipients
onProgress(pct: number) => voidNoUpload progress callback (0-100)
signalAbortSignalNoCancel the operation

*Provide either files or data, not both.

Retry options

Pass retry on the PostGuardConfig to tune how chunk PUTs and downloads handle transient failures. Defaults are sensible — supply a partial object to override only what you need.

ts
const pg = new PostGuard({
  pkgUrl: 'https://pkg.staging.postguard.eu',
  cryptifyUrl: 'https://storage.staging.postguard.eu',
  retry: {
    maxAttempts: 5,
    chunkTimeoutMs: 60_000,
    onRetry: ({ attempt, maxAttempts, nextDelayMs }) => {
      console.log(`retrying in ${nextDelayMs} ms (attempt ${attempt} of ${maxAttempts})`);
    },
  },
});

Source: retry.ts#L3-L27

FieldTypeDefaultDescription
maxAttemptsnumber5Total attempts including the first one
initialDelayMsnumber500Delay before the first retry
maxDelayMsnumber30_000Cap on the pre-jitter exponential delay
multipliernumber2Multiplier applied between attempts
chunkTimeoutMsnumber60_000Per-attempt timeout for a chunk PUT
finalizeTimeoutMsnumber120_000Per-attempt timeout for the finalize call
downloadTimeoutMsnumber0 (off)Per-attempt timeout for the download GET. 0 means no per-attempt timeout — the retry budget bounds it instead
onRetry(event: RetryEvent) => voidundefinedFires after a retriable failure, before the backoff delay

RetryEvent carries attempt (1-indexed, the attempt that just failed), maxAttempts, the underlying error, and nextDelayMs. Use it to drive a "retrying… (attempt N of M)" indicator.

What gets retried: 5xx responses, fetch-level network errors (TypeError from Failed to fetch), and per-attempt timeout aborts. What does not: 4xx responses, UploadSessionExpiredError (see Error Handling), and caller-driven aborts via your AbortSignal. initUpload and finalizeUpload are deliberately not retried — both are session-defining steps where a silent retry could mask a server-side state mismatch.

The same retry config governs downloads. See Decryption — Retries and resumable downloads.

Resume an interrupted upload

A long-running upload can be interrupted by a page refresh, tab crash, navigation away, or process restart. The SDK exposes two primitives for rehydrating an in-flight session from Cryptify rather than starting over: the FileState type and the resumeUpload function.

FileState

FileState carries everything Cryptify needs to accept the next chunk for an in-flight upload. The two persistable fields are uuid and recoveryToken; the rest can be reconstructed by calling resumeUpload.

FieldTypeDescription
tokenstringCurrent rolling token sent on the next chunk PUT
prevTokenstring | undefinedToken from the most recent committed chunk. Used on retry so Cryptify's idempotent-retry path can replay a lost response. undefined until the first chunk is committed
uuidstringUpload UUID issued at init
recoveryTokenstringBearer credential issued by POST /fileupload/init (wire field recovery_token). Persist alongside uuid in consumer-owned storage

Source: cryptify.ts#L13-L33

resumeUpload

ts
import { resumeUpload, type FileState } from '@e4a/pg-js';

const { state, uploaded } = await resumeUpload(
  cryptifyUrl,
  uuid,
  recoveryToken,
  signal
);

Calls GET /fileupload/{uuid}/status with the X-Recovery-Token header and returns { state: FileState; uploaded: number }:

  • cryptify_token from the response is mapped to state.token.
  • prev_token is mapped to state.prevToken and is omitted before the first committed chunk.
  • uploaded is the byte offset to resume from.

Source: cryptify.ts#L143-L178

Failure mode

A 404 response with Cryptify's structured upload_session_not_found body surfaces as UploadSessionExpiredError. Cryptify deliberately collapses "unknown UUID" and "wrong recovery token" into the same response, so callers should treat both the same way: the session is gone, start a new upload. See UploadSessionExpiredError in the error reference.

Capture recoveryToken via onUploadInit

UploadOptions and CreateEnvelopeOptions accept an onUploadInit callback that hands the caller the {uuid, recoveryToken} pair needed by resumeUpload. Persist both fields to durable storage from inside the callback so a later session can rehydrate the upload after a process restart.

FieldTypeDescription
onUploadInit(info: { uuid: string; recoveryToken: string }) => voidFires once, synchronously, after upload_init resolves and before the first chunk PUT

The callback runs inside the upload stream's start handler. Keep the body short and synchronous; a throw errors the upload stream. A chrome.storage.local.set or localStorage.setItem is fine.

Source: types.ts#L107-L112

With Sealed.upload:

ts
const sealed = pg.encrypt({ sign, recipients, files });
const result = await sealed.upload({
  onUploadInit: ({ uuid, recoveryToken }) => {
    localStorage.setItem('pg-upload', JSON.stringify({ uuid, recoveryToken }));
  },
});

With createEnvelope, pass the same callback through CreateEnvelopeOptions:

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

const envelope = await createEnvelope({
  sealed,
  from,
  onUploadInit: ({ uuid, recoveryToken }) => {
    chrome.storage.local.set({ pgUpload: { uuid, recoveryToken } });
  },
});

After a restart, read the stored pair and call resumeUpload(cryptifyUrl, uuid, recoveryToken, signal) to recover the in-flight session.

Source: types.ts#L246-L250

Notify options

The upload is silent by default. Both recipient and sender mails are opt-in. Pass notify to enable either or both.

OptionTypeDefaultDescription
recipientsbooleanfalseSend a download-link email to each recipient
senderbooleanfalseSend a delivery confirmation to the sender
messagestringundefinedOptional unencrypted text included in any mail sent
language'EN' | 'NL''EN'Notification email template language

Encrypt raw data

For email addons, use data instead of files. The Thunderbird addon's crypto popup encrypts the full MIME message (body + attachments) as raw bytes, then wraps it in an email envelope:

ts
const sealed = pg.encrypt({
  sign: pg.sign.yivi({
    element: "#yivi-web-form",
    senderEmail: data.senderEmail,
  }),
  recipients,
  data: mimeData,
});

const envelope = await pg.email.createEnvelope({
  sealed,
  from: data.from,
  websiteUrl: data.websiteUrl,
});

Source: yivi-popup.ts#L90-L136

Call .toBytes() to get the encrypted data, or pass the Sealed object directly to pg.email.createEnvelope() for email integration.

Error handling

All encryption methods can throw:

  • PostGuardError: general SDK error
  • NetworkError: PKG or Cryptify communication failure (includes status and body properties)
  • YiviNotInstalledError: Yivi packages not installed (when using pg.sign.yivi)
  • YiviSessionError: the Yivi disclosure session ended without success (cancelled, timed out, aborted), only when using pg.sign.yivi

When the sender uses pg.sign.yivi(...), distinguish a cancelled disclosure from a real failure by checking YiviSessionError first:

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

try {
  const { uuid } = await pg.encrypt({
    files,
    recipients,
    sign: pg.sign.yivi({ element: '#yivi-web-form', senderEmail }),
  }).upload();
} catch (e) {
  if (e instanceof YiviSessionError) {
    showMessage(e.cancelled ? 'Sign-in cancelled.' : `Sign-in failed: ${e.reason}.`);
    return;
  }
  throw e;
}

See Error Handling for the full error reference.