@upvoiced/sdk 0.0.0-stage → 1.32.1

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/dist/errors.js ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * P2-015 (ADR-0067): every non-2xx answer becomes an UpVoicedError carrying the API's error
3
+ * shape `{ error: { code, message, details? } }` (spec 05). Bodies that are not that shape
4
+ * (a proxy's HTML 502) get `code: "http_<status>"`.
5
+ */
6
+ export class UpVoicedError extends Error {
7
+ name = 'UpVoicedError';
8
+ /** HTTP status. */
9
+ status;
10
+ /** Stable machine-readable code (`insufficient_scope`, `not_found`, `rate_limited`, …). */
11
+ code;
12
+ details;
13
+ method;
14
+ /** Request path without the query string. */
15
+ path;
16
+ /** Seconds from `Retry-After` (429/503), else null. */
17
+ retryAfter;
18
+ /** The Idempotency-Key the request carried; reuse it to retry the same POST safely. */
19
+ idempotencyKey;
20
+ /** The parsed (or raw text) response body. */
21
+ body;
22
+ constructor(init) {
23
+ super(init.message);
24
+ this.status = init.status;
25
+ this.code = init.code;
26
+ this.details = init.details;
27
+ this.method = init.method;
28
+ this.path = init.path;
29
+ this.retryAfter = init.retryAfter;
30
+ this.idempotencyKey = init.idempotencyKey;
31
+ this.body = init.body;
32
+ }
33
+ }
34
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
35
+ const detailsOf = (value) => Array.isArray(value)
36
+ ? value.flatMap((item) => isRecord(item) && typeof item['path'] === 'string' && typeof item['code'] === 'string'
37
+ ? [{ path: item['path'], code: item['code'] }]
38
+ : [])
39
+ : [];
40
+ /** Reads `{ error: { code, message, details? } }`, tolerating anything else. */
41
+ export function errorShape(status, statusText, body) {
42
+ const error = isRecord(body) ? body['error'] : undefined;
43
+ if (isRecord(error) && typeof error['code'] === 'string')
44
+ return {
45
+ code: error['code'],
46
+ message: typeof error['message'] === 'string' ? error['message'] : error['code'],
47
+ details: detailsOf(error['details']),
48
+ };
49
+ return {
50
+ code: `http_${String(status)}`,
51
+ message: statusText || `HTTP ${String(status)}`,
52
+ details: [],
53
+ };
54
+ }
55
+ /** `Retry-After` as seconds: delta-seconds or an HTTP date; null when absent or invalid. */
56
+ export function parseRetryAfter(value, now = Date.now()) {
57
+ if (value === null || value.trim() === '')
58
+ return null;
59
+ const trimmed = value.trim();
60
+ if (/^\d+$/.test(trimmed))
61
+ return Number(trimmed);
62
+ const at = Date.parse(trimmed);
63
+ return Number.isNaN(at) ? null : Math.max(0, Math.ceil((at - now) / 1000));
64
+ }
@@ -0,0 +1,11 @@
1
+ /** Binary consumer only. Callers bind URL to their configured API and exact resource ID;
2
+ * this utility never derives authority from a response URL or retries a request. */
3
+ export declare function downloadExportBytes(options: {
4
+ readonly url: string;
5
+ readonly headers: HeadersInit;
6
+ readonly bytes: number;
7
+ readonly mime: 'text/csv' | 'application/json' | 'application/zip' | 'image/png' | 'image/jpeg' | 'image/webp' | 'image/svg+xml';
8
+ readonly fetch?: (url: string, init: RequestInit) => Promise<Response>;
9
+ readonly assertCurrent?: () => void;
10
+ readonly signal?: AbortSignal;
11
+ }): Promise<ArrayBuffer>;
@@ -0,0 +1,106 @@
1
+ /** Binary consumer only. Callers bind URL to their configured API and exact resource ID;
2
+ * this utility never derives authority from a response URL or retries a request. */
3
+ export async function downloadExportBytes(options) {
4
+ const refused = () => new Error('Binary download is unavailable.');
5
+ const bytes = options.bytes, mime = options.mime, url = options.url, headers = new Headers(options.headers), fetcher = options.fetch ?? fetch, assertCurrent = options.assertCurrent, callerSignal = options.signal;
6
+ if (!Number.isSafeInteger(bytes) ||
7
+ bytes < 1 ||
8
+ bytes > 209715200 ||
9
+ ![
10
+ 'text/csv',
11
+ 'application/json',
12
+ 'application/zip',
13
+ 'image/png',
14
+ 'image/jpeg',
15
+ 'image/webp',
16
+ 'image/svg+xml',
17
+ ].includes(mime))
18
+ throw refused();
19
+ const controller = new AbortController(), signal = callerSignal ? AbortSignal.any([controller.signal, callerSignal]) : controller.signal;
20
+ const deadline = performance.now() + 120000;
21
+ let reader, output;
22
+ const timer = setTimeout(() => {
23
+ controller.abort();
24
+ void reader?.cancel().catch(() => { });
25
+ }, 120000);
26
+ const check = () => {
27
+ if (signal.aborted || performance.now() >= deadline)
28
+ throw refused();
29
+ assertCurrent?.();
30
+ };
31
+ const bounded = (promise) => new Promise((resolve, reject) => {
32
+ const abort = () => {
33
+ reject(refused());
34
+ };
35
+ if (signal.aborted) {
36
+ void promise.catch(() => { });
37
+ reject(refused());
38
+ return;
39
+ }
40
+ signal.addEventListener('abort', abort, { once: true });
41
+ void promise.then((value) => {
42
+ signal.removeEventListener('abort', abort);
43
+ resolve(value);
44
+ }, () => {
45
+ signal.removeEventListener('abort', abort);
46
+ reject(refused());
47
+ });
48
+ });
49
+ try {
50
+ check();
51
+ const pending = fetcher(url, {
52
+ method: 'GET',
53
+ headers,
54
+ credentials: 'omit',
55
+ cache: 'no-store',
56
+ redirect: 'error',
57
+ signal,
58
+ });
59
+ void pending.then((response) => {
60
+ if (signal.aborted)
61
+ void response.body?.cancel().catch(() => { });
62
+ }, () => { });
63
+ const response = await bounded(pending);
64
+ reader = response.body?.getReader();
65
+ check();
66
+ if (response.status !== 200 ||
67
+ response.headers.get('content-type') !== mime ||
68
+ response.headers.get('content-length') !== String(bytes) ||
69
+ !reader)
70
+ throw refused();
71
+ output = new Uint8Array(bytes);
72
+ let count = 0;
73
+ for (;;) {
74
+ check();
75
+ const next = await bounded(reader.read());
76
+ check();
77
+ if (next.done)
78
+ break;
79
+ const raw = next.value;
80
+ // Native byte streams can cross a browser/Worker realm. Require a real typed
81
+ // byte view rather than relying on this realm's constructor identity.
82
+ if (!ArrayBuffer.isView(raw) ||
83
+ Object.prototype.toString.call(raw) !== '[object Uint8Array]' ||
84
+ count + raw.byteLength > bytes)
85
+ throw refused();
86
+ const chunk = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
87
+ output.set(chunk, count);
88
+ count += chunk.byteLength;
89
+ }
90
+ if (count !== bytes)
91
+ throw refused();
92
+ check();
93
+ const result = output.buffer;
94
+ output = undefined;
95
+ return result;
96
+ }
97
+ catch {
98
+ throw refused();
99
+ }
100
+ finally {
101
+ clearTimeout(timer);
102
+ controller.abort();
103
+ void reader?.cancel().catch(() => { });
104
+ output?.fill(0);
105
+ }
106
+ }