@evumi/berth 0.0.0-stage → 0.0.3

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.
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createHandler = createHandler;
4
+ exports.toNodeHandler = toNodeHandler;
5
+ const body_js_1 = require("./body.js");
6
+ const signature_js_1 = require("./signature.js");
7
+ const util_js_1 = require("./util.js");
8
+ function snapshotEnv() {
9
+ const g = globalThis;
10
+ return { ...(g.process?.env ?? {}) };
11
+ }
12
+ const json = (status, v) => new Response(JSON.stringify(v), { status, headers: { 'content-type': 'application/json' } });
13
+ const text = (status, s) => new Response(s, { status, headers: { 'content-type': 'text/plain; charset=utf-8' } });
14
+ function makeBatch(raw) {
15
+ const settle = { acks: [], retries: [] };
16
+ const decided = new Set();
17
+ const delay = (o) => (o?.delaySeconds === undefined ? {} : { delaySeconds: o.delaySeconds });
18
+ const list = Array.isArray(raw.messages) ? raw.messages : [];
19
+ const messages = list.map((m) => {
20
+ const id = String(m.id);
21
+ const contentType = (m.contentType || 'json');
22
+ return {
23
+ id,
24
+ timestamp: new Date(Number(m.timestamp)),
25
+ body: (0, body_js_1.decodeBody)(m.body, contentType),
26
+ contentType,
27
+ attempts: Number(m.attempts ?? 1),
28
+ // The first call on a message wins, as in Workers.
29
+ ack() {
30
+ if (decided.has(id))
31
+ return;
32
+ decided.add(id);
33
+ settle.acks.push(id);
34
+ },
35
+ retry(o) {
36
+ if (decided.has(id))
37
+ return;
38
+ decided.add(id);
39
+ settle.retries.push({ id, ...delay(o) });
40
+ },
41
+ };
42
+ });
43
+ const batch = {
44
+ queue: String(raw.queue ?? ''),
45
+ messages,
46
+ ackAll() {
47
+ settle.ackAll = true;
48
+ delete settle.retryAll;
49
+ },
50
+ retryAll(o) {
51
+ settle.retryAll = delay(o);
52
+ delete settle.ackAll;
53
+ },
54
+ };
55
+ return { batch, settle };
56
+ }
57
+ /**
58
+ * A Fetch API handler for everything the platform pushes: queue batches, alarms and Cron HTTP
59
+ * targets, told apart by Berth-Source. It verifies Berth-Signature (401 when it fails), calls the
60
+ * matching handler and answers the platform: 200 with the batch settlement, 204 for an alarm or
61
+ * cron call, 500 when the handler throws (the platform retries), 404 when there is no handler.
62
+ */
63
+ function createHandler(handlers) {
64
+ return async (request) => {
65
+ const secret = handlers.secret ?? (0, util_js_1.env)('BERTH_SIGNING_SECRET') ?? '';
66
+ const raw = new Uint8Array(await request.arrayBuffer());
67
+ if (!secret)
68
+ return text(401, 'BERTH_SIGNING_SECRET is not set');
69
+ const bad = (0, signature_js_1.checkSignature)(secret, request.headers.get('Berth-Signature'), raw, { toleranceSeconds: handlers.toleranceSeconds });
70
+ if (bad)
71
+ return text(401, `invalid signature: ${bad}`);
72
+ let payload;
73
+ try {
74
+ payload = JSON.parse(new TextDecoder().decode(raw)) ?? {};
75
+ }
76
+ catch {
77
+ return text(400, 'body is not JSON');
78
+ }
79
+ const env = snapshotEnv();
80
+ const pending = [];
81
+ const ctx = { waitUntil: (p) => void pending.push(Promise.resolve(p).catch(() => { })) };
82
+ const source = request.headers.get('Berth-Source');
83
+ try {
84
+ switch (source) {
85
+ case 'queue': {
86
+ if (!handlers.queue)
87
+ return text(404, 'no queue handler');
88
+ const { batch, settle } = makeBatch(payload);
89
+ await handlers.queue(batch, env, ctx);
90
+ await Promise.all(pending);
91
+ return json(200, settle);
92
+ }
93
+ case 'alarm': {
94
+ if (!handlers.alarm)
95
+ return text(404, 'no alarm handler');
96
+ await handlers.alarm({
97
+ key: String(payload.key ?? ''),
98
+ scheduledTime: Number(payload.scheduledTime),
99
+ data: payload.data,
100
+ retryCount: Number(payload.retryCount ?? 0),
101
+ isRetry: Boolean(payload.isRetry),
102
+ }, env, ctx);
103
+ break;
104
+ }
105
+ case 'cron': {
106
+ if (!handlers.scheduled)
107
+ return text(404, 'no scheduled handler');
108
+ await handlers.scheduled({ cron: String(payload.cron ?? ''), scheduledTime: Number(payload.scheduledTime), name: String(payload.name ?? ''), noRetry() { } }, env, ctx);
109
+ break;
110
+ }
111
+ default:
112
+ return text(404, `unknown Berth-Source ${JSON.stringify(source)}`);
113
+ }
114
+ await Promise.all(pending);
115
+ return new Response(null, { status: 204 });
116
+ }
117
+ catch (e) {
118
+ await Promise.all(pending);
119
+ return text(500, `handler failed: ${e?.message ?? e}`);
120
+ }
121
+ };
122
+ }
123
+ async function readNodeBody(req) {
124
+ if (req.body instanceof Uint8Array)
125
+ return new Uint8Array(req.body); // Buffer, e.g. express.raw()
126
+ if (typeof req.body === 'string')
127
+ return new TextEncoder().encode(req.body);
128
+ if (req.readableEnded) {
129
+ throw new Error('the request body was already read by a parser; mount this handler before express.json() or use express.raw()');
130
+ }
131
+ const chunks = [];
132
+ for await (const c of req)
133
+ chunks.push(typeof c === 'string' ? new TextEncoder().encode(c) : c);
134
+ let len = 0;
135
+ for (const c of chunks)
136
+ len += c.length;
137
+ const out = new Uint8Array(len);
138
+ let off = 0;
139
+ for (const c of chunks) {
140
+ out.set(c, off);
141
+ off += c.length;
142
+ }
143
+ return out;
144
+ }
145
+ /** Adapts a Fetch handler to node:http / Express: `app.post('/berth', toNodeHandler(handler))`. */
146
+ function toNodeHandler(handler) {
147
+ return async (req, res) => {
148
+ let response;
149
+ try {
150
+ const body = await readNodeBody(req);
151
+ const headers = new Headers();
152
+ for (const [k, v] of Object.entries(req.headers)) {
153
+ if (v === undefined)
154
+ continue;
155
+ headers.set(k, Array.isArray(v) ? v.join(', ') : v);
156
+ }
157
+ const host = headers.get('host') ?? 'localhost';
158
+ const method = req.method ?? 'POST';
159
+ response = await handler(new Request(new URL(req.url ?? '/', `http://${host}`), { method, headers, body: method === 'GET' || method === 'HEAD' ? undefined : body }));
160
+ }
161
+ catch (e) {
162
+ response = text(400, e?.message ?? String(e));
163
+ }
164
+ res.statusCode = response.status;
165
+ response.headers.forEach((v, k) => void res.setHeader(k, v));
166
+ res.end(new Uint8Array(await response.arrayBuffer()));
167
+ };
168
+ }
@@ -0,0 +1,4 @@
1
+ export { BerthError, type ErrorCode } from './errors.js';
2
+ export { type AckRequest, type AckResult, type Alarm, type BatchMessage, type Client, type ClientOptions, type ContentType, type FetchLike, type PulledMessage, type PullOptions, type Queue, type SendOptions, type SendResult, alarm, createClient, queue, } from './client.js';
3
+ export { type AlarmInfo, type Env, type ExecutionContext, type FetchHandler, type Handlers, type Message, type MessageBatch, type NodeRequest, type NodeResponse, type ScheduledController, createHandler, toNodeHandler, } from './handler.js';
4
+ export { type RawBody, type SignatureError, type VerifyOptions, checkSignature, verifySignature } from './signature.js';
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifySignature = exports.checkSignature = exports.toNodeHandler = exports.createHandler = exports.queue = exports.createClient = exports.alarm = exports.BerthError = void 0;
4
+ var errors_js_1 = require("./errors.js");
5
+ Object.defineProperty(exports, "BerthError", { enumerable: true, get: function () { return errors_js_1.BerthError; } });
6
+ var client_js_1 = require("./client.js");
7
+ Object.defineProperty(exports, "alarm", { enumerable: true, get: function () { return client_js_1.alarm; } });
8
+ Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_js_1.createClient; } });
9
+ Object.defineProperty(exports, "queue", { enumerable: true, get: function () { return client_js_1.queue; } });
10
+ var handler_js_1 = require("./handler.js");
11
+ Object.defineProperty(exports, "createHandler", { enumerable: true, get: function () { return handler_js_1.createHandler; } });
12
+ Object.defineProperty(exports, "toNodeHandler", { enumerable: true, get: function () { return handler_js_1.toNodeHandler; } });
13
+ var signature_js_1 = require("./signature.js");
14
+ Object.defineProperty(exports, "checkSignature", { enumerable: true, get: function () { return signature_js_1.checkSignature; } });
15
+ Object.defineProperty(exports, "verifySignature", { enumerable: true, get: function () { return signature_js_1.verifySignature; } });
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,5 @@
1
+ /** SHA-256 of the concatenation of parts. */
2
+ export declare function sha256(...parts: Uint8Array[]): Uint8Array;
3
+ /** HMAC-SHA256(key, concatenation of parts). */
4
+ export declare function hmacSha256(key: Uint8Array, ...parts: Uint8Array[]): Uint8Array;
5
+ export declare function toHex(b: Uint8Array): string;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ // Synchronous HMAC-SHA256 in plain JS. verifySignature has to be synchronous and run in edge
3
+ // runtimes too, where node:crypto is absent and WebCrypto is async only. Bodies are at most
4
+ // 256 KiB, so speed does not matter much. Checked against node:crypto in the tests.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.sha256 = sha256;
7
+ exports.hmacSha256 = hmacSha256;
8
+ exports.toHex = toHex;
9
+ const K = new Uint32Array([
10
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
11
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
12
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
13
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
14
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
15
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
16
+ ]);
17
+ /** SHA-256 of the concatenation of parts. */
18
+ function sha256(...parts) {
19
+ let len = 0;
20
+ for (const p of parts)
21
+ len += p.length;
22
+ // Message + 0x80 + zero padding + 64-bit big-endian bit length, a multiple of 64 bytes.
23
+ const total = Math.ceil((len + 9) / 64) * 64;
24
+ const buf = new Uint8Array(total);
25
+ let off = 0;
26
+ for (const p of parts) {
27
+ buf.set(p, off);
28
+ off += p.length;
29
+ }
30
+ buf[len] = 0x80;
31
+ const view = new DataView(buf.buffer);
32
+ const bits = len * 8;
33
+ view.setUint32(total - 8, Math.floor(bits / 0x100000000));
34
+ view.setUint32(total - 4, bits >>> 0);
35
+ const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
36
+ const w = new Uint32Array(64);
37
+ for (let i = 0; i < total; i += 64) {
38
+ for (let t = 0; t < 16; t++)
39
+ w[t] = view.getUint32(i + t * 4);
40
+ for (let t = 16; t < 64; t++) {
41
+ const a = w[t - 15];
42
+ const b = w[t - 2];
43
+ const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
44
+ const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
45
+ w[t] = (w[t - 16] + s0 + w[t - 7] + s1) | 0;
46
+ }
47
+ let a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];
48
+ for (let t = 0; t < 64; t++) {
49
+ const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
50
+ const ch = (e & f) ^ (~e & g);
51
+ const t1 = (hh + S1 + ch + K[t] + w[t]) | 0;
52
+ const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
53
+ const maj = (a & b) ^ (a & c) ^ (b & c);
54
+ const t2 = (S0 + maj) | 0;
55
+ hh = g;
56
+ g = f;
57
+ f = e;
58
+ e = (d + t1) | 0;
59
+ d = c;
60
+ c = b;
61
+ b = a;
62
+ a = (t1 + t2) | 0;
63
+ }
64
+ h[0] = (h[0] + a) | 0;
65
+ h[1] = (h[1] + b) | 0;
66
+ h[2] = (h[2] + c) | 0;
67
+ h[3] = (h[3] + d) | 0;
68
+ h[4] = (h[4] + e) | 0;
69
+ h[5] = (h[5] + f) | 0;
70
+ h[6] = (h[6] + g) | 0;
71
+ h[7] = (h[7] + hh) | 0;
72
+ }
73
+ const out = new Uint8Array(32);
74
+ const ov = new DataView(out.buffer);
75
+ for (let i = 0; i < 8; i++)
76
+ ov.setUint32(i * 4, h[i]);
77
+ return out;
78
+ }
79
+ /** HMAC-SHA256(key, concatenation of parts). */
80
+ function hmacSha256(key, ...parts) {
81
+ const k = key.length > 64 ? sha256(key) : key;
82
+ const block = new Uint8Array(64);
83
+ block.set(k);
84
+ const ipad = new Uint8Array(64);
85
+ const opad = new Uint8Array(64);
86
+ for (let i = 0; i < 64; i++) {
87
+ ipad[i] = block[i] ^ 0x36;
88
+ opad[i] = block[i] ^ 0x5c;
89
+ }
90
+ return sha256(opad, sha256(ipad, ...parts));
91
+ }
92
+ const HEX = '0123456789abcdef';
93
+ function toHex(b) {
94
+ let s = '';
95
+ for (const x of b)
96
+ s += HEX[x >> 4] + HEX[x & 15];
97
+ return s;
98
+ }
@@ -0,0 +1,20 @@
1
+ /** Why a signature was rejected. */
2
+ export type SignatureError = 'missing' | 'malformed' | 'expired' | 'mismatch';
3
+ export interface VerifyOptions {
4
+ /** The receiver's clock: a Date or unix seconds. Default: now. */
5
+ now?: Date | number;
6
+ /** How far the signature's timestamp may be from now, both ways. Default 300. */
7
+ toleranceSeconds?: number;
8
+ }
9
+ export type RawBody = string | Uint8Array | ArrayBuffer;
10
+ /**
11
+ * Checks a Berth-Signature header (`t=<unix>,v1=<hex>`) against the raw request body and says
12
+ * why it fails, or null when it is valid. Synchronous; never throws. Several v1 values are
13
+ * accepted (one must match), so the secret can be rotated; unknown keys are ignored.
14
+ */
15
+ export declare function checkSignature(secret: string | Uint8Array, header: string | null | undefined, rawBody: RawBody, opts?: VerifyOptions): SignatureError | null;
16
+ /**
17
+ * Whether a Berth-Signature header is valid for the raw request body. Verify the bytes as they
18
+ * arrived, before any JSON parsing. Synchronous; never throws.
19
+ */
20
+ export declare function verifySignature(secret: string | Uint8Array, header: string | null | undefined, rawBody: RawBody, opts?: VerifyOptions): boolean;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkSignature = checkSignature;
4
+ exports.verifySignature = verifySignature;
5
+ const sha256_js_1 = require("./sha256.js");
6
+ const utf8 = new TextEncoder();
7
+ const bytes = (v) => (typeof v === 'string' ? utf8.encode(v) : v instanceof Uint8Array ? v : new Uint8Array(v));
8
+ const INT64_MAX = (1n << 63n) - 1n;
9
+ const INT64_MIN = -(1n << 63n);
10
+ /**
11
+ * Checks a Berth-Signature header (`t=<unix>,v1=<hex>`) against the raw request body and says
12
+ * why it fails, or null when it is valid. Synchronous; never throws. Several v1 values are
13
+ * accepted (one must match), so the secret can be rotated; unknown keys are ignored.
14
+ */
15
+ function checkSignature(secret, header, rawBody, opts = {}) {
16
+ try {
17
+ if (typeof header !== 'string' || header.trim() === '')
18
+ return 'missing';
19
+ let t;
20
+ const sigs = [];
21
+ for (const part of header.split(',')) {
22
+ const p = part.trim();
23
+ const i = p.indexOf('=');
24
+ if (i < 0)
25
+ return 'malformed';
26
+ const k = p.slice(0, i);
27
+ const v = p.slice(i + 1);
28
+ if (k === 't') {
29
+ if (t !== undefined || !/^[+-]?[0-9]+$/.test(v))
30
+ return 'malformed';
31
+ t = BigInt(v);
32
+ if (t > INT64_MAX || t < INT64_MIN)
33
+ return 'malformed';
34
+ }
35
+ else if (k === 'v1') {
36
+ sigs.push(v);
37
+ }
38
+ }
39
+ if (t === undefined || sigs.length === 0)
40
+ return 'malformed';
41
+ const nowOpt = opts.now ?? Date.now() / 1000;
42
+ const now = BigInt(Math.floor(nowOpt instanceof Date ? nowOpt.getTime() / 1000 : nowOpt));
43
+ const tolerance = BigInt(Math.floor(opts.toleranceSeconds ?? 300));
44
+ const d = now - t;
45
+ if ((d < 0n ? -d : d) > tolerance)
46
+ return 'expired';
47
+ const key = typeof secret === 'string' ? utf8.encode(secret) : secret;
48
+ const want = (0, sha256_js_1.toHex)((0, sha256_js_1.hmacSha256)(key, utf8.encode(t.toString() + '.'), bytes(rawBody)));
49
+ let ok = false;
50
+ for (const s of sigs)
51
+ if (equal(s.toLowerCase(), want))
52
+ ok = true;
53
+ return ok ? null : 'mismatch';
54
+ }
55
+ catch {
56
+ return 'malformed';
57
+ }
58
+ }
59
+ /** Constant time for equal lengths; the length of a hex HMAC is public. */
60
+ function equal(a, b) {
61
+ if (a.length !== b.length)
62
+ return false;
63
+ let diff = 0;
64
+ for (let i = 0; i < a.length; i++)
65
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
66
+ return diff === 0;
67
+ }
68
+ /**
69
+ * Whether a Berth-Signature header is valid for the raw request body. Verify the bytes as they
70
+ * arrived, before any JSON parsing. Synchronous; never throws.
71
+ */
72
+ function verifySignature(secret, header, rawBody, opts) {
73
+ return checkSignature(secret, header, rawBody, opts) === null;
74
+ }
@@ -0,0 +1,3 @@
1
+ export declare function env(name: string): string | undefined;
2
+ export declare function toBase64(b: Uint8Array): string;
3
+ export declare function fromBase64(s: string): Uint8Array;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.env = env;
4
+ exports.toBase64 = toBase64;
5
+ exports.fromBase64 = fromBase64;
6
+ function env(name) {
7
+ const g = globalThis;
8
+ try {
9
+ return g.process?.env?.[name] ?? g.Deno?.env.get(name) ?? undefined;
10
+ }
11
+ catch {
12
+ return undefined; // Deno without --allow-env
13
+ }
14
+ }
15
+ function toBase64(b) {
16
+ let s = '';
17
+ for (let i = 0; i < b.length; i += 0x8000)
18
+ s += String.fromCharCode(...b.subarray(i, i + 0x8000));
19
+ return btoa(s);
20
+ }
21
+ function fromBase64(s) {
22
+ const bin = atob(s);
23
+ const out = new Uint8Array(bin.length);
24
+ for (let i = 0; i < bin.length; i++)
25
+ out[i] = bin.charCodeAt(i);
26
+ return out;
27
+ }
@@ -0,0 +1,9 @@
1
+ /** How a message body travels: a JSON value, a string, or bytes (base64 on the wire). */
2
+ export type ContentType = 'json' | 'text' | 'bytes';
3
+ /** body + content_type for the wire. Bytes bodies are sent as bytes whatever contentType says. */
4
+ export declare function encodeBody(body: unknown, contentType?: ContentType): {
5
+ body: unknown;
6
+ content_type?: ContentType;
7
+ };
8
+ /** The body as the app sees it: JSON value, string, or Uint8Array. */
9
+ export declare function decodeBody(body: unknown, contentType: string | undefined): unknown;
@@ -0,0 +1,31 @@
1
+ import { BerthError } from './errors.js';
2
+ import { fromBase64, toBase64 } from './util.js';
3
+ const isBytes = (v) => v instanceof Uint8Array || v instanceof ArrayBuffer;
4
+ /** body + content_type for the wire. Bytes bodies are sent as bytes whatever contentType says. */
5
+ export function encodeBody(body, contentType) {
6
+ if (isBytes(body))
7
+ return { body: toBase64(body instanceof Uint8Array ? body : new Uint8Array(body)), content_type: 'bytes' };
8
+ if (body === undefined)
9
+ throw new BerthError('bad_request', 'body is required');
10
+ switch (contentType) {
11
+ case undefined:
12
+ case 'json':
13
+ return contentType ? { body, content_type: 'json' } : { body };
14
+ case 'text':
15
+ if (typeof body !== 'string')
16
+ throw new BerthError('bad_request', 'contentType "text" needs a string body');
17
+ return { body, content_type: 'text' };
18
+ case 'bytes':
19
+ throw new BerthError('bad_request', 'contentType "bytes" needs a Uint8Array or ArrayBuffer body');
20
+ default:
21
+ throw new BerthError('bad_request', `unsupported contentType ${JSON.stringify(contentType)}`);
22
+ }
23
+ }
24
+ /** The body as the app sees it: JSON value, string, or Uint8Array. */
25
+ export function decodeBody(body, contentType) {
26
+ if (contentType !== 'bytes')
27
+ return body;
28
+ if (typeof body !== 'string')
29
+ throw new BerthError('invalid_response', 'bytes body is not a base64 string');
30
+ return fromBase64(body);
31
+ }
@@ -0,0 +1,111 @@
1
+ import { type ContentType } from './body.js';
2
+ export type { ContentType } from './body.js';
3
+ export interface SendOptions {
4
+ /** json (default; any JSON value), text (a string) or bytes (Uint8Array/ArrayBuffer). Bytes bodies pick bytes themselves. */
5
+ contentType?: ContentType;
6
+ /** Delay before the first delivery, 0–604800. */
7
+ delaySeconds?: number;
8
+ /** The same dedupId on the same queue within 24 h stores nothing and returns the first id. */
9
+ dedupId?: string;
10
+ }
11
+ export interface SendResult {
12
+ id: string;
13
+ /** The dedupId was seen within 24 h: id is the first message's and nothing was stored. */
14
+ deduplicated: boolean;
15
+ }
16
+ export interface BatchMessage extends SendOptions {
17
+ body: unknown;
18
+ }
19
+ export interface PullOptions {
20
+ /** 1–100, default 10. */
21
+ batchSize?: number;
22
+ /** A message not acknowledged within this is handed out again with a new leaseId. 1000–43200000, default 30000. */
23
+ visibilityTimeoutMs?: number;
24
+ }
25
+ export interface PulledMessage<T = unknown> {
26
+ id: string;
27
+ /** Pass it to ack(). */
28
+ leaseId: string;
29
+ /** JSON value (json), string (text) or Uint8Array (bytes). */
30
+ body: T;
31
+ contentType: ContentType;
32
+ /** Starts at 1. */
33
+ attempts: number;
34
+ /** Send time, Unix milliseconds. */
35
+ timestamp: number;
36
+ }
37
+ export interface AckRequest {
38
+ acks?: string[];
39
+ /** Without delaySeconds the queue's backoff applies. */
40
+ retries?: {
41
+ leaseId: string;
42
+ delaySeconds?: number;
43
+ }[];
44
+ }
45
+ export interface AckResult {
46
+ acked: number;
47
+ retried: number;
48
+ /** Out of attempts: moved to the dead-letter list. */
49
+ dead: number;
50
+ }
51
+ /** A queue producer (and pull consumer), like a Workers Queue binding. */
52
+ export interface Queue {
53
+ readonly name: string;
54
+ send(body: unknown, opts?: SendOptions): Promise<SendResult>;
55
+ /** Up to 100 messages; validation is all or nothing. opts.delaySeconds applies to messages without their own. */
56
+ sendBatch(messages: BatchMessage[], opts?: {
57
+ delaySeconds?: number;
58
+ }): Promise<{
59
+ ids: string[];
60
+ items: SendResult[];
61
+ }>;
62
+ /** Pull queues only. Maybe empty. */
63
+ pull<T = unknown>(opts?: PullOptions): Promise<{
64
+ messages: PulledMessage<T>[];
65
+ }>;
66
+ ack(req: AckRequest): Promise<AckResult>;
67
+ }
68
+ /** A named alarm of the environment, like a Durable Object's storage alarm. */
69
+ export interface Alarm {
70
+ readonly key: string;
71
+ /** Sets (or moves) the alarm. scheduledTime: a Date or Unix milliseconds; a past time fires at once. */
72
+ setAlarm(scheduledTime: number | Date, opts?: {
73
+ data?: unknown;
74
+ }): Promise<void>;
75
+ /** The scheduled time in Unix milliseconds, or null when none is pending. */
76
+ getAlarm(): Promise<number | null>;
77
+ /** The pending alarm with its data, or null. */
78
+ get<T = unknown>(): Promise<{
79
+ key: string;
80
+ scheduledTime: number;
81
+ data: T;
82
+ } | null>;
83
+ /** Cancels the alarm; fine when there is none. */
84
+ deleteAlarm(): Promise<void>;
85
+ }
86
+ export type FetchLike = (url: string, init: {
87
+ method: string;
88
+ headers: Record<string, string>;
89
+ body?: string;
90
+ signal: AbortSignal;
91
+ }) => Promise<Response>;
92
+ export interface ClientOptions {
93
+ /** Base URL without /v1. Default: env BERTH_CONFIG_URL, read at each request. */
94
+ url?: string;
95
+ /** Bearer token. Default: env BERTH_CONFIG_TOKEN, read at each request. */
96
+ token?: string;
97
+ /** Per request. Default 10000. */
98
+ timeoutMs?: number;
99
+ /** Custom fetch. Default: globalThis.fetch. */
100
+ fetch?: FetchLike;
101
+ }
102
+ export interface Client {
103
+ queue(name: string): Queue;
104
+ alarm(key: string): Alarm;
105
+ }
106
+ /** A client for queues and alarms. URL and token default to the environment, read lazily. */
107
+ export declare function createClient(opts?: ClientOptions): Client;
108
+ /** A queue of this environment, using BERTH_CONFIG_URL / BERTH_CONFIG_TOKEN (read at each request). */
109
+ export declare const queue: (name: string) => Queue;
110
+ /** A named alarm of this environment, using BERTH_CONFIG_URL / BERTH_CONFIG_TOKEN (read at each request). */
111
+ export declare const alarm: (key: string) => Alarm;