Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/sdk/js-decryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,34 @@ const result = await opened.decrypt({

*Provide either `element` or `session`. If neither is provided, the SDK throws a `DecryptionError`.

## Retries and resumable downloads

The download GET retries transient failures using the same `retry` config as uploads — see [Encryption — Retry options](/sdk/js-encryption#retry-options) for the full table and defaults.

A mid-stream failure (network drop, idle timeout) does not start over from byte zero. The SDK reissues the GET with a `Range: bytes=<received>-` header and splices the resumed body onto what the consumer already saw. The retry counter is shared across resumes, so a flapping connection that delivers some bytes per attempt still exhausts the budget rather than looping forever.

```ts
const pg = new PostGuard({
pkgUrl, cryptifyUrl,
retry: {
onRetry: ({ attempt, maxAttempts, nextDelayMs }) => {
ui.showRetry(attempt, maxAttempts, nextDelayMs);
},
},
});

const result = await pg.open({ uuid }).decrypt({ element: '#yivi-web-form' });
result.download();
```

A resume is only accepted when Cryptify replies `206 Partial Content` with a `Content-Range` whose start byte matches the requested offset. A `200 OK` on a resume request is treated as fail-not-retry — some intermediaries (caching proxies, misconfigured CDNs) silently ignore `Range` and return the full body from byte zero, which would corrupt the decoded stream. The SDK surfaces the mismatch as a `NetworkError` so the retry loop short-circuits.

<small>[Source: cryptify.ts#L238-L277](https://github.com/encryption4all/postguard-js/blob/a60716e0b4eaaed0f3763a2eebbcf6c39fc0560d/src/api/cryptify.ts#L238-L277)</small>

::: warning Behaviour change in v1.6
The internal `downloadFileWithRetry` helper now returns its `ReadableStream` synchronously instead of via a `Promise`. Stream-level errors (including the no-more-retries terminal error) surface on the consumer's first `read()`, not as a function-level rejection. Callers using the public `pg.open({ uuid }).decrypt(...)` API are unaffected — the SDK consumes the stream internally and a single `await` still surfaces the same errors. Only direct consumers of `downloadFileWithRetry` need to adjust.
:::

## Recipient selection

When the ciphertext was encrypted for multiple recipients, the SDK needs to know which recipient key to use. Pass the `recipient` parameter with the email address of the intended recipient. If there is only one recipient in the ciphertext, the parameter can be omitted.
Expand Down
37 changes: 37 additions & 0 deletions docs/sdk/js-encryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,43 @@ Requires `cryptifyUrl` to be set in the constructor.

*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.yivi.app',
cryptifyUrl: 'https://fileshare.staging.yivi.app',
retry: {
maxAttempts: 5,
chunkTimeoutMs: 60_000,
onRetry: ({ attempt, maxAttempts, nextDelayMs }) => {
console.log(`retrying in ${nextDelayMs} ms (attempt ${attempt} of ${maxAttempts})`);
},
},
});
```

<small>[Source: retry.ts#L3-L27](https://github.com/encryption4all/postguard-js/blob/a60716e0b4eaaed0f3763a2eebbcf6c39fc0560d/src/util/retry.ts#L3-L27)</small>

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `maxAttempts` | `number` | `5` | Total attempts including the first one |
| `initialDelayMs` | `number` | `500` | Delay before the first retry |
| `maxDelayMs` | `number` | `30_000` | Cap on the pre-jitter exponential delay |
| `multiplier` | `number` | `2` | Multiplier applied between attempts |
| `chunkTimeoutMs` | `number` | `60_000` | Per-attempt timeout for a chunk PUT |
| `finalizeTimeoutMs` | `number` | `120_000` | Per-attempt timeout for the finalize call |
| `downloadTimeoutMs` | `number` | `0` (off) | Per-attempt timeout for the download GET. `0` means no per-attempt timeout — the retry budget bounds it instead |
| `onRetry` | `(event: RetryEvent) => void` | `undefined` | Fires 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](/sdk/js-errors#uploadsessionexpirederror)), 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](/sdk/js-decryption#retries-and-resumable-downloads).

### Notify options

The upload is silent by default. Both recipient and sender mails are opt-in. Pass `notify` to enable either or both.
Expand Down
35 changes: 35 additions & 0 deletions docs/sdk/js-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The SDK exports a hierarchy of error classes. All errors extend `PostGuardError`
Error
└── PostGuardError
├── NetworkError
│ └── UploadSessionExpiredError
├── YiviNotInstalledError
├── YiviSessionError
└── DecryptionError
Expand Down Expand Up @@ -47,6 +48,40 @@ Thrown when:
| `429` | Rate limited | Back off and retry |
| `500` | Server error | Retry later |

## `UploadSessionExpiredError`

A subclass of `NetworkError`. Thrown when Cryptify reports that the upload session is no longer known to the server — the session sat idle past its TTL, the server was restarted, or the path UUID is malformed. Distinct from `NetworkError` so retry policies short-circuit instead of burning the budget on something that will never recover.

Surfaced from both chunk PUT and finalize paths, parsed from Cryptify's structured 404 body (`upload_session_not_found`).

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `message` | `string` | `Upload session <uuid> is no longer known to the server (<reason>)` |
| `status` | `number` | Always `404` |
| `body` | `string` | Raw 404 response body |
| `uuid` | `string` | The upload UUID that expired |
| `reason` | `string` | Cryptify's structured reason: `'expired_or_unknown'`, `'invalid_uuid'`, `'file_missing'` |

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

try {
await pg.encrypt({ files, recipients, sign }).upload();
} catch (e) {
if (e instanceof UploadSessionExpiredError) {
showMessage('Your upload session expired. Please start over.');
return;
}
throw e;
}
```

<small>[Source: errors.ts#L31-L40](https://github.com/encryption4all/postguard-js/blob/a60716e0b4eaaed0f3763a2eebbcf6c39fc0560d/src/errors.ts#L31-L40)</small>

The user must start a new upload — there is no resume path once a session is gone. Catch this case before a generic `NetworkError` branch so the message is specific.

## `YiviNotInstalledError`

Thrown when `pg.sign.yivi()` or `opened.decrypt({ element })` is used but the required Yivi packages are not installed. The SDK attempts to dynamically import `@privacybydesign/yivi-core`, `@privacybydesign/yivi-client`, and `@privacybydesign/yivi-web`. If any import fails, this error is thrown.
Expand Down
Loading