@convert-buddy/cloud-client 0.1.0-beta.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/LICENSE +64 -0
- package/NOTICE +33 -0
- package/README.md +13 -0
- package/dist/chunk-DIBGYTC7.js +372 -0
- package/dist/index.d.ts +1471 -0
- package/dist/index.js +158 -0
- package/dist/processing-DFE8ktWi.d.ts +3204 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +21 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ProcessingClient,
|
|
3
|
+
ProcessingCloudError,
|
|
4
|
+
blobFile,
|
|
5
|
+
createBrowserProcessingClient,
|
|
6
|
+
createWidgetProcessingClient
|
|
7
|
+
} from "./chunk-DIBGYTC7.js";
|
|
8
|
+
|
|
9
|
+
// src/widget.ts
|
|
10
|
+
import {
|
|
11
|
+
parseTargetSchemaV1
|
|
12
|
+
} from "@convert-buddy/importer-contracts";
|
|
13
|
+
var WidgetCloudError = class extends Error {
|
|
14
|
+
constructor(code, message, status, requestId) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.requestId = requestId;
|
|
19
|
+
this.name = "WidgetCloudError";
|
|
20
|
+
}
|
|
21
|
+
code;
|
|
22
|
+
status;
|
|
23
|
+
requestId;
|
|
24
|
+
};
|
|
25
|
+
var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
26
|
+
function importPath(id) {
|
|
27
|
+
if (!uuid.test(id)) throw new TypeError("Invalid import ID");
|
|
28
|
+
return `/v2/widget/imports/${id}`;
|
|
29
|
+
}
|
|
30
|
+
var ConvertBuddyWidgetClient = class {
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.#origin = new URL(options.apiUrl);
|
|
34
|
+
const u = this.#origin;
|
|
35
|
+
if (u.username || u.password || u.search || u.hash || u.pathname !== "/" || u.protocol !== "https:" && !(u.protocol === "http:" && ["localhost", "127.0.0.1"].includes(u.hostname))) throw new TypeError("API URL must be an HTTPS origin or local HTTP origin");
|
|
36
|
+
const publishableRoute = /^cb_pk_([a-f0-9]{64})\.[A-Za-z0-9_-]{43}$/.exec(options.publishableKey);
|
|
37
|
+
const sessionRoute = /^cb_ws_([a-f0-9]{64})\.[A-Za-z0-9_-]{43}$/.exec(options.sessionToken);
|
|
38
|
+
if (!publishableRoute || !sessionRoute || publishableRoute[1] !== sessionRoute[1]) throw new TypeError("A matching v2 publishable key and short-lived widget session are required");
|
|
39
|
+
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
40
|
+
}
|
|
41
|
+
options;
|
|
42
|
+
/** Explicitly retain prepared/corrected records as a downloadable hosted dataset. */
|
|
43
|
+
processing() {
|
|
44
|
+
return createWidgetProcessingClient({ apiUrl: this.options.apiUrl, publishableKey: this.options.publishableKey, sessionToken: () => this.options.sessionToken, fetch: this.options.fetch });
|
|
45
|
+
}
|
|
46
|
+
async archive(file, clientKey, signal) {
|
|
47
|
+
const client = this.processing();
|
|
48
|
+
const upload = await client.upload(file, { clientKey, operation: "import", contentType: "application/x-ndjson", signal });
|
|
49
|
+
return client.submit({ operation: "import", uploadId: upload.uploadId, options: { inputFormat: "ndjson", outputFormat: "ndjson" } }, clientKey, signal);
|
|
50
|
+
}
|
|
51
|
+
#origin;
|
|
52
|
+
#fetch;
|
|
53
|
+
async getConfig(signal) {
|
|
54
|
+
const config = await this.#request("GET", "/v2/widget/config", void 0, signal);
|
|
55
|
+
if (!config || !uuid.test(config.sessionId) || !uuid.test(config.versionId) || !config.config?.experience || !config.limits || config.limits.maximumBatchBytes < 1024) throw new WidgetCloudError("INVALID_RESPONSE", "Invalid widget configuration");
|
|
56
|
+
return { ...config, config: { ...config.config, schema: parseTargetSchemaV1(config.config.schema) } };
|
|
57
|
+
}
|
|
58
|
+
async createImport(clientKey, format, signal) {
|
|
59
|
+
if (!uuid.test(clientKey)) throw new TypeError("Invalid import client key");
|
|
60
|
+
const value = await this.#request("POST", "/v2/widget/imports", JSON.stringify({ clientKey, format }), signal);
|
|
61
|
+
if (!value || !uuid.test(value.importId)) throw new WidgetCloudError("INVALID_RESPONSE", "Invalid import response");
|
|
62
|
+
return value.importId;
|
|
63
|
+
}
|
|
64
|
+
getImport(id, signal) {
|
|
65
|
+
return this.#request("GET", importPath(id), void 0, signal);
|
|
66
|
+
}
|
|
67
|
+
async deliverBatch(id, batchId, records, signal) {
|
|
68
|
+
const body = JSON.stringify({ batchId, records });
|
|
69
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(batchId) || !records.length || records.length > 1e4 || new TextEncoder().encode(body).byteLength > 2097152) throw new WidgetCloudError("BATCH_LIMIT", "Batch exceeds the delivery contract");
|
|
70
|
+
for (let attempt = 0; ; attempt++) {
|
|
71
|
+
try {
|
|
72
|
+
const receipt = await this.#request("POST", `${importPath(id)}/batches`, body, signal);
|
|
73
|
+
if (!receipt || receipt.batchId !== batchId || receipt.recordCount !== records.length || !["delivered", "duplicate"].includes(receipt.status)) throw new WidgetCloudError("INVALID_RECEIPT", "The destination acknowledgement could not be verified");
|
|
74
|
+
return;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (attempt >= 2 || !(error instanceof WidgetCloudError) || !["BATCH_IN_FLIGHT", "DELIVERY_UNCERTAIN"].includes(error.code)) throw error;
|
|
77
|
+
await new Promise((resolve, reject) => {
|
|
78
|
+
const abort = () => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
reject(signal?.reason);
|
|
81
|
+
};
|
|
82
|
+
const timer = setTimeout(() => {
|
|
83
|
+
signal?.removeEventListener("abort", abort);
|
|
84
|
+
resolve();
|
|
85
|
+
}, 500 * 3 ** attempt);
|
|
86
|
+
if (signal?.aborted) abort();
|
|
87
|
+
else signal?.addEventListener("abort", abort, { once: true });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async completeImport(id, recordsRejected, recordsDeduplicated, signal) {
|
|
93
|
+
await this.#request("POST", `${importPath(id)}/complete`, JSON.stringify({ recordsRejected, recordsDeduplicated }), signal);
|
|
94
|
+
}
|
|
95
|
+
async cancelImport(id, signal) {
|
|
96
|
+
await this.#request("POST", `${importPath(id)}/cancel`, "{}", signal);
|
|
97
|
+
}
|
|
98
|
+
async #request(method, path, body, signal) {
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const abort = () => controller.abort(signal?.reason);
|
|
101
|
+
if (signal?.aborted) abort();
|
|
102
|
+
else signal?.addEventListener("abort", abort, { once: true });
|
|
103
|
+
const timer = setTimeout(() => controller.abort(new DOMException("Cloud request timed out", "TimeoutError")), 15e3);
|
|
104
|
+
try {
|
|
105
|
+
const response = await this.#fetch(new URL(path, this.#origin), { method, body, redirect: "manual", credentials: "omit", cache: "no-store", signal: controller.signal, headers: { authorization: `Bearer ${this.options.sessionToken}`, "X-Convert-Buddy-Key": this.options.publishableKey, ...body ? { "content-type": "application/json" } : {} } });
|
|
106
|
+
if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
|
|
107
|
+
await response.body?.cancel();
|
|
108
|
+
throw new WidgetCloudError("REDIRECT_DENIED", "Cloud redirects are not permitted");
|
|
109
|
+
}
|
|
110
|
+
const reader = response.body?.getReader();
|
|
111
|
+
const parts = [];
|
|
112
|
+
let length = 0;
|
|
113
|
+
try {
|
|
114
|
+
while (reader) {
|
|
115
|
+
const chunk = await reader.read();
|
|
116
|
+
if (chunk.done) break;
|
|
117
|
+
length += chunk.value.byteLength;
|
|
118
|
+
if (length > 1048576) {
|
|
119
|
+
await reader.cancel();
|
|
120
|
+
throw new WidgetCloudError("RESPONSE_LIMIT", "Cloud metadata exceeds its limit");
|
|
121
|
+
}
|
|
122
|
+
parts.push(chunk.value);
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
reader?.releaseLock();
|
|
126
|
+
}
|
|
127
|
+
const bytes = new Uint8Array(length);
|
|
128
|
+
let offset = 0;
|
|
129
|
+
for (const part of parts) {
|
|
130
|
+
bytes.set(part, offset);
|
|
131
|
+
offset += part.length;
|
|
132
|
+
}
|
|
133
|
+
let value;
|
|
134
|
+
try {
|
|
135
|
+
value = JSON.parse(new TextDecoder().decode(bytes));
|
|
136
|
+
} catch {
|
|
137
|
+
throw new WidgetCloudError("INVALID_RESPONSE", "Cloud returned invalid metadata", response.status);
|
|
138
|
+
}
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
const error = value;
|
|
141
|
+
throw new WidgetCloudError(error.error?.code ?? "HTTP_ERROR", error.error?.message ?? "Cloud request failed", response.status, response.headers.get("X-Request-Id") ?? void 0);
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
} finally {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
signal?.removeEventListener("abort", abort);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
export {
|
|
151
|
+
ConvertBuddyWidgetClient,
|
|
152
|
+
ProcessingClient,
|
|
153
|
+
ProcessingCloudError,
|
|
154
|
+
WidgetCloudError,
|
|
155
|
+
blobFile,
|
|
156
|
+
createBrowserProcessingClient,
|
|
157
|
+
createWidgetProcessingClient
|
|
158
|
+
};
|