@revenexx/integrations-node-sdk 0.13.0 → 0.15.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/dist/{chunk-X5HI7WPI.js → chunk-WB3BYMLA.js} +12 -0
- package/dist/cli.cjs +31 -1
- package/dist/cli.js +23 -3
- package/dist/index.cjs +189 -2
- package/dist/index.d.cts +149 -2
- package/dist/index.d.ts +149 -2
- package/dist/index.js +167 -3
- package/package.json +1 -1
|
@@ -14,6 +14,17 @@ function extractCredentialManifests(credentials) {
|
|
|
14
14
|
|
|
15
15
|
// src/manifest.ts
|
|
16
16
|
var MANIFEST_VERSION = "v0-draft";
|
|
17
|
+
function parsePackageMeta(raw) {
|
|
18
|
+
const obj = raw && typeof raw === "object" ? raw : {};
|
|
19
|
+
const str = (v) => typeof v === "string" ? v.trim() : "";
|
|
20
|
+
const revenexx = obj.revenexx && typeof obj.revenexx === "object" ? obj.revenexx : {};
|
|
21
|
+
const displayName = str(revenexx.displayName);
|
|
22
|
+
return {
|
|
23
|
+
name: str(obj.name),
|
|
24
|
+
version: str(obj.version),
|
|
25
|
+
displayName: displayName !== "" ? displayName : void 0
|
|
26
|
+
};
|
|
27
|
+
}
|
|
17
28
|
function buildManifest(nodes, credentials = [], templates = []) {
|
|
18
29
|
const manifest = {
|
|
19
30
|
manifestVersion: MANIFEST_VERSION,
|
|
@@ -34,5 +45,6 @@ export {
|
|
|
34
45
|
extractCredentialManifest,
|
|
35
46
|
extractCredentialManifests,
|
|
36
47
|
MANIFEST_VERSION,
|
|
48
|
+
parsePackageMeta,
|
|
37
49
|
buildManifest
|
|
38
50
|
};
|
package/dist/cli.cjs
CHANGED
|
@@ -93,6 +93,17 @@ function extractCredentialManifests(credentials) {
|
|
|
93
93
|
|
|
94
94
|
// src/manifest.ts
|
|
95
95
|
var MANIFEST_VERSION = "v0-draft";
|
|
96
|
+
function parsePackageMeta(raw) {
|
|
97
|
+
const obj = raw && typeof raw === "object" ? raw : {};
|
|
98
|
+
const str = (v) => typeof v === "string" ? v.trim() : "";
|
|
99
|
+
const revenexx = obj.revenexx && typeof obj.revenexx === "object" ? obj.revenexx : {};
|
|
100
|
+
const displayName = str(revenexx.displayName);
|
|
101
|
+
return {
|
|
102
|
+
name: str(obj.name),
|
|
103
|
+
version: str(obj.version),
|
|
104
|
+
displayName: displayName !== "" ? displayName : void 0
|
|
105
|
+
};
|
|
106
|
+
}
|
|
96
107
|
function buildManifest(nodes, credentials = [], templates = []) {
|
|
97
108
|
const manifest = {
|
|
98
109
|
manifestVersion: MANIFEST_VERSION,
|
|
@@ -113,6 +124,17 @@ function fail(message) {
|
|
|
113
124
|
console.error(`Error: ${message}`);
|
|
114
125
|
process.exit(1);
|
|
115
126
|
}
|
|
127
|
+
function readPackageJson(root) {
|
|
128
|
+
const pkgPath = (0, import_node_path2.resolve)(root, "package.json");
|
|
129
|
+
if (!fs2.existsSync(pkgPath)) {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
134
|
+
} catch {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
116
138
|
async function runManifest() {
|
|
117
139
|
const distEntry = (0, import_node_path2.resolve)(projectRoot, "dist", "index.js");
|
|
118
140
|
if (!fs2.existsSync(distEntry)) {
|
|
@@ -130,6 +152,13 @@ async function runManifest() {
|
|
|
130
152
|
}
|
|
131
153
|
const credentials = mod.CREDENTIALS ?? [];
|
|
132
154
|
const templates = mod.TEMPLATES ?? [];
|
|
155
|
+
const meta = parsePackageMeta(readPackageJson(projectRoot));
|
|
156
|
+
const hasPackage = meta.name !== "" && meta.version !== "";
|
|
157
|
+
if (hasPackage && !meta.displayName) {
|
|
158
|
+
console.warn(
|
|
159
|
+
'\u26A0 package.json has no "revenexx.displayName" \u2014 the node palette will fall back to the raw package name.'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
133
162
|
const manifest = buildManifest(mod.NODES, credentials, templates);
|
|
134
163
|
const outDir = (0, import_node_path2.resolve)(projectRoot, "dist");
|
|
135
164
|
const outFile = (0, import_node_path2.resolve)(outDir, "manifest.json");
|
|
@@ -137,8 +166,9 @@ async function runManifest() {
|
|
|
137
166
|
fs2.writeFileSync(outFile, JSON.stringify(manifest, null, 2), "utf-8");
|
|
138
167
|
const credentialCount = manifest.credentials?.length ?? 0;
|
|
139
168
|
const templateCount = manifest.templates?.length ?? 0;
|
|
169
|
+
const bundlePrefix = hasPackage ? `package ${meta.displayName ? `"${meta.displayName}" ` : ""}(${meta.name}), ` : "";
|
|
140
170
|
console.log(
|
|
141
|
-
`\u2713 dist/manifest.json \u2014 ${manifest.nodes.length} node(s), ${credentialCount} credential(s), ${templateCount} template(s)`
|
|
171
|
+
`\u2713 dist/manifest.json \u2014 ${bundlePrefix}${manifest.nodes.length} node(s), ${credentialCount} credential(s), ${templateCount} template(s)`
|
|
142
172
|
);
|
|
143
173
|
for (const m of manifest.nodes) {
|
|
144
174
|
console.log(` node ${m.slug}@${m.version}`);
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
buildManifest
|
|
4
|
-
|
|
3
|
+
buildManifest,
|
|
4
|
+
parsePackageMeta
|
|
5
|
+
} from "./chunk-WB3BYMLA.js";
|
|
5
6
|
|
|
6
7
|
// src/cli.ts
|
|
7
8
|
import * as fs2 from "fs";
|
|
@@ -63,6 +64,17 @@ function fail(message) {
|
|
|
63
64
|
console.error(`Error: ${message}`);
|
|
64
65
|
process.exit(1);
|
|
65
66
|
}
|
|
67
|
+
function readPackageJson(root) {
|
|
68
|
+
const pkgPath = resolve2(root, "package.json");
|
|
69
|
+
if (!fs2.existsSync(pkgPath)) {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
66
78
|
async function runManifest() {
|
|
67
79
|
const distEntry = resolve2(projectRoot, "dist", "index.js");
|
|
68
80
|
if (!fs2.existsSync(distEntry)) {
|
|
@@ -80,6 +92,13 @@ async function runManifest() {
|
|
|
80
92
|
}
|
|
81
93
|
const credentials = mod.CREDENTIALS ?? [];
|
|
82
94
|
const templates = mod.TEMPLATES ?? [];
|
|
95
|
+
const meta = parsePackageMeta(readPackageJson(projectRoot));
|
|
96
|
+
const hasPackage = meta.name !== "" && meta.version !== "";
|
|
97
|
+
if (hasPackage && !meta.displayName) {
|
|
98
|
+
console.warn(
|
|
99
|
+
'\u26A0 package.json has no "revenexx.displayName" \u2014 the node palette will fall back to the raw package name.'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
83
102
|
const manifest = buildManifest(mod.NODES, credentials, templates);
|
|
84
103
|
const outDir = resolve2(projectRoot, "dist");
|
|
85
104
|
const outFile = resolve2(outDir, "manifest.json");
|
|
@@ -87,8 +106,9 @@ async function runManifest() {
|
|
|
87
106
|
fs2.writeFileSync(outFile, JSON.stringify(manifest, null, 2), "utf-8");
|
|
88
107
|
const credentialCount = manifest.credentials?.length ?? 0;
|
|
89
108
|
const templateCount = manifest.templates?.length ?? 0;
|
|
109
|
+
const bundlePrefix = hasPackage ? `package ${meta.displayName ? `"${meta.displayName}" ` : ""}(${meta.name}), ` : "";
|
|
90
110
|
console.log(
|
|
91
|
-
`\u2713 dist/manifest.json \u2014 ${manifest.nodes.length} node(s), ${credentialCount} credential(s), ${templateCount} template(s)`
|
|
111
|
+
`\u2713 dist/manifest.json \u2014 ${bundlePrefix}${manifest.nodes.length} node(s), ${credentialCount} credential(s), ${templateCount} template(s)`
|
|
92
112
|
);
|
|
93
113
|
for (const m of manifest.nodes) {
|
|
94
114
|
console.log(` node ${m.slug}@${m.version}`);
|
package/dist/index.cjs
CHANGED
|
@@ -23,28 +23,41 @@ __export(index_exports, {
|
|
|
23
23
|
ApiKeyCredential: () => ApiKeyCredential,
|
|
24
24
|
BaseCredential: () => BaseCredential,
|
|
25
25
|
BasicAuthCredential: () => BasicAuthCredential,
|
|
26
|
+
DEFAULT_MAX_RESPONSE_BYTES: () => DEFAULT_MAX_RESPONSE_BYTES,
|
|
26
27
|
DEFAULT_RETRY_ATTEMPTS: () => DEFAULT_RETRY_ATTEMPTS,
|
|
27
28
|
DEFAULT_RETRY_DELAY_MS: () => DEFAULT_RETRY_DELAY_MS,
|
|
29
|
+
DEFAULT_RETRY_POLICY: () => DEFAULT_RETRY_POLICY,
|
|
28
30
|
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
29
31
|
MANIFEST_VERSION: () => MANIFEST_VERSION,
|
|
32
|
+
MAX_RESPONSE_BYTES: () => MAX_RESPONSE_BYTES,
|
|
30
33
|
MAX_RETRY_ATTEMPTS: () => MAX_RETRY_ATTEMPTS,
|
|
31
34
|
MAX_TIMEOUT_MS: () => MAX_TIMEOUT_MS,
|
|
32
35
|
NodeError: () => NodeError,
|
|
33
36
|
OAuth2AuthCodeCredential: () => OAuth2AuthCodeCredential,
|
|
34
37
|
OAuth2ClientCredentialsCredential: () => OAuth2ClientCredentialsCredential,
|
|
38
|
+
RetryableError: () => RetryableError,
|
|
35
39
|
SimpleValueCredential: () => SimpleValueCredential,
|
|
40
|
+
backoffDelay: () => backoffDelay,
|
|
36
41
|
buildManifest: () => buildManifest,
|
|
42
|
+
clampResponseBytes: () => clampResponseBytes,
|
|
37
43
|
extractCredentialManifest: () => extractCredentialManifest,
|
|
38
44
|
extractCredentialManifests: () => extractCredentialManifests,
|
|
39
45
|
extractManifest: () => extractManifest,
|
|
40
46
|
extractManifests: () => extractManifests,
|
|
41
47
|
isNodeWithIteration: () => isNodeWithIteration,
|
|
42
48
|
isOAuthAuthorizeCredential: () => isOAuthAuthorizeCredential,
|
|
49
|
+
maxBytesConfigField: () => maxBytesConfigField,
|
|
43
50
|
normalizeCredentialType: () => normalizeCredentialType,
|
|
44
51
|
normalizeLocalized: () => normalizeLocalized,
|
|
52
|
+
parsePackageMeta: () => parsePackageMeta,
|
|
53
|
+
readArrayBuffer: () => readArrayBuffer,
|
|
54
|
+
readJsonOrText: () => readJsonOrText,
|
|
55
|
+
readText: () => readText,
|
|
45
56
|
retryConfigFields: () => retryConfigFields,
|
|
46
57
|
safeFetch: () => safeFetch,
|
|
47
|
-
|
|
58
|
+
sleepWithSignal: () => sleepWithSignal,
|
|
59
|
+
timeoutConfigField: () => timeoutConfigField,
|
|
60
|
+
withRetry: () => withRetry
|
|
48
61
|
});
|
|
49
62
|
module.exports = __toCommonJS(index_exports);
|
|
50
63
|
|
|
@@ -122,6 +135,12 @@ var MAX_TIMEOUT_MS = 12e4;
|
|
|
122
135
|
var DEFAULT_RETRY_ATTEMPTS = 0;
|
|
123
136
|
var MAX_RETRY_ATTEMPTS = 5;
|
|
124
137
|
var DEFAULT_RETRY_DELAY_MS = 1e3;
|
|
138
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 25 * 1024 * 1024;
|
|
139
|
+
var MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
|
|
140
|
+
function clampResponseBytes(maxBytes) {
|
|
141
|
+
if (!Number.isFinite(maxBytes) || maxBytes < 1) return MAX_RESPONSE_BYTES;
|
|
142
|
+
return Math.min(maxBytes, MAX_RESPONSE_BYTES);
|
|
143
|
+
}
|
|
125
144
|
async function safeFetch(url, options = {}) {
|
|
126
145
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, ...fetchOptions } = options;
|
|
127
146
|
const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
@@ -159,6 +178,82 @@ async function safeFetch(url, options = {}) {
|
|
|
159
178
|
}
|
|
160
179
|
throw lastError;
|
|
161
180
|
}
|
|
181
|
+
function tooLargeError(status, bytes, maxBytes) {
|
|
182
|
+
return new NodeError(
|
|
183
|
+
"RESPONSE_TOO_LARGE",
|
|
184
|
+
`Response body of ${bytes} bytes exceeds the ${maxBytes}-byte limit`,
|
|
185
|
+
{ status }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
async function readArrayBuffer(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
189
|
+
const cap = clampResponseBytes(maxBytes);
|
|
190
|
+
const declared = Number(res.headers.get("content-length"));
|
|
191
|
+
if (Number.isFinite(declared) && declared > cap) {
|
|
192
|
+
throw tooLargeError(res.status, declared, cap);
|
|
193
|
+
}
|
|
194
|
+
const body = res.body;
|
|
195
|
+
if (!body) {
|
|
196
|
+
const buf = await res.arrayBuffer();
|
|
197
|
+
if (buf.byteLength > cap) throw tooLargeError(res.status, buf.byteLength, cap);
|
|
198
|
+
return buf;
|
|
199
|
+
}
|
|
200
|
+
const reader = body.getReader();
|
|
201
|
+
const chunks = [];
|
|
202
|
+
let total = 0;
|
|
203
|
+
try {
|
|
204
|
+
for (; ; ) {
|
|
205
|
+
const { done, value } = await reader.read();
|
|
206
|
+
if (done) break;
|
|
207
|
+
total += value.byteLength;
|
|
208
|
+
if (total > cap) {
|
|
209
|
+
await reader.cancel().catch(() => {
|
|
210
|
+
});
|
|
211
|
+
throw tooLargeError(res.status, total, cap);
|
|
212
|
+
}
|
|
213
|
+
chunks.push(value);
|
|
214
|
+
}
|
|
215
|
+
} finally {
|
|
216
|
+
reader.releaseLock();
|
|
217
|
+
}
|
|
218
|
+
const out = new Uint8Array(total);
|
|
219
|
+
let offset = 0;
|
|
220
|
+
for (const chunk of chunks) {
|
|
221
|
+
out.set(chunk, offset);
|
|
222
|
+
offset += chunk.byteLength;
|
|
223
|
+
}
|
|
224
|
+
return out.buffer;
|
|
225
|
+
}
|
|
226
|
+
async function readText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
227
|
+
const buf = await readArrayBuffer(res, maxBytes);
|
|
228
|
+
return new TextDecoder().decode(buf);
|
|
229
|
+
}
|
|
230
|
+
function isJsonContentType(contentType) {
|
|
231
|
+
const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
232
|
+
return mediaType === "application/json" || mediaType.endsWith("+json");
|
|
233
|
+
}
|
|
234
|
+
async function readJsonOrText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
235
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
236
|
+
const text = await readText(res, maxBytes);
|
|
237
|
+
if (!isJsonContentType(contentType)) return text;
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(text);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
throw new NodeError(
|
|
242
|
+
"RESPONSE_PARSE_ERROR",
|
|
243
|
+
`Invalid JSON response: ${e.message}`,
|
|
244
|
+
{ status: res.status }
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function maxBytesConfigField(opts) {
|
|
249
|
+
return {
|
|
250
|
+
key: "maxBytes",
|
|
251
|
+
label: "Max response size (bytes)",
|
|
252
|
+
type: "number",
|
|
253
|
+
default: opts?.default ?? DEFAULT_MAX_RESPONSE_BYTES,
|
|
254
|
+
validation: { min: 1, max: Math.min(opts?.max ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES) }
|
|
255
|
+
};
|
|
256
|
+
}
|
|
162
257
|
function timeoutConfigField(opts) {
|
|
163
258
|
return {
|
|
164
259
|
key: "timeoutMs",
|
|
@@ -187,8 +282,87 @@ function retryConfigFields(opts) {
|
|
|
187
282
|
];
|
|
188
283
|
}
|
|
189
284
|
|
|
285
|
+
// src/retry.ts
|
|
286
|
+
var RetryableError = class extends Error {
|
|
287
|
+
/** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
|
|
288
|
+
retryAfterMs;
|
|
289
|
+
constructor(message, opts) {
|
|
290
|
+
super(message, { cause: opts?.cause });
|
|
291
|
+
this.name = "RetryableError";
|
|
292
|
+
this.retryAfterMs = opts?.retryAfterMs;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
var DEFAULT_RETRY_POLICY = {
|
|
296
|
+
maxAttempts: 3,
|
|
297
|
+
baseDelayMs: 500,
|
|
298
|
+
maxDelayMs: 3e4,
|
|
299
|
+
factor: 2,
|
|
300
|
+
jitter: true
|
|
301
|
+
};
|
|
302
|
+
function abortReason(signal) {
|
|
303
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
304
|
+
}
|
|
305
|
+
function backoffDelay(attempt, policy) {
|
|
306
|
+
const exponent = Math.max(0, attempt - 1);
|
|
307
|
+
const raw = policy.baseDelayMs * Math.pow(policy.factor, exponent);
|
|
308
|
+
const cap = Math.min(policy.maxDelayMs, raw);
|
|
309
|
+
return policy.jitter ? Math.random() * cap : cap;
|
|
310
|
+
}
|
|
311
|
+
function sleepWithSignal(ms, signal) {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
if (signal.aborted) {
|
|
314
|
+
reject(abortReason(signal));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const onAbort = () => {
|
|
318
|
+
clearTimeout(timer);
|
|
319
|
+
reject(abortReason(signal));
|
|
320
|
+
};
|
|
321
|
+
const timer = setTimeout(() => {
|
|
322
|
+
signal.removeEventListener("abort", onAbort);
|
|
323
|
+
resolve();
|
|
324
|
+
}, ms);
|
|
325
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async function withRetry(fn, policy, hooks) {
|
|
329
|
+
const effective = { ...DEFAULT_RETRY_POLICY, ...policy };
|
|
330
|
+
const maxAttempts = Math.max(1, effective.maxAttempts);
|
|
331
|
+
const { signal, logger, onRetry } = hooks;
|
|
332
|
+
for (let attempt = 1; ; attempt++) {
|
|
333
|
+
if (signal.aborted) throw abortReason(signal);
|
|
334
|
+
try {
|
|
335
|
+
return await fn(attempt);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
if (!(err instanceof RetryableError) || attempt >= maxAttempts) throw err;
|
|
338
|
+
const retryAfter = err.retryAfterMs;
|
|
339
|
+
const rawDelay = typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : backoffDelay(attempt, effective);
|
|
340
|
+
const delayMs = Number.isFinite(rawDelay) && rawDelay >= 0 ? rawDelay : 0;
|
|
341
|
+
onRetry?.({ attempt, delayMs, error: err });
|
|
342
|
+
logger?.warn("Retrying after retryable error", {
|
|
343
|
+
attempt,
|
|
344
|
+
maxAttempts,
|
|
345
|
+
delayMs,
|
|
346
|
+
error: err.message
|
|
347
|
+
});
|
|
348
|
+
await sleepWithSignal(delayMs, signal);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
190
353
|
// src/manifest.ts
|
|
191
354
|
var MANIFEST_VERSION = "v0-draft";
|
|
355
|
+
function parsePackageMeta(raw) {
|
|
356
|
+
const obj = raw && typeof raw === "object" ? raw : {};
|
|
357
|
+
const str = (v) => typeof v === "string" ? v.trim() : "";
|
|
358
|
+
const revenexx = obj.revenexx && typeof obj.revenexx === "object" ? obj.revenexx : {};
|
|
359
|
+
const displayName = str(revenexx.displayName);
|
|
360
|
+
return {
|
|
361
|
+
name: str(obj.name),
|
|
362
|
+
version: str(obj.version),
|
|
363
|
+
displayName: displayName !== "" ? displayName : void 0
|
|
364
|
+
};
|
|
365
|
+
}
|
|
192
366
|
function buildManifest(nodes, credentials = [], templates = []) {
|
|
193
367
|
const manifest = {
|
|
194
368
|
manifestVersion: MANIFEST_VERSION,
|
|
@@ -412,26 +586,39 @@ function base64Url(buf) {
|
|
|
412
586
|
ApiKeyCredential,
|
|
413
587
|
BaseCredential,
|
|
414
588
|
BasicAuthCredential,
|
|
589
|
+
DEFAULT_MAX_RESPONSE_BYTES,
|
|
415
590
|
DEFAULT_RETRY_ATTEMPTS,
|
|
416
591
|
DEFAULT_RETRY_DELAY_MS,
|
|
592
|
+
DEFAULT_RETRY_POLICY,
|
|
417
593
|
DEFAULT_TIMEOUT_MS,
|
|
418
594
|
MANIFEST_VERSION,
|
|
595
|
+
MAX_RESPONSE_BYTES,
|
|
419
596
|
MAX_RETRY_ATTEMPTS,
|
|
420
597
|
MAX_TIMEOUT_MS,
|
|
421
598
|
NodeError,
|
|
422
599
|
OAuth2AuthCodeCredential,
|
|
423
600
|
OAuth2ClientCredentialsCredential,
|
|
601
|
+
RetryableError,
|
|
424
602
|
SimpleValueCredential,
|
|
603
|
+
backoffDelay,
|
|
425
604
|
buildManifest,
|
|
605
|
+
clampResponseBytes,
|
|
426
606
|
extractCredentialManifest,
|
|
427
607
|
extractCredentialManifests,
|
|
428
608
|
extractManifest,
|
|
429
609
|
extractManifests,
|
|
430
610
|
isNodeWithIteration,
|
|
431
611
|
isOAuthAuthorizeCredential,
|
|
612
|
+
maxBytesConfigField,
|
|
432
613
|
normalizeCredentialType,
|
|
433
614
|
normalizeLocalized,
|
|
615
|
+
parsePackageMeta,
|
|
616
|
+
readArrayBuffer,
|
|
617
|
+
readJsonOrText,
|
|
618
|
+
readText,
|
|
434
619
|
retryConfigFields,
|
|
435
620
|
safeFetch,
|
|
436
|
-
|
|
621
|
+
sleepWithSignal,
|
|
622
|
+
timeoutConfigField,
|
|
623
|
+
withRetry
|
|
437
624
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -434,6 +434,21 @@ declare const MAX_TIMEOUT_MS = 120000;
|
|
|
434
434
|
declare const DEFAULT_RETRY_ATTEMPTS = 0;
|
|
435
435
|
declare const MAX_RETRY_ATTEMPTS = 5;
|
|
436
436
|
declare const DEFAULT_RETRY_DELAY_MS = 1000;
|
|
437
|
+
/**
|
|
438
|
+
* Default cap for response bodies read via {@link readArrayBuffer} /
|
|
439
|
+
* {@link readText} / {@link readJsonOrText}. Guards the (shared) worker process
|
|
440
|
+
* against a single oversized response exhausting its memory.
|
|
441
|
+
*/
|
|
442
|
+
declare const DEFAULT_MAX_RESPONSE_BYTES: number;
|
|
443
|
+
/**
|
|
444
|
+
* Hard upper ceiling for any per-node `maxBytes`. Even a permissive node author
|
|
445
|
+
* (who calls {@link maxBytesConfigField} without an explicit `max`, or passes a
|
|
446
|
+
* large `maxBytes` to the `read*` helpers) can never lift the cap above this, so
|
|
447
|
+
* the shared worker's memory stays bounded. Analogous to {@link MAX_TIMEOUT_MS}.
|
|
448
|
+
*/
|
|
449
|
+
declare const MAX_RESPONSE_BYTES: number;
|
|
450
|
+
/** Clamp a requested `maxBytes` into `[1, MAX_RESPONSE_BYTES]`. */
|
|
451
|
+
declare function clampResponseBytes(maxBytes: number): number;
|
|
437
452
|
interface SafeFetchRetry {
|
|
438
453
|
attempts: number;
|
|
439
454
|
delayMs?: number;
|
|
@@ -445,6 +460,31 @@ interface SafeFetchOptions extends RequestInit {
|
|
|
445
460
|
retry?: SafeFetchRetry;
|
|
446
461
|
}
|
|
447
462
|
declare function safeFetch(url: string | URL, options?: SafeFetchOptions): Promise<Response>;
|
|
463
|
+
/**
|
|
464
|
+
* Read a response body into an ArrayBuffer while enforcing a hard byte cap.
|
|
465
|
+
*
|
|
466
|
+
* The `Content-Length` header is used as a fast-reject (bail before downloading
|
|
467
|
+
* anything), but the limit is *also* enforced while streaming, since the header
|
|
468
|
+
* can be absent or lie. On overrun the stream is cancelled and a
|
|
469
|
+
* `NodeError('RESPONSE_TOO_LARGE', …, { status })` is thrown.
|
|
470
|
+
*/
|
|
471
|
+
declare function readArrayBuffer(res: Response, maxBytes?: number): Promise<ArrayBuffer>;
|
|
472
|
+
/** Read a response body as UTF-8 text, capped at `maxBytes` (see {@link readArrayBuffer}). */
|
|
473
|
+
declare function readText(res: Response, maxBytes?: number): Promise<string>;
|
|
474
|
+
/**
|
|
475
|
+
* Read a response body as JSON when the `Content-Type` denotes JSON (see
|
|
476
|
+
* {@link isJsonContentType}), otherwise as text — the content-type sniff
|
|
477
|
+
* previously duplicated across the HTTP/Upload/DeepL node sinks. Capped at
|
|
478
|
+
* `maxBytes` (see {@link readArrayBuffer}).
|
|
479
|
+
*
|
|
480
|
+
* A malformed JSON body surfaces as `NodeError('RESPONSE_PARSE_ERROR', …, { status })`
|
|
481
|
+
* rather than a raw `SyntaxError`, keeping to the SDK error contract.
|
|
482
|
+
*/
|
|
483
|
+
declare function readJsonOrText(res: Response, maxBytes?: number): Promise<unknown>;
|
|
484
|
+
declare function maxBytesConfigField(opts?: {
|
|
485
|
+
default?: number;
|
|
486
|
+
max?: number;
|
|
487
|
+
}): IConfigField;
|
|
448
488
|
declare function timeoutConfigField(opts?: {
|
|
449
489
|
default?: number;
|
|
450
490
|
max?: number;
|
|
@@ -454,6 +494,82 @@ declare function retryConfigFields(opts?: {
|
|
|
454
494
|
defaultDelayMs?: number;
|
|
455
495
|
}): IConfigField[];
|
|
456
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Transport-agnostic retry/backoff primitive (PO-139).
|
|
499
|
+
*
|
|
500
|
+
* Wraps *any* async operation — raw `fetch`, an SDK client (axios), a future
|
|
501
|
+
* transport — with exponential backoff + full jitter, honouring a
|
|
502
|
+
* server-dictated delay (HTTP `Retry-After`) when the caller supplies one.
|
|
503
|
+
*
|
|
504
|
+
* The retry decision lives with the connector: it throws a {@link RetryableError}
|
|
505
|
+
* only when an attempt failed *and* is worth retrying. Everything else is
|
|
506
|
+
* rethrown as-is, and terminal API errors that connectors model as *values*
|
|
507
|
+
* (e.g. a `{ kind: 'http-error' }` union) simply flow back as the return value.
|
|
508
|
+
*
|
|
509
|
+
* This is deliberately NOT a circuit breaker, rate limiter, or HTTP client, and
|
|
510
|
+
* it does not replace Temporal activity-level retries — it is the fine-grained,
|
|
511
|
+
* idempotency-aware, `Retry-After`-honouring layer inside a single activity.
|
|
512
|
+
*/
|
|
513
|
+
/** Thrown by the wrapped fn to signal "this attempt failed but is retryable". */
|
|
514
|
+
declare class RetryableError extends Error {
|
|
515
|
+
/** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
|
|
516
|
+
readonly retryAfterMs?: number;
|
|
517
|
+
constructor(message: string, opts?: {
|
|
518
|
+
retryAfterMs?: number;
|
|
519
|
+
cause?: unknown;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
interface RetryPolicy {
|
|
523
|
+
/** Total tries including the first. */
|
|
524
|
+
maxAttempts: number;
|
|
525
|
+
/** Base delay for the exponential backoff. */
|
|
526
|
+
baseDelayMs: number;
|
|
527
|
+
/** Upper bound for any single computed delay. */
|
|
528
|
+
maxDelayMs: number;
|
|
529
|
+
/** Exponential growth factor. */
|
|
530
|
+
factor: number;
|
|
531
|
+
/** Apply full jitter to the computed backoff. */
|
|
532
|
+
jitter: boolean;
|
|
533
|
+
}
|
|
534
|
+
declare const DEFAULT_RETRY_POLICY: RetryPolicy;
|
|
535
|
+
interface RetryHooks {
|
|
536
|
+
/** Pass `ctx.signal` so cancelling a workflow cancels the wait (and stops retrying) immediately. */
|
|
537
|
+
signal: AbortSignal;
|
|
538
|
+
logger?: {
|
|
539
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
540
|
+
};
|
|
541
|
+
onRetry?(info: {
|
|
542
|
+
attempt: number;
|
|
543
|
+
delayMs: number;
|
|
544
|
+
error: unknown;
|
|
545
|
+
}): void;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Pure backoff math, exported for testing/reuse. Returns the delay before the
|
|
549
|
+
* retry that follows the given (1-based) failed `attempt`: an exponential term
|
|
550
|
+
* `baseDelayMs * factor^(attempt-1)` capped at `maxDelayMs`, with full jitter
|
|
551
|
+
* (`random() * cap`) when `policy.jitter` is set.
|
|
552
|
+
*/
|
|
553
|
+
declare function backoffDelay(attempt: number, policy: RetryPolicy): number;
|
|
554
|
+
/**
|
|
555
|
+
* Abort-aware sleep. Resolves after `ms`, or rejects with the signal's reason
|
|
556
|
+
* (a DOMException `AbortError` by default) as soon as `signal` aborts — so a
|
|
557
|
+
* caller's retry loop stops cleanly instead of finishing the wait.
|
|
558
|
+
*/
|
|
559
|
+
declare function sleepWithSignal(ms: number, signal: AbortSignal): Promise<void>;
|
|
560
|
+
/**
|
|
561
|
+
* Runs `fn`; if it throws a {@link RetryableError} and attempts remain, waits
|
|
562
|
+
* (`RetryableError.retryAfterMs` when present, else the computed
|
|
563
|
+
* {@link backoffDelay}) respecting `signal`, then retries. Any other throw is
|
|
564
|
+
* rethrown immediately; on the final attempt the RetryableError itself is
|
|
565
|
+
* thrown (preserving its `retryAfterMs`/`cause`), not its `.cause`.
|
|
566
|
+
*
|
|
567
|
+
* Distinct from `safeFetch`'s built-in fixed-delay `retry` option, which is
|
|
568
|
+
* unchanged: that is a simple in-fetch retry; this is the richer, transport-
|
|
569
|
+
* agnostic, `Retry-After`-aware primitive connectors wrap around any operation.
|
|
570
|
+
*/
|
|
571
|
+
declare function withRetry<T>(fn: (attempt: number) => Promise<T>, policy: Partial<RetryPolicy>, hooks: RetryHooks): Promise<T>;
|
|
572
|
+
|
|
457
573
|
/**
|
|
458
574
|
* Envelope version expected by the integrations server-side
|
|
459
575
|
* `TarballInspector`. Earlier (pre-registry) builds emitted a bare array
|
|
@@ -478,10 +594,41 @@ interface NodeManifest {
|
|
|
478
594
|
*/
|
|
479
595
|
templates?: ITemplateDescription[];
|
|
480
596
|
}
|
|
597
|
+
/**
|
|
598
|
+
* The registry-relevant fields of a node package's `package.json`, as read by
|
|
599
|
+
* the CLI. `name`/`version` identify the package; `displayName` is the
|
|
600
|
+
* human-readable bundle label shown in the editor's node palette (e.g.
|
|
601
|
+
* „Business Central"). All three are read straight from `package.json` by the
|
|
602
|
+
* integrations server on upload — the CLI reads them only to warn about a
|
|
603
|
+
* missing label and to annotate the manifest log line.
|
|
604
|
+
*
|
|
605
|
+
* The label lives under a namespaced `revenexx` group in `package.json`
|
|
606
|
+
* (`{ "revenexx": { "displayName": "…" } }`), not a bespoke top-level key, so
|
|
607
|
+
* it can't collide with unrelated tooling that squats on `displayName`.
|
|
608
|
+
*/
|
|
609
|
+
interface NodePackageMeta {
|
|
610
|
+
name: string;
|
|
611
|
+
version: string;
|
|
612
|
+
/** Human-readable bundle label (e.g. „Business Central"); optional. */
|
|
613
|
+
displayName?: string;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Extracts {@link NodePackageMeta} from parsed `package.json` contents, keeping
|
|
617
|
+
* only the registry-relevant fields and coercing anything malformed to a safe
|
|
618
|
+
* shape. All three fields are trimmed; a blank or whitespace-only value becomes
|
|
619
|
+
* `''` (`name`/`version`) or `undefined` (`displayName`, matching how the server
|
|
620
|
+
* treats it), so whitespace can't masquerade as a present value in tooling.
|
|
621
|
+
* The bundle label is read from the `revenexx` group (`revenexx.displayName`).
|
|
622
|
+
* Does not validate that `name`/`version` are present — the integrations server
|
|
623
|
+
* enforces that on upload; this is a typed, lenient read for tooling.
|
|
624
|
+
*/
|
|
625
|
+
declare function parsePackageMeta(raw: unknown): NodePackageMeta;
|
|
481
626
|
/**
|
|
482
627
|
* Builds the manifest envelope from a package's `NODES` (and optional
|
|
483
628
|
* `CREDENTIALS` / `TEMPLATES`) exports. The result is what gets written to
|
|
484
|
-
* `dist/manifest.json` and uploaded to the registry inside the tarball.
|
|
629
|
+
* `dist/manifest.json` and uploaded to the registry inside the tarball. The
|
|
630
|
+
* bundle label is NOT carried here — the integrations server reads it straight
|
|
631
|
+
* from `package.json` (`revenexx.displayName`).
|
|
485
632
|
*/
|
|
486
633
|
declare function buildManifest(nodes: INode[], credentials?: ICredential[], templates?: ITemplateDescription[]): NodeManifest;
|
|
487
634
|
|
|
@@ -593,4 +740,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
|
|
|
593
740
|
test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
594
741
|
}
|
|
595
742
|
|
|
596
|
-
export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, buildManifest, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, normalizeCredentialType, normalizeLocalized, retryConfigFields, safeFetch, timeoutConfigField };
|
|
743
|
+
export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, backoffDelay, buildManifest, clampResponseBytes, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, sleepWithSignal, timeoutConfigField, withRetry };
|
package/dist/index.d.ts
CHANGED
|
@@ -434,6 +434,21 @@ declare const MAX_TIMEOUT_MS = 120000;
|
|
|
434
434
|
declare const DEFAULT_RETRY_ATTEMPTS = 0;
|
|
435
435
|
declare const MAX_RETRY_ATTEMPTS = 5;
|
|
436
436
|
declare const DEFAULT_RETRY_DELAY_MS = 1000;
|
|
437
|
+
/**
|
|
438
|
+
* Default cap for response bodies read via {@link readArrayBuffer} /
|
|
439
|
+
* {@link readText} / {@link readJsonOrText}. Guards the (shared) worker process
|
|
440
|
+
* against a single oversized response exhausting its memory.
|
|
441
|
+
*/
|
|
442
|
+
declare const DEFAULT_MAX_RESPONSE_BYTES: number;
|
|
443
|
+
/**
|
|
444
|
+
* Hard upper ceiling for any per-node `maxBytes`. Even a permissive node author
|
|
445
|
+
* (who calls {@link maxBytesConfigField} without an explicit `max`, or passes a
|
|
446
|
+
* large `maxBytes` to the `read*` helpers) can never lift the cap above this, so
|
|
447
|
+
* the shared worker's memory stays bounded. Analogous to {@link MAX_TIMEOUT_MS}.
|
|
448
|
+
*/
|
|
449
|
+
declare const MAX_RESPONSE_BYTES: number;
|
|
450
|
+
/** Clamp a requested `maxBytes` into `[1, MAX_RESPONSE_BYTES]`. */
|
|
451
|
+
declare function clampResponseBytes(maxBytes: number): number;
|
|
437
452
|
interface SafeFetchRetry {
|
|
438
453
|
attempts: number;
|
|
439
454
|
delayMs?: number;
|
|
@@ -445,6 +460,31 @@ interface SafeFetchOptions extends RequestInit {
|
|
|
445
460
|
retry?: SafeFetchRetry;
|
|
446
461
|
}
|
|
447
462
|
declare function safeFetch(url: string | URL, options?: SafeFetchOptions): Promise<Response>;
|
|
463
|
+
/**
|
|
464
|
+
* Read a response body into an ArrayBuffer while enforcing a hard byte cap.
|
|
465
|
+
*
|
|
466
|
+
* The `Content-Length` header is used as a fast-reject (bail before downloading
|
|
467
|
+
* anything), but the limit is *also* enforced while streaming, since the header
|
|
468
|
+
* can be absent or lie. On overrun the stream is cancelled and a
|
|
469
|
+
* `NodeError('RESPONSE_TOO_LARGE', …, { status })` is thrown.
|
|
470
|
+
*/
|
|
471
|
+
declare function readArrayBuffer(res: Response, maxBytes?: number): Promise<ArrayBuffer>;
|
|
472
|
+
/** Read a response body as UTF-8 text, capped at `maxBytes` (see {@link readArrayBuffer}). */
|
|
473
|
+
declare function readText(res: Response, maxBytes?: number): Promise<string>;
|
|
474
|
+
/**
|
|
475
|
+
* Read a response body as JSON when the `Content-Type` denotes JSON (see
|
|
476
|
+
* {@link isJsonContentType}), otherwise as text — the content-type sniff
|
|
477
|
+
* previously duplicated across the HTTP/Upload/DeepL node sinks. Capped at
|
|
478
|
+
* `maxBytes` (see {@link readArrayBuffer}).
|
|
479
|
+
*
|
|
480
|
+
* A malformed JSON body surfaces as `NodeError('RESPONSE_PARSE_ERROR', …, { status })`
|
|
481
|
+
* rather than a raw `SyntaxError`, keeping to the SDK error contract.
|
|
482
|
+
*/
|
|
483
|
+
declare function readJsonOrText(res: Response, maxBytes?: number): Promise<unknown>;
|
|
484
|
+
declare function maxBytesConfigField(opts?: {
|
|
485
|
+
default?: number;
|
|
486
|
+
max?: number;
|
|
487
|
+
}): IConfigField;
|
|
448
488
|
declare function timeoutConfigField(opts?: {
|
|
449
489
|
default?: number;
|
|
450
490
|
max?: number;
|
|
@@ -454,6 +494,82 @@ declare function retryConfigFields(opts?: {
|
|
|
454
494
|
defaultDelayMs?: number;
|
|
455
495
|
}): IConfigField[];
|
|
456
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Transport-agnostic retry/backoff primitive (PO-139).
|
|
499
|
+
*
|
|
500
|
+
* Wraps *any* async operation — raw `fetch`, an SDK client (axios), a future
|
|
501
|
+
* transport — with exponential backoff + full jitter, honouring a
|
|
502
|
+
* server-dictated delay (HTTP `Retry-After`) when the caller supplies one.
|
|
503
|
+
*
|
|
504
|
+
* The retry decision lives with the connector: it throws a {@link RetryableError}
|
|
505
|
+
* only when an attempt failed *and* is worth retrying. Everything else is
|
|
506
|
+
* rethrown as-is, and terminal API errors that connectors model as *values*
|
|
507
|
+
* (e.g. a `{ kind: 'http-error' }` union) simply flow back as the return value.
|
|
508
|
+
*
|
|
509
|
+
* This is deliberately NOT a circuit breaker, rate limiter, or HTTP client, and
|
|
510
|
+
* it does not replace Temporal activity-level retries — it is the fine-grained,
|
|
511
|
+
* idempotency-aware, `Retry-After`-honouring layer inside a single activity.
|
|
512
|
+
*/
|
|
513
|
+
/** Thrown by the wrapped fn to signal "this attempt failed but is retryable". */
|
|
514
|
+
declare class RetryableError extends Error {
|
|
515
|
+
/** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
|
|
516
|
+
readonly retryAfterMs?: number;
|
|
517
|
+
constructor(message: string, opts?: {
|
|
518
|
+
retryAfterMs?: number;
|
|
519
|
+
cause?: unknown;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
interface RetryPolicy {
|
|
523
|
+
/** Total tries including the first. */
|
|
524
|
+
maxAttempts: number;
|
|
525
|
+
/** Base delay for the exponential backoff. */
|
|
526
|
+
baseDelayMs: number;
|
|
527
|
+
/** Upper bound for any single computed delay. */
|
|
528
|
+
maxDelayMs: number;
|
|
529
|
+
/** Exponential growth factor. */
|
|
530
|
+
factor: number;
|
|
531
|
+
/** Apply full jitter to the computed backoff. */
|
|
532
|
+
jitter: boolean;
|
|
533
|
+
}
|
|
534
|
+
declare const DEFAULT_RETRY_POLICY: RetryPolicy;
|
|
535
|
+
interface RetryHooks {
|
|
536
|
+
/** Pass `ctx.signal` so cancelling a workflow cancels the wait (and stops retrying) immediately. */
|
|
537
|
+
signal: AbortSignal;
|
|
538
|
+
logger?: {
|
|
539
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
540
|
+
};
|
|
541
|
+
onRetry?(info: {
|
|
542
|
+
attempt: number;
|
|
543
|
+
delayMs: number;
|
|
544
|
+
error: unknown;
|
|
545
|
+
}): void;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Pure backoff math, exported for testing/reuse. Returns the delay before the
|
|
549
|
+
* retry that follows the given (1-based) failed `attempt`: an exponential term
|
|
550
|
+
* `baseDelayMs * factor^(attempt-1)` capped at `maxDelayMs`, with full jitter
|
|
551
|
+
* (`random() * cap`) when `policy.jitter` is set.
|
|
552
|
+
*/
|
|
553
|
+
declare function backoffDelay(attempt: number, policy: RetryPolicy): number;
|
|
554
|
+
/**
|
|
555
|
+
* Abort-aware sleep. Resolves after `ms`, or rejects with the signal's reason
|
|
556
|
+
* (a DOMException `AbortError` by default) as soon as `signal` aborts — so a
|
|
557
|
+
* caller's retry loop stops cleanly instead of finishing the wait.
|
|
558
|
+
*/
|
|
559
|
+
declare function sleepWithSignal(ms: number, signal: AbortSignal): Promise<void>;
|
|
560
|
+
/**
|
|
561
|
+
* Runs `fn`; if it throws a {@link RetryableError} and attempts remain, waits
|
|
562
|
+
* (`RetryableError.retryAfterMs` when present, else the computed
|
|
563
|
+
* {@link backoffDelay}) respecting `signal`, then retries. Any other throw is
|
|
564
|
+
* rethrown immediately; on the final attempt the RetryableError itself is
|
|
565
|
+
* thrown (preserving its `retryAfterMs`/`cause`), not its `.cause`.
|
|
566
|
+
*
|
|
567
|
+
* Distinct from `safeFetch`'s built-in fixed-delay `retry` option, which is
|
|
568
|
+
* unchanged: that is a simple in-fetch retry; this is the richer, transport-
|
|
569
|
+
* agnostic, `Retry-After`-aware primitive connectors wrap around any operation.
|
|
570
|
+
*/
|
|
571
|
+
declare function withRetry<T>(fn: (attempt: number) => Promise<T>, policy: Partial<RetryPolicy>, hooks: RetryHooks): Promise<T>;
|
|
572
|
+
|
|
457
573
|
/**
|
|
458
574
|
* Envelope version expected by the integrations server-side
|
|
459
575
|
* `TarballInspector`. Earlier (pre-registry) builds emitted a bare array
|
|
@@ -478,10 +594,41 @@ interface NodeManifest {
|
|
|
478
594
|
*/
|
|
479
595
|
templates?: ITemplateDescription[];
|
|
480
596
|
}
|
|
597
|
+
/**
|
|
598
|
+
* The registry-relevant fields of a node package's `package.json`, as read by
|
|
599
|
+
* the CLI. `name`/`version` identify the package; `displayName` is the
|
|
600
|
+
* human-readable bundle label shown in the editor's node palette (e.g.
|
|
601
|
+
* „Business Central"). All three are read straight from `package.json` by the
|
|
602
|
+
* integrations server on upload — the CLI reads them only to warn about a
|
|
603
|
+
* missing label and to annotate the manifest log line.
|
|
604
|
+
*
|
|
605
|
+
* The label lives under a namespaced `revenexx` group in `package.json`
|
|
606
|
+
* (`{ "revenexx": { "displayName": "…" } }`), not a bespoke top-level key, so
|
|
607
|
+
* it can't collide with unrelated tooling that squats on `displayName`.
|
|
608
|
+
*/
|
|
609
|
+
interface NodePackageMeta {
|
|
610
|
+
name: string;
|
|
611
|
+
version: string;
|
|
612
|
+
/** Human-readable bundle label (e.g. „Business Central"); optional. */
|
|
613
|
+
displayName?: string;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Extracts {@link NodePackageMeta} from parsed `package.json` contents, keeping
|
|
617
|
+
* only the registry-relevant fields and coercing anything malformed to a safe
|
|
618
|
+
* shape. All three fields are trimmed; a blank or whitespace-only value becomes
|
|
619
|
+
* `''` (`name`/`version`) or `undefined` (`displayName`, matching how the server
|
|
620
|
+
* treats it), so whitespace can't masquerade as a present value in tooling.
|
|
621
|
+
* The bundle label is read from the `revenexx` group (`revenexx.displayName`).
|
|
622
|
+
* Does not validate that `name`/`version` are present — the integrations server
|
|
623
|
+
* enforces that on upload; this is a typed, lenient read for tooling.
|
|
624
|
+
*/
|
|
625
|
+
declare function parsePackageMeta(raw: unknown): NodePackageMeta;
|
|
481
626
|
/**
|
|
482
627
|
* Builds the manifest envelope from a package's `NODES` (and optional
|
|
483
628
|
* `CREDENTIALS` / `TEMPLATES`) exports. The result is what gets written to
|
|
484
|
-
* `dist/manifest.json` and uploaded to the registry inside the tarball.
|
|
629
|
+
* `dist/manifest.json` and uploaded to the registry inside the tarball. The
|
|
630
|
+
* bundle label is NOT carried here — the integrations server reads it straight
|
|
631
|
+
* from `package.json` (`revenexx.displayName`).
|
|
485
632
|
*/
|
|
486
633
|
declare function buildManifest(nodes: INode[], credentials?: ICredential[], templates?: ITemplateDescription[]): NodeManifest;
|
|
487
634
|
|
|
@@ -593,4 +740,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
|
|
|
593
740
|
test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
594
741
|
}
|
|
595
742
|
|
|
596
|
-
export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, buildManifest, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, normalizeCredentialType, normalizeLocalized, retryConfigFields, safeFetch, timeoutConfigField };
|
|
743
|
+
export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, backoffDelay, buildManifest, clampResponseBytes, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, sleepWithSignal, timeoutConfigField, withRetry };
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
extractCredentialManifest,
|
|
5
5
|
extractCredentialManifests,
|
|
6
6
|
extractManifest,
|
|
7
|
-
extractManifests
|
|
8
|
-
|
|
7
|
+
extractManifests,
|
|
8
|
+
parsePackageMeta
|
|
9
|
+
} from "./chunk-WB3BYMLA.js";
|
|
9
10
|
|
|
10
11
|
// src/types.ts
|
|
11
12
|
function isNodeWithIteration(node) {
|
|
@@ -67,6 +68,12 @@ var MAX_TIMEOUT_MS = 12e4;
|
|
|
67
68
|
var DEFAULT_RETRY_ATTEMPTS = 0;
|
|
68
69
|
var MAX_RETRY_ATTEMPTS = 5;
|
|
69
70
|
var DEFAULT_RETRY_DELAY_MS = 1e3;
|
|
71
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 25 * 1024 * 1024;
|
|
72
|
+
var MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
|
|
73
|
+
function clampResponseBytes(maxBytes) {
|
|
74
|
+
if (!Number.isFinite(maxBytes) || maxBytes < 1) return MAX_RESPONSE_BYTES;
|
|
75
|
+
return Math.min(maxBytes, MAX_RESPONSE_BYTES);
|
|
76
|
+
}
|
|
70
77
|
async function safeFetch(url, options = {}) {
|
|
71
78
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, ...fetchOptions } = options;
|
|
72
79
|
const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
@@ -104,6 +111,82 @@ async function safeFetch(url, options = {}) {
|
|
|
104
111
|
}
|
|
105
112
|
throw lastError;
|
|
106
113
|
}
|
|
114
|
+
function tooLargeError(status, bytes, maxBytes) {
|
|
115
|
+
return new NodeError(
|
|
116
|
+
"RESPONSE_TOO_LARGE",
|
|
117
|
+
`Response body of ${bytes} bytes exceeds the ${maxBytes}-byte limit`,
|
|
118
|
+
{ status }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
async function readArrayBuffer(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
122
|
+
const cap = clampResponseBytes(maxBytes);
|
|
123
|
+
const declared = Number(res.headers.get("content-length"));
|
|
124
|
+
if (Number.isFinite(declared) && declared > cap) {
|
|
125
|
+
throw tooLargeError(res.status, declared, cap);
|
|
126
|
+
}
|
|
127
|
+
const body = res.body;
|
|
128
|
+
if (!body) {
|
|
129
|
+
const buf = await res.arrayBuffer();
|
|
130
|
+
if (buf.byteLength > cap) throw tooLargeError(res.status, buf.byteLength, cap);
|
|
131
|
+
return buf;
|
|
132
|
+
}
|
|
133
|
+
const reader = body.getReader();
|
|
134
|
+
const chunks = [];
|
|
135
|
+
let total = 0;
|
|
136
|
+
try {
|
|
137
|
+
for (; ; ) {
|
|
138
|
+
const { done, value } = await reader.read();
|
|
139
|
+
if (done) break;
|
|
140
|
+
total += value.byteLength;
|
|
141
|
+
if (total > cap) {
|
|
142
|
+
await reader.cancel().catch(() => {
|
|
143
|
+
});
|
|
144
|
+
throw tooLargeError(res.status, total, cap);
|
|
145
|
+
}
|
|
146
|
+
chunks.push(value);
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
reader.releaseLock();
|
|
150
|
+
}
|
|
151
|
+
const out = new Uint8Array(total);
|
|
152
|
+
let offset = 0;
|
|
153
|
+
for (const chunk of chunks) {
|
|
154
|
+
out.set(chunk, offset);
|
|
155
|
+
offset += chunk.byteLength;
|
|
156
|
+
}
|
|
157
|
+
return out.buffer;
|
|
158
|
+
}
|
|
159
|
+
async function readText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
160
|
+
const buf = await readArrayBuffer(res, maxBytes);
|
|
161
|
+
return new TextDecoder().decode(buf);
|
|
162
|
+
}
|
|
163
|
+
function isJsonContentType(contentType) {
|
|
164
|
+
const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
165
|
+
return mediaType === "application/json" || mediaType.endsWith("+json");
|
|
166
|
+
}
|
|
167
|
+
async function readJsonOrText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
168
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
169
|
+
const text = await readText(res, maxBytes);
|
|
170
|
+
if (!isJsonContentType(contentType)) return text;
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(text);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
throw new NodeError(
|
|
175
|
+
"RESPONSE_PARSE_ERROR",
|
|
176
|
+
`Invalid JSON response: ${e.message}`,
|
|
177
|
+
{ status: res.status }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function maxBytesConfigField(opts) {
|
|
182
|
+
return {
|
|
183
|
+
key: "maxBytes",
|
|
184
|
+
label: "Max response size (bytes)",
|
|
185
|
+
type: "number",
|
|
186
|
+
default: opts?.default ?? DEFAULT_MAX_RESPONSE_BYTES,
|
|
187
|
+
validation: { min: 1, max: Math.min(opts?.max ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES) }
|
|
188
|
+
};
|
|
189
|
+
}
|
|
107
190
|
function timeoutConfigField(opts) {
|
|
108
191
|
return {
|
|
109
192
|
key: "timeoutMs",
|
|
@@ -132,6 +215,74 @@ function retryConfigFields(opts) {
|
|
|
132
215
|
];
|
|
133
216
|
}
|
|
134
217
|
|
|
218
|
+
// src/retry.ts
|
|
219
|
+
var RetryableError = class extends Error {
|
|
220
|
+
/** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
|
|
221
|
+
retryAfterMs;
|
|
222
|
+
constructor(message, opts) {
|
|
223
|
+
super(message, { cause: opts?.cause });
|
|
224
|
+
this.name = "RetryableError";
|
|
225
|
+
this.retryAfterMs = opts?.retryAfterMs;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var DEFAULT_RETRY_POLICY = {
|
|
229
|
+
maxAttempts: 3,
|
|
230
|
+
baseDelayMs: 500,
|
|
231
|
+
maxDelayMs: 3e4,
|
|
232
|
+
factor: 2,
|
|
233
|
+
jitter: true
|
|
234
|
+
};
|
|
235
|
+
function abortReason(signal) {
|
|
236
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
237
|
+
}
|
|
238
|
+
function backoffDelay(attempt, policy) {
|
|
239
|
+
const exponent = Math.max(0, attempt - 1);
|
|
240
|
+
const raw = policy.baseDelayMs * Math.pow(policy.factor, exponent);
|
|
241
|
+
const cap = Math.min(policy.maxDelayMs, raw);
|
|
242
|
+
return policy.jitter ? Math.random() * cap : cap;
|
|
243
|
+
}
|
|
244
|
+
function sleepWithSignal(ms, signal) {
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
if (signal.aborted) {
|
|
247
|
+
reject(abortReason(signal));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const onAbort = () => {
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
reject(abortReason(signal));
|
|
253
|
+
};
|
|
254
|
+
const timer = setTimeout(() => {
|
|
255
|
+
signal.removeEventListener("abort", onAbort);
|
|
256
|
+
resolve();
|
|
257
|
+
}, ms);
|
|
258
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
async function withRetry(fn, policy, hooks) {
|
|
262
|
+
const effective = { ...DEFAULT_RETRY_POLICY, ...policy };
|
|
263
|
+
const maxAttempts = Math.max(1, effective.maxAttempts);
|
|
264
|
+
const { signal, logger, onRetry } = hooks;
|
|
265
|
+
for (let attempt = 1; ; attempt++) {
|
|
266
|
+
if (signal.aborted) throw abortReason(signal);
|
|
267
|
+
try {
|
|
268
|
+
return await fn(attempt);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if (!(err instanceof RetryableError) || attempt >= maxAttempts) throw err;
|
|
271
|
+
const retryAfter = err.retryAfterMs;
|
|
272
|
+
const rawDelay = typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : backoffDelay(attempt, effective);
|
|
273
|
+
const delayMs = Number.isFinite(rawDelay) && rawDelay >= 0 ? rawDelay : 0;
|
|
274
|
+
onRetry?.({ attempt, delayMs, error: err });
|
|
275
|
+
logger?.warn("Retrying after retryable error", {
|
|
276
|
+
attempt,
|
|
277
|
+
maxAttempts,
|
|
278
|
+
delayMs,
|
|
279
|
+
error: err.message
|
|
280
|
+
});
|
|
281
|
+
await sleepWithSignal(delayMs, signal);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
135
286
|
// src/credentials.ts
|
|
136
287
|
import { createHash, randomBytes } from "crypto";
|
|
137
288
|
function readString(source, key) {
|
|
@@ -340,26 +491,39 @@ export {
|
|
|
340
491
|
ApiKeyCredential,
|
|
341
492
|
BaseCredential,
|
|
342
493
|
BasicAuthCredential,
|
|
494
|
+
DEFAULT_MAX_RESPONSE_BYTES,
|
|
343
495
|
DEFAULT_RETRY_ATTEMPTS,
|
|
344
496
|
DEFAULT_RETRY_DELAY_MS,
|
|
497
|
+
DEFAULT_RETRY_POLICY,
|
|
345
498
|
DEFAULT_TIMEOUT_MS,
|
|
346
499
|
MANIFEST_VERSION,
|
|
500
|
+
MAX_RESPONSE_BYTES,
|
|
347
501
|
MAX_RETRY_ATTEMPTS,
|
|
348
502
|
MAX_TIMEOUT_MS,
|
|
349
503
|
NodeError,
|
|
350
504
|
OAuth2AuthCodeCredential,
|
|
351
505
|
OAuth2ClientCredentialsCredential,
|
|
506
|
+
RetryableError,
|
|
352
507
|
SimpleValueCredential,
|
|
508
|
+
backoffDelay,
|
|
353
509
|
buildManifest,
|
|
510
|
+
clampResponseBytes,
|
|
354
511
|
extractCredentialManifest,
|
|
355
512
|
extractCredentialManifests,
|
|
356
513
|
extractManifest,
|
|
357
514
|
extractManifests,
|
|
358
515
|
isNodeWithIteration,
|
|
359
516
|
isOAuthAuthorizeCredential,
|
|
517
|
+
maxBytesConfigField,
|
|
360
518
|
normalizeCredentialType,
|
|
361
519
|
normalizeLocalized,
|
|
520
|
+
parsePackageMeta,
|
|
521
|
+
readArrayBuffer,
|
|
522
|
+
readJsonOrText,
|
|
523
|
+
readText,
|
|
362
524
|
retryConfigFields,
|
|
363
525
|
safeFetch,
|
|
364
|
-
|
|
526
|
+
sleepWithSignal,
|
|
527
|
+
timeoutConfigField,
|
|
528
|
+
withRetry
|
|
365
529
|
};
|