@particle-academy/fancy-connector-core 0.0.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/index.cjs ADDED
@@ -0,0 +1,1356 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ AMBIGUOUS_REFUSAL: () => AMBIGUOUS_REFUSAL,
24
+ CONNECTOR_API_VERSION: () => CONNECTOR_API_VERSION,
25
+ CONNECTOR_MODES: () => CONNECTOR_MODES,
26
+ ConnectorAmbiguous: () => ConnectorAmbiguous,
27
+ ConnectorApiMismatch: () => ConnectorApiMismatch,
28
+ ConnectorAuthError: () => ConnectorAuthError,
29
+ ConnectorConfigError: () => ConnectorConfigError,
30
+ ConnectorError: () => ConnectorError,
31
+ ConnectorIdempotencyExpiredError: () => ConnectorIdempotencyExpiredError,
32
+ ConnectorModeError: () => ConnectorModeError,
33
+ ConnectorRateLimited: () => ConnectorRateLimited,
34
+ ConnectorRequestError: () => ConnectorRequestError,
35
+ ConnectorTransient: () => ConnectorTransient,
36
+ ConnectorUnreachable: () => ConnectorUnreachable,
37
+ DEFAULT_IDEMPOTENCY_WINDOW_SECONDS: () => DEFAULT_IDEMPOTENCY_WINDOW_SECONDS,
38
+ DEFAULT_RETRY: () => DEFAULT_RETRY,
39
+ FAKE_EPOCH: () => FAKE_EPOCH,
40
+ MAX_IDEMPOTENCY_KEY_LENGTH: () => MAX_IDEMPOTENCY_KEY_LENGTH,
41
+ NO_IDEMPOTENCY_KEY_WARNING: () => NO_IDEMPOTENCY_KEY_WARNING,
42
+ RENDERER_VERSION: () => RENDERER_VERSION,
43
+ REVIEW_STALE_DAYS: () => REVIEW_STALE_DAYS,
44
+ SUPPORTED_CONNECTOR_API: () => SUPPORTED_CONNECTOR_API,
45
+ assertConnectorApi: () => assertConnectorApi,
46
+ byteRangeOf: () => byteRangeOf,
47
+ callConnector: () => callConnector,
48
+ capabilityProblems: () => capabilityProblems,
49
+ checkAgainstOpenApi: () => checkAgainstOpenApi,
50
+ checkAgainstRecordedShape: () => checkAgainstRecordedShape,
51
+ classifyError: () => classifyError,
52
+ classifyHttp: () => classifyHttp,
53
+ classifyStatus: () => classifyStatus,
54
+ classifyThrown: () => classifyThrown,
55
+ compareShape: () => compareShape,
56
+ connectionsFor: () => connectionsFor,
57
+ constantTimeEquals: () => constantTimeEquals,
58
+ deliver: () => deliver,
59
+ fakeRequest: () => fakeRequest,
60
+ fakeValues: () => fakeValues,
61
+ getConnectionHost: () => getConnectionHost,
62
+ graphemes: () => graphemes,
63
+ header: () => header,
64
+ hmac: () => hmac,
65
+ httpFailure: () => httpFailure,
66
+ idempotencyKeyFor: () => idempotencyKeyFor,
67
+ isEmptyPayload: () => isEmptyPayload,
68
+ isRemote: () => isRemote,
69
+ isUnconditionallyRetryable: () => isUnconditionallyRetryable,
70
+ linkRanges: () => linkRanges,
71
+ measure: () => measure,
72
+ payloadHash: () => payloadHash,
73
+ postChain: () => postChain,
74
+ probe: () => probe,
75
+ registerConnectionHost: () => registerConnectionHost,
76
+ registerTransport: () => registerTransport,
77
+ render: () => render,
78
+ reported: () => reported,
79
+ resetRateState: () => resetRateState,
80
+ resolveConnection: () => resolveConnection,
81
+ resolveConnectorMode: () => resolveConnectorMode,
82
+ respectRate: () => respectRate,
83
+ runIdentityFrom: () => runIdentityFrom,
84
+ runKeyFrom: () => runKeyFrom,
85
+ runProbes: () => runProbes,
86
+ satisfiesMinimum: () => satisfiesMinimum,
87
+ seedFrom: () => seedFrom,
88
+ shapeOf: () => shapeOf,
89
+ shouldRetry: () => shouldRetry,
90
+ sliceByteRange: () => sliceByteRange,
91
+ splitToFit: () => splitToFit,
92
+ triggerEvent: () => triggerEvent,
93
+ unchecked: () => unchecked,
94
+ utf8Length: () => utf8Length,
95
+ verifyDelivery: () => verifyDelivery,
96
+ verifyHmac: () => verifyHmac
97
+ });
98
+ module.exports = __toCommonJS(src_exports);
99
+
100
+ // src/chain.ts
101
+ async function postChain(segments, answering, post) {
102
+ const posted = [];
103
+ let root = answering?.root;
104
+ let parent = answering?.parent;
105
+ for (const [index, text] of segments.entries()) {
106
+ let made;
107
+ try {
108
+ made = await post(text, root && parent ? { root, parent } : void 0, index);
109
+ } catch (error) {
110
+ return { posted, failed: { index, error } };
111
+ }
112
+ posted.push(made);
113
+ root = root ?? made;
114
+ parent = made;
115
+ }
116
+ return { posted };
117
+ }
118
+
119
+ // src/compat.ts
120
+ var CONNECTOR_API_VERSION = 1;
121
+ var SUPPORTED_CONNECTOR_API = [1];
122
+ var ConnectorApiMismatch = class extends Error {
123
+ connector;
124
+ declared;
125
+ constructor(message, connector, declared) {
126
+ super(message);
127
+ this.name = "ConnectorApiMismatch";
128
+ this.connector = connector;
129
+ this.declared = declared;
130
+ }
131
+ };
132
+ function assertConnectorApi(connectorId, declared) {
133
+ if (SUPPORTED_CONNECTOR_API.includes(declared)) return;
134
+ const supported = SUPPORTED_CONNECTOR_API.join(", ");
135
+ throw new ConnectorApiMismatch(
136
+ declared > CONNECTOR_API_VERSION ? `"${connectorId}" was written against connector API ${declared}, and this core implements ${supported}. The CONNECTOR is newer: upgrade @particle-academy/fancy-connector-core. Vendoring a connector ahead of the core it needs is the one direction that cannot be made to work by trying.` : `"${connectorId}" was written against connector API ${declared}, which is older than anything this core still runs (${supported}). The CONNECTOR is behind: re-vendor it \u2014 \`npx fancy-cli@latest add connector ${connectorId}\` \u2014 which fetches the copy written for this core. Nothing is adapted automatically, because guessing what a two-version-old connector meant is how a connector quietly starts doing something else.`,
137
+ connectorId,
138
+ declared
139
+ );
140
+ }
141
+ function satisfiesMinimum(coreVersion, minimum) {
142
+ const parts = (value) => value.split(".").slice(0, 3).map((segment) => Number.parseInt(segment, 10) || 0);
143
+ const [a = 0, b = 0, c = 0] = parts(coreVersion);
144
+ const [x = 0, y = 0, z = 0] = parts(minimum);
145
+ if (a !== x) return a > x;
146
+ if (b !== y) return b > y;
147
+ return c >= z;
148
+ }
149
+
150
+ // src/delivery.ts
151
+ var UNREACHABLE_CODES = /* @__PURE__ */ new Set([
152
+ "ECONNREFUSED",
153
+ "ENOTFOUND",
154
+ "EAI_AGAIN",
155
+ "EHOSTUNREACH",
156
+ "ENETUNREACH",
157
+ "ECONNRESET",
158
+ "EPIPE",
159
+ "ERR_SOCKET_CONNECTION_TIMEOUT"
160
+ ]);
161
+ var AMBIGUOUS_CODES = /* @__PURE__ */ new Set([
162
+ "ETIMEDOUT",
163
+ "UND_ERR_HEADERS_TIMEOUT",
164
+ "UND_ERR_BODY_TIMEOUT",
165
+ "UND_ERR_CONNECT_TIMEOUT"
166
+ ]);
167
+ var AMBIGUOUS_NAMES = /* @__PURE__ */ new Set(["AbortError", "TimeoutError"]);
168
+ function classifyError(error) {
169
+ const detail = error instanceof Error ? error.message : String(error);
170
+ const code = error?.code ?? error?.cause?.code ?? "";
171
+ const name = error instanceof Error ? error.name : "";
172
+ if (UNREACHABLE_CODES.has(code)) return { kind: "unreachable", detail: `${code}: ${detail}` };
173
+ if (AMBIGUOUS_NAMES.has(name) || AMBIGUOUS_CODES.has(code)) {
174
+ return { kind: "ambiguous", detail: `${name || code}: ${detail}` };
175
+ }
176
+ return { kind: "ambiguous", detail };
177
+ }
178
+ function classifyStatus(status, body = "", retryAfterHeader) {
179
+ const detail = `${status}${body ? `: ${body.slice(0, 200)}` : ""}`;
180
+ const seconds = retryAfterHeader === void 0 || retryAfterHeader === null ? NaN : Number(retryAfterHeader);
181
+ if (status === 429 || status >= 500) {
182
+ return {
183
+ kind: "refused-explicitly",
184
+ detail,
185
+ ...Number.isFinite(seconds) ? { retryAfter: seconds } : {}
186
+ };
187
+ }
188
+ return { kind: "rejected", detail };
189
+ }
190
+ var DEFAULT_RETRY = {
191
+ attempts: 3,
192
+ baseDelayMs: 500,
193
+ maxDelayMs: 8e3,
194
+ idempotent: false
195
+ };
196
+ function shouldRetry(kind, policy) {
197
+ if (kind === "rejected") return false;
198
+ if (kind === "unreachable" || kind === "refused-explicitly") return true;
199
+ return policy.idempotent;
200
+ }
201
+ function isUnconditionallyRetryable(kind) {
202
+ return kind === "unreachable" || kind === "refused-explicitly";
203
+ }
204
+ var AMBIGUOUS_REFUSAL = "The request may or may not have gone through, and this connector offers no way to repeat it harmlessly. Refusing to try again \u2014 a duplicate is worse than a call that needs doing by hand. Check the provider before re-running.";
205
+ async function deliver(send, policy = DEFAULT_RETRY, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
206
+ const attempts = [];
207
+ const budget = Math.max(1, policy.attempts);
208
+ for (let attempt = 1; attempt <= budget; attempt++) {
209
+ try {
210
+ const value = await send(attempt);
211
+ return { ok: true, value, attempts };
212
+ } catch (error) {
213
+ const classified = error.classified ?? classifyError(error);
214
+ if (!shouldRetry(classified.kind, policy)) {
215
+ attempts.push({ attempt, kind: classified.kind, detail: classified.detail });
216
+ return {
217
+ ok: false,
218
+ attempts,
219
+ kind: classified.kind,
220
+ gaveUp: classified.kind === "ambiguous" ? `${AMBIGUOUS_REFUSAL} (${classified.detail})` : classified.detail
221
+ };
222
+ }
223
+ if (attempt >= budget) {
224
+ attempts.push({ attempt, kind: classified.kind, detail: classified.detail });
225
+ return {
226
+ ok: false,
227
+ attempts,
228
+ kind: classified.kind,
229
+ gaveUp: `Gave up after ${attempt} attempts. ${classified.detail}`
230
+ };
231
+ }
232
+ const backoff = Math.min(policy.baseDelayMs * 2 ** (attempt - 1), policy.maxDelayMs);
233
+ const waitedMs = classified.retryAfter ? Math.max(backoff, classified.retryAfter * 1e3) : backoff;
234
+ attempts.push({ attempt, kind: classified.kind, detail: classified.detail, waitedMs });
235
+ await sleep(waitedMs);
236
+ }
237
+ }
238
+ return { ok: false, attempts, gaveUp: "Exhausted every attempt." };
239
+ }
240
+ var lastCallAt = /* @__PURE__ */ new Map();
241
+ async function respectRate(channel, minIntervalMs, now = () => Date.now(), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
242
+ if (minIntervalMs <= 0) return { waitedMs: 0 };
243
+ const previous = lastCallAt.get(channel);
244
+ const at = now();
245
+ const waitedMs = previous === void 0 ? 0 : Math.max(0, previous + minIntervalMs - at);
246
+ if (waitedMs > 0) await sleep(waitedMs);
247
+ lastCallAt.set(channel, at + waitedMs);
248
+ return { waitedMs };
249
+ }
250
+ function resetRateState() {
251
+ lastCallAt.clear();
252
+ }
253
+
254
+ // src/errors.ts
255
+ var ConnectorError = class extends Error {
256
+ service;
257
+ operation;
258
+ status;
259
+ providerCode;
260
+ /**
261
+ * What kind of failure this is. The primitive every retry decision reads.
262
+ *
263
+ * Defaults to `ambiguous` on the base class deliberately: an unclassified
264
+ * failure must not be assumed safe.
265
+ */
266
+ kind = "ambiguous";
267
+ constructor(message, ctx) {
268
+ super(message);
269
+ this.name = new.target.name;
270
+ this.service = ctx.service;
271
+ this.operation = ctx.operation;
272
+ this.status = ctx.status;
273
+ this.providerCode = ctx.providerCode;
274
+ }
275
+ /**
276
+ * Safe to retry **without knowing anything about the connector**.
277
+ *
278
+ * Ambiguous failures answer `false` here even though they may be retryable on
279
+ * an idempotent connector — ask `shouldRetry(error.kind, { idempotent })` for
280
+ * that. This getter is the conservative half on purpose.
281
+ */
282
+ get retryable() {
283
+ return isUnconditionallyRetryable(this.kind);
284
+ }
285
+ /** The classification, in the shape `deliver()` reads off a thrown error. */
286
+ get classified() {
287
+ return { kind: this.kind, detail: this.message };
288
+ }
289
+ };
290
+ var ConnectorConfigError = class extends ConnectorError {
291
+ kind = "rejected";
292
+ };
293
+ var ConnectorAuthError = class extends ConnectorError {
294
+ kind = "rejected";
295
+ };
296
+ var ConnectorRateLimited = class extends ConnectorError {
297
+ kind = "refused-explicitly";
298
+ /** Seconds to wait, when the provider said. */
299
+ retryAfter;
300
+ constructor(message, ctx) {
301
+ super(message, ctx);
302
+ this.retryAfter = ctx.retryAfter;
303
+ }
304
+ get classified() {
305
+ return {
306
+ kind: this.kind,
307
+ detail: this.message,
308
+ ...this.retryAfter === void 0 ? {} : { retryAfter: this.retryAfter }
309
+ };
310
+ }
311
+ };
312
+ var ConnectorTransient = class extends ConnectorError {
313
+ kind = "refused-explicitly";
314
+ };
315
+ var ConnectorUnreachable = class extends ConnectorError {
316
+ kind = "unreachable";
317
+ };
318
+ var ConnectorAmbiguous = class extends ConnectorError {
319
+ kind = "ambiguous";
320
+ };
321
+ var ConnectorRequestError = class extends ConnectorError {
322
+ kind = "rejected";
323
+ };
324
+ function classifyHttp(status, ctx, body, retryAfter) {
325
+ const detail = body.trim() === "" ? "" : ` \u2014 ${truncate(body, 400)}`;
326
+ const where = `${ctx.service}.${ctx.operation}`;
327
+ if (status === 429) {
328
+ return new ConnectorRateLimited(`${where}: rate limited by the provider${detail}`, {
329
+ ...ctx,
330
+ status,
331
+ ...retryAfter === void 0 ? {} : { retryAfter }
332
+ });
333
+ }
334
+ if (status === 401 || status === 403) {
335
+ return new ConnectorAuthError(
336
+ `${where}: the provider rejected the credential (${status})${detail}. Check the credentials and that they match the mode you are running in \u2014 a live key in sandbox, or the reverse, fails exactly like this.`,
337
+ { ...ctx, status }
338
+ );
339
+ }
340
+ if (status >= 500) {
341
+ return new ConnectorTransient(`${where}: provider returned ${status}${detail}`, { ...ctx, status });
342
+ }
343
+ return new ConnectorRequestError(`${where}: request rejected with ${status}${detail}`, { ...ctx, status });
344
+ }
345
+ function classifyThrown(cause, ctx) {
346
+ const classified = classifyError(cause);
347
+ const message = `${ctx.service}.${ctx.operation}: ${classified.detail}`;
348
+ return classified.kind === "unreachable" ? new ConnectorUnreachable(message, ctx) : new ConnectorAmbiguous(message, ctx);
349
+ }
350
+ function httpFailure(status, body, retryAfter) {
351
+ const classified = classifyStatus(status, body, retryAfter);
352
+ const error = new Error(classified.detail);
353
+ error.classified = classified;
354
+ return error;
355
+ }
356
+ function truncate(value, max) {
357
+ return value.length <= max ? value : `${value.slice(0, max)}\u2026`;
358
+ }
359
+
360
+ // src/faker.ts
361
+ var FAKE_EPOCH = "2026-01-01T00:00:00.000Z";
362
+ function seedFrom(...parts) {
363
+ const text = parts.map((part) => typeof part === "string" ? part : stableJson(part)).join("|");
364
+ let hash = 2166136261;
365
+ for (let i = 0; i < text.length; i++) {
366
+ hash ^= text.charCodeAt(i);
367
+ hash = Math.imul(hash, 16777619) >>> 0;
368
+ }
369
+ return hash >>> 0;
370
+ }
371
+ function stableJson(value) {
372
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
373
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
374
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableJson(v)}`);
375
+ return `{${entries.join(",")}}`;
376
+ }
377
+ function fakeValues(seed) {
378
+ let state = seed || 2654435769;
379
+ const next = () => {
380
+ state ^= state << 13;
381
+ state >>>= 0;
382
+ state ^= state >>> 17;
383
+ state ^= state << 5;
384
+ state >>>= 0;
385
+ return state;
386
+ };
387
+ const hex = (length) => {
388
+ let out = "";
389
+ while (out.length < length) out += next().toString(16).padStart(8, "0");
390
+ return out.slice(0, length);
391
+ };
392
+ return {
393
+ hex,
394
+ id: (prefix) => `${prefix}_fake_${hex(12)}`,
395
+ int: (min, max) => min + next() % Math.max(1, max - min + 1),
396
+ pick: (options) => options[next() % options.length],
397
+ timestamp: (offsetSeconds = 0) => new Date(Date.parse(FAKE_EPOCH) + offsetSeconds * 1e3).toISOString()
398
+ };
399
+ }
400
+ function fakeRequest(service, operation, config, input) {
401
+ return { config, input, fake: fakeValues(seedFrom(service, operation, config)) };
402
+ }
403
+
404
+ // src/mode.ts
405
+ var CONNECTOR_MODES = ["fake", "sandbox", "live"];
406
+ function resolveConnectorMode(inputs) {
407
+ const { requested, connectionMode, sandbox, hasSandboxCredentials, environment } = inputs;
408
+ if (requested && requested !== "auto") {
409
+ if (requested === "sandbox" && sandbox === "none") {
410
+ throw new ConnectorModeError(
411
+ `This connector's provider has no sandbox estate, so "sandbox" cannot be honoured. Use "fake" to develop without credentials, or "live" to talk to the provider.`
412
+ );
413
+ }
414
+ return requested;
415
+ }
416
+ if (connectionMode) return connectionMode;
417
+ if (environment.production) return "live";
418
+ return sandbox !== "none" && hasSandboxCredentials ? "sandbox" : "fake";
419
+ }
420
+ var ConnectorModeError = class extends Error {
421
+ constructor(message) {
422
+ super(message);
423
+ this.name = "ConnectorModeError";
424
+ }
425
+ };
426
+ function isRemote(mode) {
427
+ return mode !== "fake";
428
+ }
429
+
430
+ // src/connection.ts
431
+ var host = null;
432
+ function registerConnectionHost(next) {
433
+ host = next;
434
+ return () => {
435
+ if (host === next) host = null;
436
+ };
437
+ }
438
+ function getConnectionHost() {
439
+ return host;
440
+ }
441
+ function connectionsFor(service) {
442
+ const current = getConnectionHost();
443
+ if (!current) return [];
444
+ return Object.entries(current.connections).filter(([, spec]) => spec.service === service).map(([id, spec]) => ({ id, label: spec.label ?? id }));
445
+ }
446
+ function resolveConnection(options) {
447
+ const { service, operation, sandbox } = options;
448
+ const requestedMode = options.requested ?? "auto";
449
+ if (requestedMode === "sandbox" && sandbox === "none") {
450
+ throw new ConnectorModeError(
451
+ `${service} has no sandbox estate, so "sandbox" cannot be honoured. Use "fake" to develop without credentials, or "live" to talk to the provider.`
452
+ );
453
+ }
454
+ const id = options.connectionId?.trim() || service;
455
+ if (options.credentials) {
456
+ if (requestedMode === "fake") return { id, service, mode: "fake", credentials: {} };
457
+ const mode2 = requestedMode === "auto" ? "live" : requestedMode;
458
+ const missing2 = (options.requires ?? []).filter((key) => !options.credentials?.[key]);
459
+ if (missing2.length > 0) {
460
+ throw new ConnectorConfigError(
461
+ `${service}.${operation} was given credentials with no ${missing2.join(", ")}. Nothing is inferred from an incomplete credential set \u2014 a partial one is a configuration mistake, and quietly falling back to another source is how the wrong account gets written to.`,
462
+ { service, operation }
463
+ );
464
+ }
465
+ const explicitBase = options.baseUrls?.[mode2];
466
+ return {
467
+ id,
468
+ service,
469
+ mode: mode2,
470
+ credentials: options.credentials,
471
+ ...explicitBase ? { baseUrl: explicitBase } : {}
472
+ };
473
+ }
474
+ const current = getConnectionHost();
475
+ const spec = current?.connections[id];
476
+ if (spec && spec.service !== service) {
477
+ throw new ConnectorConfigError(
478
+ `connection "${id}" is configured for the "${spec.service}" service, but this node needs "${service}".`,
479
+ { service, operation }
480
+ );
481
+ }
482
+ if (!current || !spec) {
483
+ if (requestedMode !== "auto" && requestedMode !== "fake") {
484
+ throw new ConnectorConfigError(
485
+ `no "${id}" connection is registered, so ${service}.${operation} cannot run in "${requestedMode}" mode. Call registerConnectionHost({ connections: { ${id}: { service: "${service}", live: { \u2026 } } } }) \u2014 the node has no credentials of its own and must not invent any. Set the node's mode to "fake" to develop without one.`,
486
+ { service, operation }
487
+ );
488
+ }
489
+ return { id, service, mode: "fake", credentials: {} };
490
+ }
491
+ const sandboxCreds = spec.sandbox ?? {};
492
+ const mode = resolveConnectorMode({
493
+ requested: requestedMode,
494
+ connectionMode: spec.mode ?? null,
495
+ sandbox,
496
+ hasSandboxCredentials: hasAll(sandboxCreds, options.requires ?? []),
497
+ environment: current.environment
498
+ });
499
+ if (mode === "fake") {
500
+ return { id, service, mode, credentials: {} };
501
+ }
502
+ const credentials = (mode === "sandbox" ? spec.sandbox : spec.live) ?? {};
503
+ const missing = (options.requires ?? []).filter((key) => !credentials[key]);
504
+ if (missing.length > 0) {
505
+ throw new ConnectorConfigError(
506
+ `connection "${id}" has no ${missing.join(", ")} for ${mode} mode. Set connections.${id}.${mode}.${missing[0]}, or run this node in "fake" mode.`,
507
+ { service, operation }
508
+ );
509
+ }
510
+ const baseUrl = spec.baseUrl?.[mode] ?? options.baseUrls?.[mode];
511
+ return { id, service, mode, credentials, ...baseUrl ? { baseUrl } : {} };
512
+ }
513
+ function hasAll(creds, keys) {
514
+ return keys.length > 0 && keys.every((key) => Boolean(creds[key]));
515
+ }
516
+
517
+ // src/client.ts
518
+ var registeredTransport = null;
519
+ function registerTransport(next) {
520
+ registeredTransport = next;
521
+ }
522
+ async function callConnector(service, options) {
523
+ const config = options.config ?? {};
524
+ const connection = resolveConnection({
525
+ service: service.service,
526
+ operation: options.operation,
527
+ connectionId: options.connectionId ?? readString(config, "connection"),
528
+ requested: options.mode ?? readString(config, "mode"),
529
+ credentials: options.credentials ?? null,
530
+ sandbox: service.sandbox,
531
+ baseUrls: service.baseUrls,
532
+ requires: service.requires
533
+ });
534
+ if (connection.mode === "fake") {
535
+ return {
536
+ data: service.faker(
537
+ options.operation,
538
+ fakeRequest(service.service, options.operation, config, options.input)
539
+ ),
540
+ mode: "fake",
541
+ connection: connection.id
542
+ };
543
+ }
544
+ const ctx = { service: service.service, operation: options.operation };
545
+ const prepared = await prepare(service, connection, options);
546
+ const send = options.transport ?? registeredTransport ?? fetchTransport;
547
+ const outcome = await deliver(
548
+ async () => {
549
+ let response;
550
+ try {
551
+ response = await send(prepared);
552
+ } catch (cause) {
553
+ throw classifyThrown(cause, ctx);
554
+ }
555
+ if (response.status < 400) {
556
+ return response.body.trim() === "" ? null : parseJson(response.body, ctx);
557
+ }
558
+ throw classifyHttp(response.status, ctx, response.body, readRetryAfter(response.headers));
559
+ },
560
+ {
561
+ ...DEFAULT_RETRY,
562
+ attempts: options.attempts ?? DEFAULT_RETRY.attempts,
563
+ idempotent: options.idempotent ?? false
564
+ }
565
+ );
566
+ if (options.onAttempt) {
567
+ for (const attempt of outcome.attempts) options.onAttempt(attempt);
568
+ }
569
+ if (!outcome.ok) {
570
+ throw failureFrom(outcome, ctx, options.idempotent ?? false);
571
+ }
572
+ return { data: outcome.value, mode: connection.mode, connection: connection.id };
573
+ }
574
+ function failureFrom(outcome, ctx, idempotent) {
575
+ const last = outcome.attempts.at(-1);
576
+ const kind = outcome.kind ?? last?.kind ?? "ambiguous";
577
+ const error = new ConnectorError(outcome.gaveUp ?? `${ctx.service}.${ctx.operation} failed.`, ctx);
578
+ Object.defineProperty(error, "kind", { value: kind, enumerable: false });
579
+ Object.defineProperty(error, "attempts", { value: outcome.attempts, enumerable: false });
580
+ Object.defineProperty(error, "idempotent", { value: idempotent, enumerable: false });
581
+ return error;
582
+ }
583
+ async function prepare(service, connection, options) {
584
+ const base = connection.baseUrl ?? service.baseUrls[connection.mode];
585
+ if (!base) {
586
+ throw new ConnectorConfigError(
587
+ `${service.service}: no base URL for "${connection.mode}" mode. The service descriptor must declare one per mode it supports.`,
588
+ { service: service.service, operation: options.operation }
589
+ );
590
+ }
591
+ const { method, path, query, headers, json, form } = options.request;
592
+ const url = new URL(path.replace(/^\/+/, ""), base.endsWith("/") ? base : `${base}/`);
593
+ for (const [key, value] of Object.entries(query ?? {})) {
594
+ if (value !== void 0) url.searchParams.set(key, String(value));
595
+ }
596
+ const prepared = {
597
+ method,
598
+ url: url.toString(),
599
+ headers: { Accept: "application/json", ...headers }
600
+ };
601
+ if (form) {
602
+ prepared.headers["Content-Type"] = "application/x-www-form-urlencoded";
603
+ prepared.body = encodeForm(form);
604
+ } else if (json !== void 0) {
605
+ prepared.headers["Content-Type"] = "application/json";
606
+ prepared.body = JSON.stringify(json);
607
+ }
608
+ if (options.idempotencyKey && service.idempotencyHeader) {
609
+ prepared.headers[service.idempotencyHeader] = options.idempotencyKey;
610
+ }
611
+ await service.authorize(connection.credentials, prepared, connection.mode);
612
+ return prepared;
613
+ }
614
+ function encodeForm(form) {
615
+ const pairs = [];
616
+ const walk = (prefix, value) => {
617
+ if (value === void 0 || value === null) return;
618
+ if (Array.isArray(value)) {
619
+ value.forEach((item, index) => walk(`${prefix}[${index}]`, item));
620
+ return;
621
+ }
622
+ if (typeof value === "object") {
623
+ for (const [key, nested] of Object.entries(value)) {
624
+ walk(prefix === "" ? key : `${prefix}[${key}]`, nested);
625
+ }
626
+ return;
627
+ }
628
+ pairs.push(`${encodeURIComponent(prefix)}=${encodeURIComponent(String(value))}`);
629
+ };
630
+ walk("", form);
631
+ return pairs.join("&");
632
+ }
633
+ var fetchTransport = async (request) => {
634
+ const globalFetch = globalThis.fetch;
635
+ if (!globalFetch) {
636
+ throw new Error(
637
+ "no global fetch in this runtime \u2014 call registerTransport() with one before running a connector remotely."
638
+ );
639
+ }
640
+ const response = await globalFetch(request.url, {
641
+ method: request.method,
642
+ headers: request.headers,
643
+ ...request.body === void 0 ? {} : { body: request.body }
644
+ });
645
+ const headers = {};
646
+ response.headers.forEach((value, key) => {
647
+ headers[key.toLowerCase()] = value;
648
+ });
649
+ return { status: response.status, headers, body: await response.text() };
650
+ };
651
+ function parseJson(body, ctx) {
652
+ try {
653
+ return JSON.parse(body);
654
+ } catch {
655
+ const error = new ConnectorError(
656
+ `${ctx.service}.${ctx.operation}: the provider returned a body that is not JSON.`,
657
+ ctx
658
+ );
659
+ Object.defineProperty(error, "kind", { value: "rejected", enumerable: false });
660
+ throw error;
661
+ }
662
+ }
663
+ function readRetryAfter(headers) {
664
+ const raw = headers["retry-after"];
665
+ if (!raw) return void 0;
666
+ const seconds = Number(raw);
667
+ return Number.isFinite(seconds) ? seconds : void 0;
668
+ }
669
+ function readString(config, key) {
670
+ const value = config[key];
671
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
672
+ }
673
+
674
+ // src/drift.ts
675
+ function resolveRef(document, node, depth = 0) {
676
+ if (depth > 8 || node === null || typeof node !== "object") return node;
677
+ const ref = node.$ref;
678
+ if (typeof ref !== "string") return node;
679
+ if (!ref.startsWith("#/")) return node;
680
+ let current = document;
681
+ for (const segment of ref.slice(2).split("/")) {
682
+ if (current === null || typeof current !== "object") return node;
683
+ current = current[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
684
+ }
685
+ return resolveRef(document, current, depth + 1);
686
+ }
687
+ function propertiesOf(document, schema, depth = 0) {
688
+ const names = /* @__PURE__ */ new Set();
689
+ const resolved = resolveRef(document, schema, depth);
690
+ if (!resolved || typeof resolved !== "object" || depth > 6) return names;
691
+ const properties = resolveRef(document, resolved.properties, depth + 1);
692
+ if (properties && typeof properties === "object") {
693
+ for (const key of Object.keys(properties)) names.add(key);
694
+ }
695
+ for (const key of ["items", "additionalProperties"]) {
696
+ const nested = resolved[key];
697
+ if (nested) for (const name of propertiesOf(document, nested, depth + 1)) names.add(name);
698
+ }
699
+ for (const key of ["allOf", "oneOf", "anyOf"]) {
700
+ const list = resolved[key];
701
+ if (Array.isArray(list)) {
702
+ for (const entry of list) for (const name of propertiesOf(document, entry, depth + 1)) names.add(name);
703
+ }
704
+ }
705
+ return names;
706
+ }
707
+ function requestSchemaOf(document, operation) {
708
+ const body = resolveRef(document, operation.requestBody);
709
+ if (!body?.content) return void 0;
710
+ for (const media of Object.values(body.content)) {
711
+ if (media?.schema) return media.schema;
712
+ }
713
+ return void 0;
714
+ }
715
+ function responseSchemaOf(document, operation) {
716
+ const responses = resolveRef(document, operation.responses);
717
+ if (!responses) return void 0;
718
+ for (const status of Object.keys(responses)) {
719
+ if (!/^2\d\d$/.test(status) && status !== "default") continue;
720
+ const response = resolveRef(document, responses[status]);
721
+ if (!response?.content) continue;
722
+ for (const media of Object.values(response.content)) {
723
+ if (media?.schema) return media.schema;
724
+ }
725
+ }
726
+ return void 0;
727
+ }
728
+ function checkAgainstOpenApi(contract, document) {
729
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
730
+ const findings = [];
731
+ if (!document || typeof document !== "object" || !document.paths) {
732
+ return {
733
+ connector: contract.connector,
734
+ checkedAt,
735
+ outcome: "unchecked",
736
+ method: "openapi",
737
+ findings: [
738
+ {
739
+ kind: "unreadable-spec",
740
+ detail: "the document has no `paths`, so it is not an OpenAPI description we can read. Reported as UNCHECKED rather than clean \u2014 a checker that cannot see is not a checker that saw nothing wrong."
741
+ }
742
+ ]
743
+ };
744
+ }
745
+ const spec = document;
746
+ for (const operation of contract.operations) {
747
+ const path = spec.paths?.[operation.path];
748
+ if (!path) {
749
+ findings.push({
750
+ kind: "missing-operation",
751
+ operation: operation.operation,
752
+ detail: `${operation.method} ${operation.path} is not in the spec at all. The path moved or was removed.`
753
+ });
754
+ continue;
755
+ }
756
+ const method = path[operation.method.toLowerCase()];
757
+ if (!method) {
758
+ findings.push({
759
+ kind: "missing-operation",
760
+ operation: operation.operation,
761
+ detail: `${operation.path} exists but no longer accepts ${operation.method}.`
762
+ });
763
+ continue;
764
+ }
765
+ if (operation.sends?.length) {
766
+ const accepted = propertiesOf(spec, requestSchemaOf(spec, method));
767
+ if (accepted.size > 0) {
768
+ for (const field of operation.sends) {
769
+ if (!accepted.has(field)) {
770
+ findings.push({
771
+ kind: "missing-request-field",
772
+ operation: operation.operation,
773
+ detail: `sends \`${field}\`, which the spec no longer lists on ${operation.method} ${operation.path}.`
774
+ });
775
+ }
776
+ }
777
+ }
778
+ }
779
+ if (operation.reads?.length) {
780
+ const returned = propertiesOf(spec, responseSchemaOf(spec, method));
781
+ if (returned.size > 0) {
782
+ for (const field of operation.reads) {
783
+ if (!returned.has(field)) {
784
+ findings.push({
785
+ kind: "missing-response-field",
786
+ operation: operation.operation,
787
+ detail: `reads \`${field}\`, which the spec no longer returns from ${operation.method} ${operation.path}. This is the silent kind: the code keeps running and produces nothing.`
788
+ });
789
+ }
790
+ }
791
+ }
792
+ }
793
+ }
794
+ findings.push(...staleReviewFindings(contract, checkedAt));
795
+ return {
796
+ connector: contract.connector,
797
+ checkedAt,
798
+ outcome: findings.some((finding) => finding.kind !== "stale-review") ? "drifted" : "clean",
799
+ method: "openapi",
800
+ findings
801
+ };
802
+ }
803
+ function shapeOf(value, prefix = "", depth = 0) {
804
+ if (depth > 6 || value === null || typeof value !== "object") return prefix ? [prefix] : [];
805
+ if (Array.isArray(value)) {
806
+ return value.length === 0 ? [`${prefix}[]`] : shapeOf(value[0], `${prefix}[]`, depth + 1);
807
+ }
808
+ const names = [];
809
+ for (const [key, nested] of Object.entries(value)) {
810
+ const path = prefix ? `${prefix}.${key}` : key;
811
+ names.push(...shapeOf(nested, path, depth + 1));
812
+ }
813
+ return names.sort();
814
+ }
815
+ function checkAgainstRecordedShape(contract, operation, liveResponse) {
816
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
817
+ const declared = contract.operations.find((entry) => entry.operation === operation);
818
+ const present = new Set(shapeOf(liveResponse));
819
+ const findings = [];
820
+ for (const field of declared?.reads ?? []) {
821
+ const found = [...present].some((name) => name === field || name.replace(/\[\]/g, "") === field);
822
+ if (!found) {
823
+ findings.push({
824
+ kind: "missing-response-field",
825
+ operation,
826
+ detail: `reads \`${field}\`, which the live response no longer carries. Present instead: ${[...present].slice(0, 20).join(", ")}${present.size > 20 ? ", \u2026" : ""}`
827
+ });
828
+ }
829
+ }
830
+ findings.push(...staleReviewFindings(contract, checkedAt));
831
+ return {
832
+ connector: contract.connector,
833
+ checkedAt,
834
+ outcome: findings.some((finding) => finding.kind !== "stale-review") ? "drifted" : "clean",
835
+ method: "recorded-shape",
836
+ findings
837
+ };
838
+ }
839
+ var REVIEW_STALE_DAYS = 180;
840
+ function staleReviewFindings(contract, checkedAt) {
841
+ const reviewed = Date.parse(contract.reviewedOn);
842
+ if (!Number.isFinite(reviewed)) {
843
+ return [{ kind: "stale-review", detail: `reviewedOn "${contract.reviewedOn}" is not a date.` }];
844
+ }
845
+ const days = Math.floor((Date.parse(checkedAt) - reviewed) / 864e5);
846
+ return days > REVIEW_STALE_DAYS ? [
847
+ {
848
+ kind: "stale-review",
849
+ detail: `nobody has read this provider's documentation in ${days} days. Not drift \u2014 an absence of looking, which is the state a silent break grows in.`
850
+ }
851
+ ] : [];
852
+ }
853
+ function unchecked(contract) {
854
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
855
+ return {
856
+ connector: contract.connector,
857
+ checkedAt,
858
+ outcome: "unchecked",
859
+ method: "none",
860
+ findings: [
861
+ {
862
+ kind: "unreadable-spec",
863
+ detail: contract.spec.kind === "none" ? contract.spec.note : "no spec document was supplied to the checker, so nothing was compared."
864
+ },
865
+ ...staleReviewFindings(contract, checkedAt)
866
+ ]
867
+ };
868
+ }
869
+
870
+ // src/idempotency.ts
871
+ var DEFAULT_IDEMPOTENCY_WINDOW_SECONDS = 24 * 60 * 60;
872
+ var MAX_IDEMPOTENCY_KEY_LENGTH = 255;
873
+ var ConnectorIdempotencyExpiredError = class extends ConnectorConfigError {
874
+ };
875
+ function runIdentityFrom(ctx) {
876
+ const bag = ctx;
877
+ return bag?.run ?? null;
878
+ }
879
+ function runKeyFrom(ctx) {
880
+ const identity = runIdentityFrom(ctx);
881
+ if (identity) return identity.runKey;
882
+ const bag = ctx;
883
+ const seeded = bag?.inputs?.__runKey;
884
+ return typeof seeded === "string" && seeded.trim() !== "" ? seeded.trim() : null;
885
+ }
886
+ function idempotencyKeyFor(ctx, nodeId, options = {}) {
887
+ const identity = runIdentityFrom(ctx);
888
+ const windowSeconds = options.windowSeconds === void 0 ? DEFAULT_IDEMPOTENCY_WINDOW_SECONDS : options.windowSeconds;
889
+ if (!identity) {
890
+ const runKey = runKeyFrom(ctx);
891
+ return runKey === null ? null : fit(`${runKey}:${nodeId}`);
892
+ }
893
+ if (!identity.isReplaySafe(windowSeconds, options.now ?? /* @__PURE__ */ new Date())) {
894
+ throw new ConnectorIdempotencyExpiredError(
895
+ `attempt ${identity.attempt} of this step began at ${identity.firstAttemptAt}, which is outside the provider's ${windowSeconds}s idempotency window. The provider has forgotten the original key, so resending it and sending a new one would BOTH write twice. Refusing, so a person can reconcile \u2014 check whether the first attempt landed before re-running this workflow.`,
896
+ {
897
+ service: options.context?.service ?? "",
898
+ operation: options.context?.operation ?? "",
899
+ ...options.context
900
+ }
901
+ );
902
+ }
903
+ return fit(identity.stepKey(nodeId, options.occurrence));
904
+ }
905
+ var NO_IDEMPOTENCY_KEY_WARNING = "no run identity available, so this write is being sent WITHOUT an idempotency key. A retry would repeat the effect rather than recover from it \u2014 the node is marked unsafe-to-replay, so a durable runner will give it one attempt. Durable runs supply an identity automatically; for a synchronous run, pass `run` in RunOptions.";
906
+ function fit(key) {
907
+ if (key.length <= MAX_IDEMPOTENCY_KEY_LENGTH) return key;
908
+ const digest = fnv1a(key);
909
+ const head = key.slice(0, MAX_IDEMPOTENCY_KEY_LENGTH - digest.length - 1);
910
+ return `${head}~${digest}`;
911
+ }
912
+ function fnv1a(value) {
913
+ let hash = 2166136261;
914
+ for (let i = 0; i < value.length; i++) {
915
+ hash ^= value.charCodeAt(i);
916
+ hash = hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)) >>> 0;
917
+ }
918
+ return hash.toString(16).padStart(8, "0");
919
+ }
920
+
921
+ // src/metrics.ts
922
+ function compareShape(connectorId, declared, produced) {
923
+ const declaredKeys = new Set((declared ?? []).map((metric) => metric.key));
924
+ const producedKeys = new Set(Object.keys(produced));
925
+ const undeclared = [...producedKeys].filter((key) => !declaredKeys.has(key)).sort();
926
+ const unproduced = [...declaredKeys].filter((key) => !producedKeys.has(key)).sort();
927
+ if (undeclared.length === 0 && unproduced.length === 0) return null;
928
+ return { connector: connectorId, undeclared, unproduced };
929
+ }
930
+ function capabilityProblems(connector) {
931
+ const problems = [];
932
+ if (connector.capabilities.metrics) {
933
+ if (!connector.metricShape || connector.metricShape.length === 0) {
934
+ problems.push(
935
+ `${connector.id} claims capabilities.metrics but declares no metricShape. A pull will therefore ask it, get nothing, and report nothing \u2014 which reads as "nobody engaged" rather than "not implemented".`
936
+ );
937
+ }
938
+ if (!connector.fetchMetrics) {
939
+ problems.push(`${connector.id} claims capabilities.metrics but has no fetchMetrics.`);
940
+ }
941
+ }
942
+ if (!connector.capabilities.metrics && connector.metricShape) {
943
+ problems.push(
944
+ `${connector.id} declares a metricShape but capabilities.metrics is false. One of the two is wrong, and the shape is the more believable half.`
945
+ );
946
+ }
947
+ if (connector.capabilities.feedback && !connector.fetchFeedback) {
948
+ problems.push(`${connector.id} claims capabilities.feedback but has no fetchFeedback.`);
949
+ }
950
+ if (!connector.capabilities.feedback && connector.fetchFeedback) {
951
+ problems.push(
952
+ `${connector.id} implements fetchFeedback but does not claim the capability, so nothing will ever call it.`
953
+ );
954
+ }
955
+ for (const metric of connector.metricShape ?? []) {
956
+ if (metric.means.trim().length < 10) {
957
+ problems.push(`${connector.id}.${metric.key} does not say what it means.`);
958
+ }
959
+ }
960
+ if (connector.delivery.idempotent && connector.delivery.why.trim().length < 10) {
961
+ problems.push(
962
+ `${connector.id} declares idempotent: true with no reason. That is the one claim whose failure is a public duplicate, so it has to name the mechanism rather than restate the flag.`
963
+ );
964
+ }
965
+ if (connector.delivery.minIntervalMs > 0 && !connector.delivery.citation && connector.delivery.rateSource === "documented") {
966
+ problems.push(
967
+ `${connector.id} calls its rate limit "documented" but cites nothing. A number nobody can source gets quoted as a platform fact; label it self-imposed or cite it.`
968
+ );
969
+ }
970
+ return problems;
971
+ }
972
+ function reported(values) {
973
+ const out = {};
974
+ for (const [key, value] of Object.entries(values)) {
975
+ if (typeof value === "number" && Number.isFinite(value)) out[key] = value;
976
+ }
977
+ return out;
978
+ }
979
+
980
+ // src/probe.ts
981
+ async function probe(spec) {
982
+ let response;
983
+ try {
984
+ response = await spec.request();
985
+ } catch (error) {
986
+ const classified2 = classifyError(error);
987
+ if (classified2.kind === "unreachable") {
988
+ return { connector: spec.connector, outcome: "skip", detail: `offline \u2014 ${classified2.detail}` };
989
+ }
990
+ return {
991
+ connector: spec.connector,
992
+ outcome: "fail",
993
+ detail: `the request failed before any status arrived \u2014 ${classified2.detail}`,
994
+ kind: classified2.kind
995
+ };
996
+ }
997
+ const { status, body } = response;
998
+ if (spec.authStatuses.includes(status)) {
999
+ return {
1000
+ connector: spec.connector,
1001
+ outcome: "pass",
1002
+ status,
1003
+ detail: `${status} \u2014 the provider refused an invalid credential, which proves host, path, method and error handling. ${spec.why}`
1004
+ };
1005
+ }
1006
+ if (status < 400) {
1007
+ return {
1008
+ connector: spec.connector,
1009
+ outcome: "fail",
1010
+ status,
1011
+ detail: `${status} \u2014 the provider ACCEPTED a deliberately invalid credential. Either the credential is reaching nothing (wrong header, wrong field) or this endpoint does not authenticate. Neither is safe to ship.`
1012
+ };
1013
+ }
1014
+ const classified = classifyStatus(status, body);
1015
+ return {
1016
+ connector: spec.connector,
1017
+ outcome: "fail",
1018
+ status,
1019
+ kind: classified.kind,
1020
+ detail: `${status} \u2014 expected one of ${spec.authStatuses.join(", ")}. A ${status} here usually means the endpoint moved or the path is wrong, which no amount of local mocking would ever have said. ${body.slice(0, 200)}`
1021
+ };
1022
+ }
1023
+ async function runProbes(specs) {
1024
+ const results = [];
1025
+ for (const spec of specs) results.push(await probe(spec));
1026
+ const passed = results.filter((result) => result.outcome === "pass").length;
1027
+ const failed = results.filter((result) => result.outcome === "fail").length;
1028
+ const skipped = results.filter((result) => result.outcome === "skip").length;
1029
+ return { results, passed, failed, skipped, ok: failed === 0 };
1030
+ }
1031
+
1032
+ // src/text.ts
1033
+ var encoder = new TextEncoder();
1034
+ function graphemes(text) {
1035
+ const Segmenter = Intl.Segmenter;
1036
+ if (!Segmenter) return [...text].length;
1037
+ let count = 0;
1038
+ for (const _ of new Segmenter(void 0, { granularity: "grapheme" }).segment(text)) count++;
1039
+ return count;
1040
+ }
1041
+ function utf8Length(text) {
1042
+ return encoder.encode(text).length;
1043
+ }
1044
+ function measure(text, unit) {
1045
+ if (unit === "graphemes") return graphemes(text);
1046
+ if (unit === "utf8-bytes") return utf8Length(text);
1047
+ return text.length;
1048
+ }
1049
+ function byteRangeOf(text, charIndex, substring) {
1050
+ const byteStart = utf8Length(text.slice(0, charIndex));
1051
+ return { byteStart, byteEnd: byteStart + utf8Length(substring) };
1052
+ }
1053
+ function sliceByteRange(text, range) {
1054
+ const bytes = encoder.encode(text);
1055
+ return new TextDecoder().decode(bytes.slice(range.byteStart, range.byteEnd));
1056
+ }
1057
+ function linkRanges(text) {
1058
+ const found = [];
1059
+ for (const match of text.matchAll(/https?:\/\/[^\s]+/g)) {
1060
+ if (match.index === void 0) continue;
1061
+ const url = match[0].replace(/[.,;:!?)\]}'"]+$/, "");
1062
+ found.push({ ...byteRangeOf(text, match.index, url), url });
1063
+ }
1064
+ return found;
1065
+ }
1066
+
1067
+ // src/render.ts
1068
+ var RENDERER_VERSION = "render@1";
1069
+ var defaultNumbering = (index, total) => ` (${index}/${total})`;
1070
+ var SENTENCE_BOUNDARY = /(?<=[.!?])(?=\s)|(?<=\n)/;
1071
+ function splitToFit(text, limit, unit) {
1072
+ const count = (value) => measure(value, unit);
1073
+ const trimmed = text.trim();
1074
+ if (count(trimmed) <= limit) return { parts: trimmed === "" ? [] : [trimmed], problems: [] };
1075
+ const problems = [];
1076
+ const parts = [];
1077
+ const sentences = trimmed.split(SENTENCE_BOUNDARY).filter((part) => part !== "");
1078
+ let current = "";
1079
+ const flush = () => {
1080
+ if (current.trim() !== "") parts.push(current.trim());
1081
+ current = "";
1082
+ };
1083
+ for (const sentence of sentences) {
1084
+ const candidate = current + sentence;
1085
+ if (count(candidate.trim()) <= limit) {
1086
+ current = candidate;
1087
+ continue;
1088
+ }
1089
+ flush();
1090
+ if (count(sentence.trim()) <= limit) {
1091
+ current = sentence;
1092
+ continue;
1093
+ }
1094
+ let buffer = "";
1095
+ for (const word of sentence.split(/(\s+)/)) {
1096
+ if (count((buffer + word).trim()) <= limit) {
1097
+ buffer += word;
1098
+ continue;
1099
+ }
1100
+ if (buffer.trim() !== "") parts.push(buffer.trim());
1101
+ buffer = "";
1102
+ if (count(word.trim()) > limit) {
1103
+ problems.push(
1104
+ `"${word.trim().slice(0, 40)}\u2026" is ${count(word.trim())} ${unit} on its own, longer than one whole message (${limit}). It cannot be split without breaking it, so this needs rewriting rather than adapting.`
1105
+ );
1106
+ continue;
1107
+ }
1108
+ buffer = word;
1109
+ }
1110
+ current = buffer;
1111
+ }
1112
+ flush();
1113
+ return { parts: parts.filter((part) => part !== ""), problems };
1114
+ }
1115
+ function render(text, rules) {
1116
+ const { limit, unit, label } = rules;
1117
+ const count = (value) => measure(value, unit);
1118
+ const trimmed = text.trim();
1119
+ if (limit === null || count(trimmed) <= limit) {
1120
+ return {
1121
+ segments: [{ text: trimmed, count: count(trimmed), ...rules.links ? { links: linkRanges(trimmed) } : {} }],
1122
+ unit,
1123
+ limit,
1124
+ problems: [],
1125
+ rendererVersion: RENDERER_VERSION
1126
+ };
1127
+ }
1128
+ if (!rules.thread) {
1129
+ return {
1130
+ segments: [{ text: trimmed, count: count(trimmed), ...rules.links ? { links: linkRanges(trimmed) } : {} }],
1131
+ unit,
1132
+ limit,
1133
+ problems: [
1134
+ `${count(trimmed)} ${unit}; ${label}'s limit is ${limit}, and it has no thread mechanism to split into. This needs shortening rather than adapting.`
1135
+ ],
1136
+ rendererVersion: RENDERER_VERSION
1137
+ };
1138
+ }
1139
+ const numbering = rules.numbering ?? defaultNumbering;
1140
+ const first = splitToFit(text, limit, unit);
1141
+ const willNumber = first.parts.length > 1;
1142
+ const suffixCost = willNumber ? count(numbering(99, 99)) : 0;
1143
+ const { parts, problems } = willNumber ? splitToFit(text, limit - suffixCost, unit) : first;
1144
+ const segments = parts.map((part, index) => {
1145
+ const withNumber = parts.length > 1 ? `${part}${numbering(index + 1, parts.length)}` : part;
1146
+ return {
1147
+ text: withNumber,
1148
+ count: count(withNumber),
1149
+ // Computed per segment and never sliced from the original — an offset into
1150
+ // the whole text is meaningless once the text has been split.
1151
+ ...rules.links ? { links: linkRanges(withNumber) } : {}
1152
+ };
1153
+ });
1154
+ const over = segments.filter((segment) => segment.count > limit);
1155
+ if (over.length > 0) {
1156
+ problems.push(
1157
+ `${over.length} segment(s) still exceed ${limit} ${unit} after splitting. That is a renderer defect rather than a content problem \u2014 the split should not produce an over-length message.`
1158
+ );
1159
+ }
1160
+ return { segments, unit, limit, problems, rendererVersion: RENDERER_VERSION };
1161
+ }
1162
+ async function payloadHash(payload) {
1163
+ const subtle = globalThis.crypto?.subtle;
1164
+ if (!subtle) {
1165
+ throw new Error(
1166
+ "WebCrypto is not available in this runtime, so a payload hash cannot be computed. Refusing rather than returning a weaker fingerprint that a host would trust equally."
1167
+ );
1168
+ }
1169
+ const canonical = JSON.stringify({
1170
+ // Inside the hash, deliberately — see RENDERER_VERSION.
1171
+ v: payload.rendererVersion,
1172
+ segments: payload.segments.map((segment) => segment.text)
1173
+ });
1174
+ const digest = await subtle.digest("SHA-256", new TextEncoder().encode(canonical));
1175
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1176
+ }
1177
+ function isEmptyPayload(payload) {
1178
+ return payload.segments.every((segment) => segment.text.trim() === "");
1179
+ }
1180
+
1181
+ // src/webhook.ts
1182
+ async function verifyHmac(options) {
1183
+ const { raw, signature, secret, scheme, timestamp } = options;
1184
+ if (!secret) {
1185
+ return { ok: false, reason: "no signing secret configured for this trigger" };
1186
+ }
1187
+ if (!signature) return { ok: false, reason: "delivery carried no signature header" };
1188
+ if (scheme.tolerance !== void 0) {
1189
+ if (!timestamp) return { ok: false, reason: "delivery carried no timestamp header" };
1190
+ const sent = Number(timestamp);
1191
+ if (!Number.isFinite(sent)) return { ok: false, reason: "timestamp header is not a number" };
1192
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
1193
+ if (Math.abs(now - sent) > scheme.tolerance) {
1194
+ return { ok: false, reason: `delivery is outside the ${scheme.tolerance}s replay window` };
1195
+ }
1196
+ }
1197
+ const expected = await hmac(secret, scheme.payload(raw, timestamp), scheme.algorithm, scheme.encoding ?? "hex");
1198
+ return constantTimeEquals(expected, signature) ? { ok: true } : { ok: false, reason: "signature did not match" };
1199
+ }
1200
+ async function hmac(secret, payload, algorithm, encoding = "hex") {
1201
+ const subtle = globalThis.crypto?.subtle;
1202
+ if (!subtle) {
1203
+ throw new Error(
1204
+ "WebCrypto is not available in this runtime, so webhook signatures cannot be verified. Refusing to accept deliveries unverified."
1205
+ );
1206
+ }
1207
+ const encoder2 = new TextEncoder();
1208
+ const key = await subtle.importKey(
1209
+ "raw",
1210
+ encoder2.encode(secret),
1211
+ { name: "HMAC", hash: algorithm },
1212
+ false,
1213
+ ["sign"]
1214
+ );
1215
+ const bytes = new Uint8Array(await subtle.sign("HMAC", key, encoder2.encode(payload)));
1216
+ if (encoding === "base64") {
1217
+ let binary = "";
1218
+ bytes.forEach((byte) => {
1219
+ binary += String.fromCharCode(byte);
1220
+ });
1221
+ return btoa(binary);
1222
+ }
1223
+ return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1224
+ }
1225
+ function constantTimeEquals(a, b) {
1226
+ if (a.length !== b.length) return false;
1227
+ let diff = 0;
1228
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1229
+ return diff === 0;
1230
+ }
1231
+ function header(headers, name) {
1232
+ const wanted = name.toLowerCase();
1233
+ for (const [key, value] of Object.entries(headers)) {
1234
+ if (key.toLowerCase() !== wanted) continue;
1235
+ return Array.isArray(value) ? value[0] : value;
1236
+ }
1237
+ return void 0;
1238
+ }
1239
+
1240
+ // src/trigger.ts
1241
+ async function verifyDelivery(trigger, delivery, secret, now) {
1242
+ const spec = trigger.verification;
1243
+ if (!spec) {
1244
+ return {
1245
+ ok: false,
1246
+ reason: `${trigger.service}.${trigger.operation} declares no signature scheme, so a delivery cannot be trusted.`
1247
+ };
1248
+ }
1249
+ const rawHeader = header(delivery.headers, spec.signatureHeader);
1250
+ const parsed = spec.parse && rawHeader ? spec.parse(rawHeader) : { signature: rawHeader };
1251
+ const timestamp = parsed.timestamp ?? (spec.timestampHeader ? header(delivery.headers, spec.timestampHeader) : void 0);
1252
+ return verifyHmac({
1253
+ raw: delivery.raw,
1254
+ signature: parsed.signature,
1255
+ secret,
1256
+ scheme: spec.scheme,
1257
+ timestamp,
1258
+ ...now === void 0 ? {} : { now }
1259
+ });
1260
+ }
1261
+ function triggerEvent(trigger, connection, injected, config) {
1262
+ if (hasDelivery(injected)) return injected;
1263
+ if (connection.mode === "fake") {
1264
+ return trigger.faker(
1265
+ trigger.operation,
1266
+ fakeRequest(trigger.service, trigger.operation, config)
1267
+ );
1268
+ }
1269
+ throw new Error(
1270
+ `${trigger.service}.${trigger.operation}: no event was delivered to this trigger. Its delivery mechanism is "${trigger.delivery}" \u2014 ${trigger.setup} Set the node's mode to "fake" to design against a sample event instead.`
1271
+ );
1272
+ }
1273
+ function hasDelivery(value) {
1274
+ if (value === void 0 || value === null) return false;
1275
+ if (Array.isArray(value)) return value.length > 0;
1276
+ if (typeof value === "object") return Object.keys(value).length > 0;
1277
+ return true;
1278
+ }
1279
+ // Annotate the CommonJS export names for ESM import in node:
1280
+ 0 && (module.exports = {
1281
+ AMBIGUOUS_REFUSAL,
1282
+ CONNECTOR_API_VERSION,
1283
+ CONNECTOR_MODES,
1284
+ ConnectorAmbiguous,
1285
+ ConnectorApiMismatch,
1286
+ ConnectorAuthError,
1287
+ ConnectorConfigError,
1288
+ ConnectorError,
1289
+ ConnectorIdempotencyExpiredError,
1290
+ ConnectorModeError,
1291
+ ConnectorRateLimited,
1292
+ ConnectorRequestError,
1293
+ ConnectorTransient,
1294
+ ConnectorUnreachable,
1295
+ DEFAULT_IDEMPOTENCY_WINDOW_SECONDS,
1296
+ DEFAULT_RETRY,
1297
+ FAKE_EPOCH,
1298
+ MAX_IDEMPOTENCY_KEY_LENGTH,
1299
+ NO_IDEMPOTENCY_KEY_WARNING,
1300
+ RENDERER_VERSION,
1301
+ REVIEW_STALE_DAYS,
1302
+ SUPPORTED_CONNECTOR_API,
1303
+ assertConnectorApi,
1304
+ byteRangeOf,
1305
+ callConnector,
1306
+ capabilityProblems,
1307
+ checkAgainstOpenApi,
1308
+ checkAgainstRecordedShape,
1309
+ classifyError,
1310
+ classifyHttp,
1311
+ classifyStatus,
1312
+ classifyThrown,
1313
+ compareShape,
1314
+ connectionsFor,
1315
+ constantTimeEquals,
1316
+ deliver,
1317
+ fakeRequest,
1318
+ fakeValues,
1319
+ getConnectionHost,
1320
+ graphemes,
1321
+ header,
1322
+ hmac,
1323
+ httpFailure,
1324
+ idempotencyKeyFor,
1325
+ isEmptyPayload,
1326
+ isRemote,
1327
+ isUnconditionallyRetryable,
1328
+ linkRanges,
1329
+ measure,
1330
+ payloadHash,
1331
+ postChain,
1332
+ probe,
1333
+ registerConnectionHost,
1334
+ registerTransport,
1335
+ render,
1336
+ reported,
1337
+ resetRateState,
1338
+ resolveConnection,
1339
+ resolveConnectorMode,
1340
+ respectRate,
1341
+ runIdentityFrom,
1342
+ runKeyFrom,
1343
+ runProbes,
1344
+ satisfiesMinimum,
1345
+ seedFrom,
1346
+ shapeOf,
1347
+ shouldRetry,
1348
+ sliceByteRange,
1349
+ splitToFit,
1350
+ triggerEvent,
1351
+ unchecked,
1352
+ utf8Length,
1353
+ verifyDelivery,
1354
+ verifyHmac
1355
+ });
1356
+ //# sourceMappingURL=index.cjs.map