@rekhu/core 0.1.0 → 0.2.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 +8 -0
- package/README.md +36 -0
- package/package.json +1 -1
- package/src/index.js +47 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@rekhu/core`. Versions follow [semantic versioning](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 0.2.0 · 6 November 2026
|
|
6
|
+
|
|
7
|
+
- **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`.
|
|
8
|
+
- **ai_enhance:** `seshat().enhance(picture, modes, options, extra)` improves one picture without a check:
|
|
9
|
+
- the modes are upscale to a print size, extend, denoise, sharpen, restore, remove_background, replace_background, remove_object, vectorize and colour_correct;
|
|
10
|
+
- large pictures run as a job: `poll(jobId)`, or a webhook checked with `verifyWebhook()`;
|
|
11
|
+
- `enhancePreview(fileId)` gives a before / after picture, and `upload()` keeps a mask or background picture.
|
|
12
|
+
|
|
5
13
|
## 0.1.0 · 6 November 2026
|
|
6
14
|
|
|
7
15
|
- **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,42 @@ It fixes:
|
|
|
54
54
|
- CMYK colours and embedded fonts;
|
|
55
55
|
- pictures, made into print PDFs.
|
|
56
56
|
|
|
57
|
+
## Seshat AI: one picture improved (ai_enhance)
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
| Mode | What it does | Options |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `upscale` | More pixels, up to 4×: to a resolution at a print size | `ppi`, `width_mm`, `height_mm`, or `factor` |
|
|
64
|
+
| `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`) |
|
|
65
|
+
| `denoise`, `sharpen` | Cleans JPEG artefacts and noise; crisper edges | `strength`, `amount` |
|
|
66
|
+
| `restore` | An old or scanned photo: scratches filled, noise and faded colour corrected | |
|
|
67
|
+
| `remove_background` | Transparent PNG, or a solid colour | `background: '#ffffff'` |
|
|
68
|
+
| `replace_background` | A new background from a picture you send | `background_file_id` (from `upload`) |
|
|
69
|
+
| `remove_object` | Fills an object or blemish from its surroundings | `region: {x, y, w, h}`, or `mask_file_id` |
|
|
70
|
+
| `vectorize` | A logo or line drawing as SVG. A photo is refused (`not_vectorizable`) | |
|
|
71
|
+
| `colour_correct` | Exposure and white balance | |
|
|
72
|
+
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
Limits:
|
|
76
|
+
- at most 40 megapixels in and 120 megapixels out;
|
|
77
|
+
- upscaling at most 4×;
|
|
78
|
+
- extending at most 12 % of a side on CPU;
|
|
79
|
+
- removing at most a quarter of the picture.
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
const s = rekhu.seshat({ tenant: { id: tenant.uuid, name: tenant.name } });
|
|
83
|
+
await s.modules(); // { modules: { preflight: false, ai_enhance: true }, modes: [...] }
|
|
84
|
+
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' });
|
|
85
|
+
const { image } = await s.enhancePreview(r.file_id); // before | after, a data: URL
|
|
86
|
+
const png = await s.download(r.file_id); // a Buffer
|
|
87
|
+
await s.poll('j_…'); // for a job
|
|
88
|
+
|
|
89
|
+
import { verifyWebhook } from '@rekhu/core';
|
|
90
|
+
if (!verifyWebhook(rawBody, req.headers['x-rekhu-signature'], storedSecret)) return res.status(401).end();
|
|
91
|
+
```
|
|
92
|
+
|
|
57
93
|
## Rekhu Machines
|
|
58
94
|
|
|
59
95
|
```js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rekhu/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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,23 @@
|
|
|
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, timingSafeEqual } from 'node:crypto';
|
|
9
10
|
import { memoryLedger, fileLedger, usage } from './ledger.js';
|
|
10
11
|
|
|
11
12
|
export { memoryLedger, fileLedger, usage };
|
|
12
|
-
export const VERSION = '0.
|
|
13
|
+
export const VERSION = '0.2.0';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether a webhook Rekhu sent is genuine: the x-rekhu-signature header (t=…,v1=…) over the raw body, with the
|
|
17
|
+
* webhook_secret the call answered, and not older than `tolerance` seconds.
|
|
18
|
+
*/
|
|
19
|
+
export function verifyWebhook(body, signature, secret, tolerance = 300) {
|
|
20
|
+
const m = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(String(signature || ''));
|
|
21
|
+
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > tolerance) return false;
|
|
22
|
+
const expected = createHmac('sha256', secret).update(`${m[1]}.${body}`).digest();
|
|
23
|
+
const given = Buffer.from(m[2], 'hex');
|
|
24
|
+
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
25
|
+
}
|
|
13
26
|
|
|
14
27
|
/** A knower could not answer: Rekhu's status (0 when the call was not made) and its error type. */
|
|
15
28
|
export class RekhuError extends Error {
|
|
@@ -104,9 +117,9 @@ export function createRekhu(options = {}) {
|
|
|
104
117
|
const k = key();
|
|
105
118
|
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
119
|
const tenant = await tenantOf(user, named);
|
|
107
|
-
if (config.tenancy === 'multi' && !tenant &&
|
|
120
|
+
if (config.tenancy === 'multi' && !tenant && !['models', 'modules'].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');
|
|
108
121
|
const who = userId ?? (user ? (options.userId ? options.userId(user) : user.id) : null);
|
|
109
|
-
if (
|
|
122
|
+
if (!['models', 'modules', 'upload', 'poll', 'enhance_preview'].includes(action)) await checkLimits(tenant, who, knower);
|
|
110
123
|
const body = ['check', 'explain'].includes(action) && input.language === undefined ? { ...input, language: await localeOf(user) } : input;
|
|
111
124
|
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) } : {}) };
|
|
112
125
|
const started = Date.now();
|
|
@@ -121,7 +134,8 @@ export function createRekhu(options = {}) {
|
|
|
121
134
|
}
|
|
122
135
|
if (!response.ok) throw await failed(response);
|
|
123
136
|
const answer = await response.json();
|
|
124
|
-
|
|
137
|
+
// what is not charged is not kept in the ledger
|
|
138
|
+
if (['models', 'modules', 'upload', 'poll', 'enhance_preview'].includes(action)) return answer;
|
|
125
139
|
const meta = answer.meta || {};
|
|
126
140
|
const row = {
|
|
127
141
|
tenant_id: tenant?.id ?? null, tenant_name: tenant?.name ?? null, user_id: who != null ? String(who) : null, knower, action, ref: answer.ref ?? null,
|
|
@@ -163,17 +177,40 @@ export function createRekhu(options = {}) {
|
|
|
163
177
|
};
|
|
164
178
|
};
|
|
165
179
|
|
|
166
|
-
|
|
180
|
+
const bytesOf = async (file) => (file instanceof Uint8Array ? file : file instanceof ArrayBuffer ? new Uint8Array(file) : typeof file?.arrayBuffer === 'function' ? new Uint8Array(await file.arrayBuffer()) : null);
|
|
181
|
+
|
|
182
|
+
/** Seshat: a print file against the order it is for; fix, preview, download. With ai_enhance: one picture improved. */
|
|
167
183
|
const seshat = (who = {}) => {
|
|
168
184
|
const k = knowerFor('seshat')(who);
|
|
185
|
+
const file = async (f, name, what) => {
|
|
186
|
+
if (!name) throw new RekhuError(`Give the file's name: ${what}(bytes, { name: 'flyer.pdf' }).`, 0, 'invalid_request_error');
|
|
187
|
+
const bytes = await bytesOf(f);
|
|
188
|
+
if (!bytes) throw new RekhuError('The file is a Buffer, Uint8Array, ArrayBuffer or Blob.', 0, 'invalid_request_error');
|
|
189
|
+
return { name, bytes };
|
|
190
|
+
};
|
|
169
191
|
return {
|
|
170
192
|
...k,
|
|
171
|
-
check: async (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
check: async (f, { name, order = {}, explain = false } = {}) => k.call('check', { order, ...(explain ? { explain: true } : {}) }, { file: await file(f, name, 'check') }),
|
|
194
|
+
/** Which modules this application may use (preflight, ai_enhance) and the modes. Not charged. */
|
|
195
|
+
modules: () => k.call('modules', {}),
|
|
196
|
+
/** A file kept for a day (a mask, a background picture): its file_id. Not charged. */
|
|
197
|
+
upload: async (f, { name } = {}) => k.call('upload', {}, { file: await file(f, name, 'upload') }),
|
|
198
|
+
/**
|
|
199
|
+
* ai_enhance: one picture (bytes with a name, or a file_id) through the modes, in order. options: ppi, width_mm,
|
|
200
|
+
* height_mm, factor, all_mm…, background, region; extra: async, webhookUrl, maskFileId, backgroundFileId.
|
|
201
|
+
* A large picture answers { status: 'queued', job_id }: poll() it, or verify the webhook with verifyWebhook().
|
|
202
|
+
*/
|
|
203
|
+
enhance: async (picture, modes, options = {}, extra = {}) => {
|
|
204
|
+
const input = {
|
|
205
|
+
mode: [].concat(modes), options,
|
|
206
|
+
...(extra.async ? { async: true } : {}), ...(extra.webhookUrl ? { webhook_url: extra.webhookUrl } : {}),
|
|
207
|
+
...(extra.maskFileId ? { mask_file_id: extra.maskFileId } : {}), ...(extra.backgroundFileId ? { background_file_id: extra.backgroundFileId } : {}),
|
|
208
|
+
};
|
|
209
|
+
if (typeof picture === 'string' && /^f_[a-z0-9]{24}$/.test(picture)) return k.call('enhance', { ...input, file_id: picture });
|
|
210
|
+
return k.call('enhance', input, { file: await file(picture, extra.name, 'enhance') });
|
|
176
211
|
},
|
|
212
|
+
enhancePreview: (fileId, { longest = 900 } = {}) => k.call('enhance_preview', { file_id: fileId, longest }),
|
|
213
|
+
poll: (jobId) => k.call('poll', { job_id: jobId }),
|
|
177
214
|
fix: (fileId, fixes = 'auto', order = {}) => k.call('fix', { file_id: fileId, fixes, order }),
|
|
178
215
|
preview: (fileId, { page = 1, longest = 1200 } = {}) => k.call('preview', { file_id: fileId, page, longest }),
|
|
179
216
|
download: (fileId) => download('seshat', fileId),
|