@rekhu/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to `@rekhu/core`. Versions follow [semantic versioning](https://semver.org).
4
4
 
5
+
6
+ ## 0.3.0 · 9 November 2026
7
+
8
+ In parity with rekhu/core 0.3.0 for Laravel:
9
+
10
+ - **Typed errors** under `RekhuError`: `InvalidKey`, `ModuleNotEnabled`, `RegionUnavailable`, `CreditExhausted`, `AppPaused`, `FileNotFound`, `InvalidRequest`, `RateLimited`, `Unavailable`, and `errorFor()`.
11
+ - **Retries without double charges:** a dropped connection, 429, 502 or 503 is tried again up to three times; charged calls carry an `idempotency-key`, and a replayed answer is not kept in the ledger twice.
12
+ - **Large files streamed** from a path; `downloadTo(fileId, path)`; `remove(fileId)`.
13
+ - **File names in any language** arrive whole.
14
+ - **Seshat:** `codes()`, structured `options` on `ask` findings, unknown fixes refused and not charged, side effects in `applied`, `resolved`.
15
+ - `download()` and `feedback()` send the tenant and user; `knowers` is optional; no account offer in error messages.
16
+ ## 0.2.0 · 6 November 2026
17
+
18
+ - **Seshat in modules:** `seshat().modules()` says which modules this application may use (`preflight`, `ai_enhance`); a call to a module that is off is refused with `module_not_enabled`.
19
+ - **ai_enhance:** `seshat().enhance(picture, modes, options, extra)` improves one picture without a check:
20
+ - the modes are upscale to a print size, extend, denoise, sharpen, restore, remove_background, replace_background, remove_object, vectorize and colour_correct;
21
+ - large pictures run as a job: `poll(jobId)`, or a webhook checked with `verifyWebhook()`;
22
+ - `enhancePreview(fileId)` gives a before / after picture, and `upload()` keeps a mask or background picture.
23
+
5
24
  ## 0.1.0 · 6 November 2026
6
25
 
7
26
  - **Rekhu Core for Node.js.** Rekhu's knowers in a Node.js back end, with the app's own tenants (`tenant`), languages (`locale`), limits a day per tenant and user (`limits`, `limitsFor`), its own price per call (`price`) and every call kept in a ledger (`memoryLedger`, `fileLedger`, or the app's own) and passed to `onUsage`.
package/README.md CHANGED
@@ -54,6 +54,50 @@ It fixes:
54
54
  - CMYK colours and embedded fonts;
55
55
  - pictures, made into print PDFs.
56
56
 
57
+ **Every code, decision and fix in one list:** `await seshat.codes()` (free) answers the versioned list to map your own tool's names against. Every `ask` finding carries `options` (`upload_new`, `print_as_is`, `fix:<fix>[:<option>]`), so you can offer the choice without `explain`.
58
+
59
+ **Fixes only, your own check:** `upload()` the file (free), then `fix(fileId, [{ fix: 'add_bleed' }, { fix: 'convert_cmyk' }], order)`. An unknown fix or option is refused (`InvalidRequest`) and not charged. `applied` lists every change, side effects too (`side_effect_of`), and `resolved` the codes the file no longer has.
60
+
61
+ **Large files:** pass a path instead of a Buffer and the file is streamed; `downloadTo(fileId, path)` writes the result straight to disk; `remove(fileId)` deletes a kept file at once. File names keep every character.
62
+
63
+ **What it costs:** `meta.usage` has one line per charged item: the check (`seshat`) and the explanation (`seshat_explain`). A refused call, or a fix with nothing to do, costs nothing.
64
+
65
+ ## Seshat AI: one picture improved (ai_enhance)
66
+
67
+ For a platform with its own preflight that only wants Seshat's image work, or next to the preflight. The module is switched on per application by Rekhu; `modules()` tells which ones this key may use.
68
+
69
+ | Mode | What it does | Options |
70
+ |---|---|---|
71
+ | `upscale` | More pixels, up to 4×: to a resolution at a print size | `ppi`, `width_mm`, `height_mm`, or `factor` |
72
+ | `extend` | Grows the picture beyond its edges (bleed); a little per side on CPU | `all_mm`, or `top_mm` `right_mm` `bottom_mm` `left_mm` (or `_px`) |
73
+ | `denoise`, `sharpen` | Cleans JPEG artefacts and noise; crisper edges | `strength`, `amount` |
74
+ | `restore` | An old or scanned photo: scratches filled, noise and faded colour corrected | |
75
+ | `remove_background` | Transparent PNG, or a solid colour | `background: '#ffffff'` |
76
+ | `replace_background` | A new background from a picture you send | `background_file_id` (from `upload`) |
77
+ | `remove_object` | Fills an object or blemish from its surroundings | `region: {x, y, w, h}`, or `mask_file_id` |
78
+ | `vectorize` | A logo or line drawing as SVG. A photo is refused (`not_vectorizable`) | |
79
+ | `colour_correct` | Exposure and white balance | |
80
+
81
+ Every answer says what it invented in `warnings` (an edge, a filled area, a reconstructed detail), with a `confidence` from 0 to 1. RGB stays RGB, and CMYK stays CMYK unless the mode needs otherwise. Files are kept a day and never used for training. A large picture runs as a job: poll it, or give a webhook URL. The webhook is signed `x-rekhu-signature: t=…,v1=…` with the `webhook_secret` the call answered. Each mode is priced on its own, and `meta.usage` says per item what it cost.
82
+
83
+ Limits:
84
+ - at most 40 megapixels in and 120 megapixels out;
85
+ - upscaling at most 4×;
86
+ - extending at most 12 % of a side on CPU;
87
+ - removing at most a quarter of the picture.
88
+
89
+ ```js
90
+ const s = rekhu.seshat({ tenant: { id: tenant.uuid, name: tenant.name } });
91
+ await s.modules(); // { modules: { preflight: false, ai_enhance: true }, modes: [...] }
92
+ const r = await s.enhance(photoBuffer, ['denoise', 'upscale'], { ppi: 300, width_mm: 100, height_mm: 70 }, { name: 'photo.png', webhookUrl: 'https://shop.example.com/hooks/rekhu' });
93
+ const { image } = await s.enhancePreview(r.file_id); // before | after, a data: URL
94
+ const png = await s.download(r.file_id); // a Buffer
95
+ await s.poll('j_…'); // for a job
96
+
97
+ import { verifyWebhook } from '@rekhu/core';
98
+ if (!verifyWebhook(rawBody, req.headers['x-rekhu-signature'], storedSecret)) return res.status(401).end();
99
+ ```
100
+
57
101
  ## Rekhu Machines
58
102
 
59
103
  ```js
@@ -99,5 +143,7 @@ The knowers and this package are the same in both.
99
143
 
100
144
  ## Errors
101
145
 
102
- - `RekhuError` carries `status` and `type`.
146
+ - Every refusal is a `RekhuError` with `status` and `type`, and its own class: `InvalidKey` (401, also a key from another Rekhu platform), `ModuleNotEnabled` and `RegionUnavailable` (403), `CreditExhausted` and `AppPaused` (402), `FileNotFound` (404), `InvalidRequest` (422, not charged), `RateLimited` and `Unavailable` (after retries).
147
+ - A dropped connection, 429, 502 or 503 is tried again up to three times. Charged calls carry an `idempotency-key`, so a retry is never charged twice (`meta.replayed`).
148
+ - `knowers` (or `REKHU_KNOWERS`) is optional: empty means every knower the key allows.
103
149
  - `LimitReached` (429) carries `scope: 'tenant' | 'user'`; nothing is sent to Rekhu.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rekhu/core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Rekhu Core for Node.js: Rekhu's knowers in your app (Seshat for print files, Rekhu Machines for printing machines), with your own tenants, languages, limits and a usage ledger per customer.",
5
5
  "keywords": ["ai", "print", "printing", "preflight", "pdf", "machines", "multi-tenant", "rekhu"],
6
6
  "homepage": "https://rekhu.com",
package/src/index.js CHANGED
@@ -6,10 +6,26 @@
6
6
  // await rekhu.machines({ user }).identify('Heidelberg Speedmaster SM 74-4 with coater');
7
7
  // await rekhu.seshat({ user }).check(pdfBuffer, { name: 'flyer.pdf', order: { width_mm: 148, height_mm: 210 } });
8
8
 
9
+ import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
10
+ import { createWriteStream, existsSync, openAsBlob } from 'node:fs';
11
+ import { Readable } from 'node:stream';
12
+ import { pipeline } from 'node:stream/promises';
9
13
  import { memoryLedger, fileLedger, usage } from './ledger.js';
10
14
 
11
15
  export { memoryLedger, fileLedger, usage };
12
- export const VERSION = '0.1.0';
16
+ export const VERSION = '0.3.0';
17
+
18
+ /**
19
+ * Whether a webhook Rekhu sent is genuine: the x-rekhu-signature header (t=…,v1=…) over the raw body, with the
20
+ * webhook_secret the call answered, and not older than `tolerance` seconds.
21
+ */
22
+ export function verifyWebhook(body, signature, secret, tolerance = 300) {
23
+ const m = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(String(signature || ''));
24
+ if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > tolerance) return false;
25
+ const expected = createHmac('sha256', secret).update(`${m[1]}.${body}`).digest();
26
+ const given = Buffer.from(m[2], 'hex');
27
+ return given.length === expected.length && timingSafeEqual(given, expected);
28
+ }
13
29
 
14
30
  /** A knower could not answer: Rekhu's status (0 when the call was not made) and its error type. */
15
31
  export class RekhuError extends Error {
@@ -21,6 +37,39 @@ export class RekhuError extends Error {
21
37
  }
22
38
  }
23
39
 
40
+ /** The key is missing, revoked, or belongs to another Rekhu platform (401). */
41
+ export class InvalidKey extends RekhuError {}
42
+ /** The knower's module is not switched on for this application (403 module_not_enabled). */
43
+ export class ModuleNotEnabled extends RekhuError {}
44
+ /** The account processes data in Europe only and this call has no engine in the EU (403 region_unavailable). */
45
+ export class RegionUnavailable extends RekhuError {}
46
+ /** The Rekhu account's credit is used up (402). */
47
+ export class CreditExhausted extends RekhuError {}
48
+ /** The application is paused or at its monthly limit in the Rekhu account (402 app_limit). */
49
+ export class AppPaused extends RekhuError {}
50
+ /** A kept file is past its day, removed, or of another account (404). */
51
+ export class FileNotFound extends RekhuError {}
52
+ /** Refused before anything ran, with what was wrong (422); nothing was charged. */
53
+ export class InvalidRequest extends RekhuError {}
54
+ /** Too many calls at once, after the client tried again (429). */
55
+ export class RateLimited extends RekhuError {}
56
+ /** Rekhu or a service behind it could not answer, after the client tried again (502, 503, 529). */
57
+ export class Unavailable extends RekhuError {}
58
+
59
+ /** The typed error for an answer from Rekhu: catch the class you handle, or RekhuError for all of them. */
60
+ export function errorFor(status, type, message) {
61
+ const Type = status === 401 ? InvalidKey : type === 'module_not_enabled' ? ModuleNotEnabled : type === 'region_unavailable' ? RegionUnavailable
62
+ : type === 'app_limit' ? AppPaused : status === 402 ? CreditExhausted : status === 404 ? FileNotFound : status === 422 || status === 400 ? InvalidRequest
63
+ : status === 429 ? RateLimited : status >= 500 ? Unavailable : RekhuError;
64
+ const error = new Type(message, status, type);
65
+ error.name = Type.name;
66
+ return error;
67
+ }
68
+
69
+ const FREE = ['models', 'modules', 'codes', 'upload', 'poll', 'enhance_preview'];
70
+ const RETRY = [429, 502, 503, 529];
71
+ const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
72
+
24
73
  /** One of the app's own limits a day is reached (scope: tenant or user); nothing was sent to Rekhu. */
25
74
  export class LimitReached extends RekhuError {
26
75
  constructor(message, scope) {
@@ -64,7 +113,7 @@ export function createRekhu(options = {}) {
64
113
 
65
114
  function key() {
66
115
  const k = parseKey(config.key);
67
- if (!k) throw new RekhuError('REKHU_APP_KEY is missing or not a Rekhu app key (agk_…). No Rekhu account yet? Create one at https://rekhu.com/register?ref=core-node: it starts with $10 credit, no card needed.', 0, 'configuration_error');
116
+ if (!k) throw new InvalidKey('REKHU_APP_KEY is missing or not a Rekhu app key (agk_…): put the app key from your Rekhu account (Projects → the app → Connection) in the environment.', 0, 'configuration_error');
68
117
  return k;
69
118
  }
70
119
 
@@ -72,7 +121,7 @@ export function createRekhu(options = {}) {
72
121
  if (named) return { id: String(named.id), name: String(named.name ?? named.id) };
73
122
  if (config.tenancy !== 'multi') return { id: config.appId, name: config.appName };
74
123
  const t = resolvers.tenant && user ? await resolvers.tenant(user) : null;
75
- if (t && typeof t === 'object' && t.id !== undefined && t.id !== null && t.id !== '') return { id: String(t.id), name: String(t.name ?? t.id), ...t, id: String(t.id) };
124
+ if (t && typeof t === 'object' && t.id !== undefined && t.id !== null && t.id !== '') return { ...t, id: String(t.id), name: String(t.name ?? t.id) };
76
125
  if (t !== null && t !== undefined && t !== '' && typeof t !== 'object') return { id: String(t), name: String(t) };
77
126
  return null;
78
127
  }
@@ -96,32 +145,55 @@ export function createRekhu(options = {}) {
96
145
 
97
146
  async function failed(response) {
98
147
  const error = await response.json().catch(() => ({}));
99
- return new RekhuError(error?.error?.message || `Rekhu answered ${response.status}`, response.status, error?.error?.type || 'api_error');
148
+ return errorFor(response.status, error?.error?.type || 'api_error', error?.error?.message || `Rekhu answered ${response.status}`);
100
149
  }
101
150
 
102
- /** One call to a knower for a user (or a named tenant): the app's limits, the call, the ledger and onUsage. */
103
- async function call(knower, action, input = {}, { user = null, userId = null, tenant: named = null, file = null } = {}) {
151
+ /**
152
+ * One request, tried again on a dropped connection or a 429, 502, 503 or 529, waiting a little longer each time (and
153
+ * as long as retry-after asks, up to 5 seconds); any other failure is the typed error for it.
154
+ */
155
+ async function send(make) {
156
+ for (let attempt = 1; ; attempt++) {
157
+ let response;
158
+ try {
159
+ response = await make();
160
+ } catch (error) {
161
+ if (error instanceof RekhuError) throw error;
162
+ if (attempt >= 3) throw new Unavailable('Rekhu could not be reached; try again in a moment.', 503, 'connection_error');
163
+ await pause(250 * 2 ** attempt);
164
+ continue;
165
+ }
166
+ if (response.ok) return response;
167
+ if (attempt < 3 && RETRY.includes(response.status)) {
168
+ await pause(Math.min(5000, Number(response.headers?.get?.('retry-after')) * 1000 || 250 * 2 ** attempt));
169
+ continue;
170
+ }
171
+ throw await failed(response);
172
+ }
173
+ }
174
+
175
+ async function headersFor(user, userId, named) {
104
176
  const k = key();
105
- if (!config.knowers.includes(knower)) throw new RekhuError(`The knower ${knower} is not installed in this app: add it to knowers (or REKHU_KNOWERS).`, 0, 'configuration_error');
106
177
  const tenant = await tenantOf(user, named);
107
- if (config.tenancy === 'multi' && !tenant && action !== 'models') throw new RekhuError('This app is multi-tenant, and no tenant was found for the user: give createRekhu a tenant function, or pass a tenant.', 0, 'configuration_error');
108
178
  const who = userId ?? (user ? (options.userId ? options.userId(user) : user.id) : null);
109
- if (action !== 'models') await checkLimits(tenant, who, knower);
179
+ return { tenant, who, headers: { accept: 'application/json', 'x-api-key': k.key, 'x-agent': k.agent, 'x-tenant': String(tenant?.id ?? config.appId), 'x-tenant-name': String(tenant?.name ?? config.appName), ...(who != null ? { 'x-user': String(who) } : {}) } };
180
+ }
181
+
182
+ /** One call to a knower for a user (or a named tenant): the app's limits, the call, the ledger and onUsage. */
183
+ async function call(knower, action, input = {}, { user = null, userId = null, tenant: named = null, file = null } = {}) {
184
+ if (config.knowers.length && !config.knowers.includes(knower)) throw new RekhuError(`The knower ${knower} is not in this app's knowers (REKHU_KNOWERS): add it, or leave the list empty to use every knower the key allows.`, 0, 'configuration_error');
185
+ const { tenant, who, headers } = await headersFor(user, userId, named);
186
+ if (config.tenancy === 'multi' && !tenant && !FREE.includes(action)) throw new RekhuError('This app is multi-tenant, and no tenant was found for the user: give createRekhu a tenant function, or pass a tenant.', 0, 'configuration_error');
187
+ if (!FREE.includes(action)) await checkLimits(tenant, who, knower);
110
188
  const body = ['check', 'explain'].includes(action) && input.language === undefined ? { ...input, language: await localeOf(user) } : input;
111
- const headers = { accept: 'application/json', 'x-api-key': k.key, 'x-agent': k.agent, 'x-tenant': String(tenant?.id ?? config.appId), 'x-tenant-name': String(tenant?.name ?? config.appName), ...(who != null ? { 'x-user': String(who) } : {}) };
189
+ if (!FREE.includes(action)) headers['idempotency-key'] = randomUUID();
112
190
  const started = Date.now();
113
- let response;
114
- try {
115
- response = file
116
- ? await config.fetch(`${platform()}/v1/knowers/${knower}/${action}`, { method: 'POST', headers: { ...headers, 'content-type': 'application/octet-stream', 'x-file-name': file.name, 'x-knower-input': JSON.stringify(body) }, body: file.bytes, signal: AbortSignal.timeout(config.timeout) })
117
- : await config.fetch(`${platform()}/v1/knowers/${knower}/${action}`, { method: 'POST', headers: { ...headers, 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(config.timeout) });
118
- } catch (error) {
119
- if (error instanceof RekhuError) throw error;
120
- throw new RekhuError('Rekhu could not be reached; try again in a moment.', 503, 'connection_error');
121
- }
122
- if (!response.ok) throw await failed(response);
191
+ const url = `${platform()}/v1/knowers/${knower}/${action}`;
192
+ const response = await send(async () => (file
193
+ ? config.fetch(url, { method: 'POST', headers: { ...headers, 'content-type': 'application/octet-stream', 'x-file-name': encodeURIComponent(file.name), 'x-knower-input': JSON.stringify({ ...body, name: file.name }) }, body: file.path ? await openAsBlob(file.path) : file.bytes, signal: AbortSignal.timeout(config.timeout) })
194
+ : config.fetch(url, { method: 'POST', headers: { ...headers, 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(config.timeout) })));
123
195
  const answer = await response.json();
124
- if (action === 'models') return answer;
196
+ if (FREE.includes(action) || answer.meta?.replayed) return answer;
125
197
  const meta = answer.meta || {};
126
198
  const row = {
127
199
  tenant_id: tenant?.id ?? null, tenant_name: tenant?.name ?? null, user_id: who != null ? String(who) : null, knower, action, ref: answer.ref ?? null,
@@ -133,21 +205,33 @@ export function createRekhu(options = {}) {
133
205
  return answer;
134
206
  }
135
207
 
136
- async function feedback(knower, ref, outcome, correction = null) {
137
- const k = key();
138
- const response = await config.fetch(`${platform()}/v1/knowers/${knower}/feedback`, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json', 'x-api-key': k.key, 'x-agent': k.agent }, body: JSON.stringify({ ref, outcome, ...(correction ? { correction } : {}) }) });
139
- if (!response.ok) throw await failed(response);
208
+ async function feedback(knower, ref, outcome, correction = null, who = {}) {
209
+ const { headers } = await headersFor(who.user ?? null, who.userId ?? null, who.tenant ?? null);
210
+ const response = await send(() => config.fetch(`${platform()}/v1/knowers/${knower}/feedback`, { method: 'POST', headers: { ...headers, 'content-type': 'application/json' }, body: JSON.stringify({ ref, outcome, ...(correction ? { correction } : {}) }) }));
140
211
  return response.json();
141
212
  }
142
213
 
143
- async function download(knower, fileId) {
144
- const k = key();
145
- const response = await config.fetch(`${platform()}/v1/knowers/${knower}/files/${encodeURIComponent(fileId)}`, { headers: { 'x-api-key': k.key, 'x-agent': k.agent } });
146
- if (!response.ok) throw await failed(response);
147
- return Buffer.from(await response.arrayBuffer());
214
+ async function fileRequest(knower, fileId, method, who = {}) {
215
+ const { headers } = await headersFor(who.user ?? null, who.userId ?? null, who.tenant ?? null);
216
+ return send(() => config.fetch(`${platform()}/v1/knowers/${knower}/files/${encodeURIComponent(fileId)}`, { method, headers, signal: AbortSignal.timeout(config.timeout) }));
148
217
  }
149
218
 
150
- const knowerFor = (name) => (who = {}) => ({ call: (action, input, extra = {}) => call(name, action, input, { ...who, ...extra }), feedback: (ref, outcome, correction) => feedback(name, ref, outcome, correction) });
219
+ async function download(knower, fileId, who = {}) {
220
+ return Buffer.from(await (await fileRequest(knower, fileId, 'GET', who)).arrayBuffer());
221
+ }
222
+
223
+ /** A result written straight to a path, without holding a large file in memory. */
224
+ async function downloadTo(knower, fileId, path, who = {}) {
225
+ const response = await fileRequest(knower, fileId, 'GET', who);
226
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(path));
227
+ return path;
228
+ }
229
+
230
+ async function remove(knower, fileId, who = {}) {
231
+ await fileRequest(knower, fileId, 'DELETE', who);
232
+ }
233
+
234
+ const knowerFor = (name) => (who = {}) => ({ call: (action, input, extra = {}) => call(name, action, input, { ...who, ...extra }), feedback: (ref, outcome, correction) => feedback(name, ref, outcome, correction, who) });
151
235
 
152
236
  /** Rekhu Machines: printing and finishing machines, sent as a Rekhu machine profile (see the README). */
153
237
  const machines = (who = {}) => {
@@ -163,20 +247,50 @@ export function createRekhu(options = {}) {
163
247
  };
164
248
  };
165
249
 
166
- /** Seshat: a print file against the order it is for; fix, preview, download. */
250
+ const bytesOf = async (file) => (file instanceof Uint8Array ? file : file instanceof ArrayBuffer ? new Uint8Array(file) : typeof file?.arrayBuffer === 'function' ? new Uint8Array(await file.arrayBuffer()) : null);
251
+
252
+ /** Seshat: a print file against the order it is for; fix, preview, download. With ai_enhance: one picture improved. */
167
253
  const seshat = (who = {}) => {
168
254
  const k = knowerFor('seshat')(who);
255
+ const file = async (f, name, what) => {
256
+ if (typeof f === 'string' && existsSync(f)) return { name: name || f.split(/[\\/]/).pop(), path: f };
257
+ if (!name) throw new RekhuError(`Give the file's name: ${what}(bytes, { name: 'flyer.pdf' }).`, 0, 'invalid_request_error');
258
+ const bytes = await bytesOf(f);
259
+ if (!bytes) throw new RekhuError('The file is a Buffer, Uint8Array, ArrayBuffer or Blob.', 0, 'invalid_request_error');
260
+ return { name, bytes };
261
+ };
169
262
  return {
170
263
  ...k,
171
- check: async (file, { name, order = {}, explain = false } = {}) => {
172
- if (!name) throw new RekhuError('Give the file\'s name: check(bytes, { name: \'flyer.pdf\', order }).', 0, 'invalid_request_error');
173
- const bytes = file instanceof Uint8Array ? file : file instanceof ArrayBuffer ? new Uint8Array(file) : typeof file?.arrayBuffer === 'function' ? new Uint8Array(await file.arrayBuffer()) : null;
174
- if (!bytes) throw new RekhuError('The file is a Buffer, Uint8Array, ArrayBuffer or Blob.', 0, 'invalid_request_error');
175
- return k.call('check', { order, ...(explain ? { explain: true } : {}) }, { file: { name, bytes } });
264
+ check: async (f, { name, order = {}, explain = false } = {}) => k.call('check', { order, ...(explain ? { explain: true } : {}) }, { file: await file(f, name, 'check') }),
265
+ /** Which modules this application may use (preflight, ai_enhance) and the modes. Not charged. */
266
+ modules: () => k.call('modules', {}),
267
+ /** A file kept for a day (a mask, a background picture): its file_id. Not charged. */
268
+ upload: async (f, { name } = {}) => k.call('upload', {}, { file: await file(f, name, 'upload') }),
269
+ /**
270
+ * ai_enhance: one picture (bytes with a name, or a file_id) through the modes, in order. options: ppi, width_mm,
271
+ * height_mm, factor, all_mm…, background, region; extra: async, webhookUrl, maskFileId, backgroundFileId.
272
+ * A large picture answers { status: 'queued', job_id }: poll() it, or verify the webhook with verifyWebhook().
273
+ */
274
+ enhance: async (picture, modes, options = {}, extra = {}) => {
275
+ const input = {
276
+ mode: [].concat(modes), options,
277
+ ...(extra.async ? { async: true } : {}), ...(extra.webhookUrl ? { webhook_url: extra.webhookUrl } : {}),
278
+ ...(extra.maskFileId ? { mask_file_id: extra.maskFileId } : {}), ...(extra.backgroundFileId ? { background_file_id: extra.backgroundFileId } : {}),
279
+ };
280
+ if (typeof picture === 'string' && /^f_[a-z0-9]{24}$/.test(picture)) return k.call('enhance', { ...input, file_id: picture });
281
+ return k.call('enhance', input, { file: await file(picture, extra.name, 'enhance') });
176
282
  },
283
+ enhancePreview: (fileId, { longest = 900 } = {}) => k.call('enhance_preview', { file_id: fileId, longest }),
284
+ poll: (jobId) => k.call('poll', { job_id: jobId }),
177
285
  fix: (fileId, fixes = 'auto', order = {}) => k.call('fix', { file_id: fileId, fixes, order }),
178
286
  preview: (fileId, { page = 1, longest = 1200 } = {}) => k.call('preview', { file_id: fileId, page, longest }),
179
- download: (fileId) => download('seshat', fileId),
287
+ download: (fileId) => download('seshat', fileId, who),
288
+ /** The same, written straight to a path: for large print files. */
289
+ downloadTo: (fileId, path) => downloadTo('seshat', fileId, path, who),
290
+ /** A kept file removed at once: a customer took it out of their order. */
291
+ remove: (fileId) => remove('seshat', fileId, who),
292
+ /** Every finding code, decision and fix with its options, versioned: the list to map against. Not charged. */
293
+ codes: () => k.call('codes', {}),
180
294
  };
181
295
  };
182
296