@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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chain.ts","../src/compat.ts","../src/delivery.ts","../src/errors.ts","../src/faker.ts","../src/mode.ts","../src/connection.ts","../src/client.ts","../src/drift.ts","../src/idempotency.ts","../src/metrics.ts","../src/probe.ts","../src/text.ts","../src/render.ts","../src/webhook.ts","../src/trigger.ts"],"sourcesContent":["/**\r\n * Posting a sequence as a CHAIN — the bug that is silent in every direction.\r\n *\r\n * ## What went wrong in the reference implementation\r\n *\r\n * A renderer split long copy into `(1/3)`, `(2/3)`, `(3/3)` and the connector\r\n * posted each segment **top-level**, on two providers. Numbered like a thread,\r\n * connected to nothing. Nothing threw; the send reported success; the numbering\r\n * made it look deliberate, so a reader assumes the thread exists and they missed\r\n * it.\r\n *\r\n * It was unreachable by any test, because the chaining lived inside a loop that\r\n * needed a live session. So the chain builder here **takes the post function as\r\n * an argument** — which is the entire reason this is a module rather than four\r\n * lines inside a connector.\r\n *\r\n * ## The rule it encodes, which is one line and easy to reverse\r\n *\r\n * > **`root` is fixed at the top of the chain and never moves. `parent` advances\r\n * > to whatever was just posted.**\r\n *\r\n * Reverse them and every message attaches to the first: a fan, not a thread —\r\n * and the provider's response looks identical either way, which is why this\r\n * needs a test rather than care.\r\n *\r\n * ## What it deliberately does NOT do\r\n *\r\n * No retry. Retry wraps ONE request (see `delivery.ts`); wrapping the chain\r\n * would re-post every earlier segment when a later one failed, turning a partial\r\n * send into a duplicated one. A partial chain is reported with what it did post,\r\n * so a person can see exactly where it stopped.\r\n */\r\n\r\n/**\r\n * A reference to something already posted, as the provider identifies it.\r\n *\r\n * Opaque to this module: AT Protocol needs a uri **and** a cid, Mastodon needs\r\n * one status id, Telegram needs a message id and a chat. A shape naming one\r\n * provider's model would be wrong for the next one, so the chain carries\r\n * whatever the poster returned and never reads inside it.\r\n */\r\nexport type ChainRef = Record<string, string | number>;\r\n\r\nexport type ChainLinks<Ref extends ChainRef> = {\r\n /** The top of the conversation. Fixed for the whole chain. */\r\n root: Ref;\r\n /** The message immediately above this one. Advances every step. */\r\n parent: Ref;\r\n};\r\n\r\nexport type ChainOutcome<Ref extends ChainRef> = {\r\n /** Everything that was posted, in order. */\r\n posted: Ref[];\r\n /** The failure that stopped it, if one did. */\r\n failed?: { index: number; error: unknown };\r\n};\r\n\r\n/**\r\n * Post `segments` as a connected chain, optionally starting inside an existing\r\n * conversation.\r\n *\r\n * `post` is given the text and the links for this position — `undefined` for a\r\n * top-level message — and returns the provider's reference to what it created.\r\n *\r\n * **Stops at the first failure and reports what it posted.** Continuing would\r\n * produce a thread with a hole in it, and unwinding is not available: nothing\r\n * here can delete a public message, and pretending otherwise would be worse than\r\n * the hole.\r\n */\r\nexport async function postChain<Ref extends ChainRef>(\r\n segments: string[],\r\n answering: ChainLinks<Ref> | undefined,\r\n post: (text: string, links: ChainLinks<Ref> | undefined, index: number) => Promise<Ref>,\r\n): Promise<ChainOutcome<Ref>> {\r\n const posted: Ref[] = [];\r\n let root = answering?.root;\r\n let parent = answering?.parent;\r\n\r\n for (const [index, text] of segments.entries()) {\r\n let made: Ref;\r\n\r\n try {\r\n made = await post(text, root && parent ? { root, parent } : undefined, index);\r\n } catch (error) {\r\n return { posted, failed: { index, error } };\r\n }\r\n\r\n posted.push(made);\r\n // The first thing WE post becomes the root of our own chain — unless we are\r\n // already inside somebody else's, in which case theirs stays the root.\r\n root = root ?? made;\r\n parent = made;\r\n }\r\n\r\n return { posted };\r\n}\r\n","/**\r\n * The connector API version — what makes two release clocks safe.\r\n *\r\n * ## The problem this exists for\r\n *\r\n * The core and the connector catalogue are separate repositories, so they\r\n * release on separate clocks. That is deliberate: a provider changing its API is\r\n * a connector fix and must not wait on a core release. But it creates a hazard\r\n * that is worse here than in an ordinary package pair, because **connectors ship\r\n * as vendored source**:\r\n *\r\n * > A consumer copies a connector into their project. Six months later they\r\n * > upgrade the core. Their copy of the connector was written against a core two\r\n * > minors older, and pre-1.0 minors are allowed to break.\r\n *\r\n * Nothing in a version range catches that, because the vendored copy has no\r\n * manifest of its own — it is just files in someone else's repository. And the\r\n * failure is the quiet kind: the connector still compiles, still runs, and does\r\n * something subtly different.\r\n *\r\n * ## The fix: a second number that moves far more slowly\r\n *\r\n * The core's package version tracks everything — fixes, additions, internal\r\n * changes. `CONNECTOR_API_VERSION` tracks only one thing: **the surface a\r\n * connector is written against.** Adding a module, fixing a classifier, or\r\n * improving a message does not move it. Changing what a `Connector` must\r\n * implement, renaming a field a connector declares, or altering what `render`\r\n * returns does.\r\n *\r\n * So a core can go 0.3 → 0.9 without a single vendored connector caring, and the\r\n * one release that does matter announces itself in a number a connector can\r\n * check at import time rather than discover at run time.\r\n *\r\n * ## The support window, and why it is two rather than one\r\n *\r\n * The core accepts the current version **and the one before it**. One would mean\r\n * every consumer has to re-vendor every connector on the same day the core ships\r\n * — which nobody does, so in practice they would pin the core instead and stop\r\n * getting fixes. Two gives a real window: the catalogue re-issues its connectors\r\n * on the new version, consumers re-vendor when they next touch that connector,\r\n * and the old shape keeps working until the version after next.\r\n *\r\n * More than two would be a promise we cannot keep, because it means maintaining\r\n * a compatibility shim for a shape nobody has read in a year.\r\n */\r\n\r\n/**\r\n * The connector surface this core implements.\r\n *\r\n * **Bump this ONLY for a change a connector's own source can see**, and say in\r\n * the changelog what a connector author must DO. If nothing in `connectors/`\r\n * would have to change, it is not a connector API change.\r\n */\r\nexport const CONNECTOR_API_VERSION = 1;\r\n\r\n/**\r\n * Versions this core will run.\r\n *\r\n * Ordered newest first, purely so the error message reads naturally.\r\n */\r\nexport const SUPPORTED_CONNECTOR_API: readonly number[] = [1];\r\n\r\nexport class ConnectorApiMismatch extends Error {\r\n readonly connector: string;\r\n\r\n readonly declared: number;\r\n\r\n constructor(message: string, connector: string, declared: number) {\r\n super(message);\r\n this.name = \"ConnectorApiMismatch\";\r\n this.connector = connector;\r\n this.declared = declared;\r\n }\r\n}\r\n\r\n/**\r\n * Refuse a connector written against a surface this core does not implement.\r\n *\r\n * **Called at registration, not at call time.** A mismatch discovered on the\r\n * first real request is a mismatch discovered in production; discovered when the\r\n * catalogue is assembled, it is a build failure with a name attached.\r\n *\r\n * The message names the direction, because the two cases need opposite actions\r\n * and are trivially confused:\r\n *\r\n * - **connector NEWER than the core** → upgrade the core.\r\n * - **connector OLDER than the window** → re-vendor the connector.\r\n *\r\n * \"Incompatible versions\" sends someone to read source; naming which side is\r\n * behind sends them to the command.\r\n */\r\nexport function assertConnectorApi(connectorId: string, declared: number): void {\r\n if (SUPPORTED_CONNECTOR_API.includes(declared)) return;\r\n\r\n const supported = SUPPORTED_CONNECTOR_API.join(\", \");\r\n\r\n throw new ConnectorApiMismatch(\r\n declared > CONNECTOR_API_VERSION\r\n ? `\"${connectorId}\" was written against connector API ${declared}, and this core implements ${supported}. ` +\r\n \"The CONNECTOR is newer: upgrade @particle-academy/fancy-connector-core. Vendoring a connector ahead of \" +\r\n \"the core it needs is the one direction that cannot be made to work by trying.\"\r\n : `\"${connectorId}\" was written against connector API ${declared}, which is older than anything this core ` +\r\n `still runs (${supported}). The CONNECTOR is behind: re-vendor it — ` +\r\n `\\`npx fancy-cli@latest add connector ${connectorId}\\` — which fetches the copy written for this core. ` +\r\n \"Nothing is adapted automatically, because guessing what a two-version-old connector meant is how a \" +\r\n \"connector quietly starts doing something else.\",\r\n connectorId,\r\n declared,\r\n );\r\n}\r\n\r\n/**\r\n * Whether a core version satisfies a connector's declared minimum.\r\n *\r\n * Exists so a registry or a CLI can answer *before* vendoring, which is the only\r\n * moment the answer is cheap. Compares the numeric prefix of each dotted\r\n * segment, so a prerelease suffix does not make `0.4.0-rc.1` sort below `0.3.9`\r\n * in a way nobody expected.\r\n */\r\nexport function satisfiesMinimum(coreVersion: string, minimum: string): boolean {\r\n const parts = (value: string): number[] =>\r\n value\r\n .split(\".\")\r\n .slice(0, 3)\r\n .map((segment) => Number.parseInt(segment, 10) || 0);\r\n\r\n const [a = 0, b = 0, c = 0] = parts(coreVersion);\r\n const [x = 0, y = 0, z = 0] = parts(minimum);\r\n\r\n if (a !== x) return a > x;\r\n if (b !== y) return b > y;\r\n\r\n return c >= z;\r\n}\r\n","/**\r\n * Failure classification — the one decision every retry gets wrong.\r\n *\r\n * ## The tension\r\n *\r\n * A network hiccup should not lose a call. And a retry after a request that\r\n * **succeeded and whose response was lost** is a second write — the exact\r\n * failure an idempotency gate exists to prevent, arriving through the door\r\n * marked *reliability*.\r\n *\r\n * Nearly every retry helper reconciles those the same way and loses: it catches\r\n * an error, waits, and tries again, because from the caller's side all failures\r\n * look alike. They are not alike.\r\n *\r\n * | what happened | did the provider receive it? | retry? |\r\n * |---|---|---|\r\n * | DNS failure, connection refused, socket reset before send | **no** | always safe |\r\n * | HTTP 429 / 5xx | **yes**, and it explicitly refused | safe — it says it did nothing |\r\n * | timeout, aborted request, unrecognised error | **unknown** | only where the provider is idempotent |\r\n * | HTTP 4xx that is not 429 | **yes**, and the answer was a real no | never |\r\n *\r\n * That third row is the whole design. A timeout looks exactly like a failure and\r\n * may have been a success.\r\n *\r\n * ## Why this is a separate axis from the error CLASS\r\n *\r\n * The first version of this runtime carried `retryable` as a fixed boolean on\r\n * each error class, and `ConnectorTransient` covered both a 5xx *and* a thrown\r\n * transport. Those are opposite cases: a 5xx is the provider telling you it did\r\n * nothing, and a thrown transport may be a socket that closed after the bytes\r\n * went out. Marking both `retryable = true` meant an ambiguous failure on a\r\n * connector with no idempotency key was retried — a silent double write, on the\r\n * path whose entire job is not producing one.\r\n *\r\n * So the primitive is the **kind**, and retryability is a function of the kind\r\n * AND what the connector declared about repeating a request. `error.retryable`\r\n * survives and now answers the narrower question — *is this safe to retry\r\n * whatever the connector is?* — which makes an old caller conservative rather\r\n * than wrong.\r\n */\r\n\r\n/** Why a call failed, in the only terms that decide whether to try again. */\r\nexport type FailureKind =\r\n /** Never reached the provider. A second attempt cannot duplicate anything. */\r\n | \"unreachable\"\r\n /** The provider answered, and its answer was \"not now\". It did nothing. */\r\n | \"refused-explicitly\"\r\n /** It may or may not have been acted on. Nobody can tell. */\r\n | \"ambiguous\"\r\n /** The provider answered and the answer was a real, permanent no. */\r\n | \"rejected\";\r\n\r\n/**\r\n * How a connector behaves when a call goes wrong, and how fast it may be used.\r\n *\r\n * **`idempotent` is the load-bearing field**, and it is the only thing that\r\n * makes an ambiguous failure safe to retry. A connector that has not thought\r\n * about it gets `false`, and an ambiguous failure is then reported for a person\r\n * instead of retried into a double write.\r\n */\r\nexport type DeliveryDeclaration = {\r\n idempotent: boolean;\r\n /**\r\n * Why it is or is not — **cited, not asserted**.\r\n *\r\n * `idempotent: true` with no reason is the one claim whose failure is a public\r\n * double write, so the reason names the mechanism (`Idempotency-Key`,\r\n * `com.atproto.repo.createRecord` having none) rather than restating the flag.\r\n */\r\n why: string;\r\n /** Smallest gap between two calls on this connector. */\r\n minIntervalMs: number;\r\n /**\r\n * Whose number `minIntervalMs` is.\r\n *\r\n * A confident figure nobody can cite is worse than an honest one that is too\r\n * slow, because the honest one gets revised when evidence turns up and the\r\n * confident one gets quoted as a platform fact.\r\n */\r\n rateSource: \"documented\" | \"self-imposed\";\r\n /** Where the numbers came from. See `citation.ts`. */\r\n citation?: Citation;\r\n};\r\n\r\n/**\r\n * Where a declared fact was read, and when.\r\n *\r\n * A citation with no date is an assertion wearing a URL. The date is what lets a\r\n * drift check say *this was true eight months ago and nobody has looked since*,\r\n * which is a different and more useful statement than *this is true*.\r\n */\r\nexport type Citation = {\r\n /** The provider's own documentation for this fact. */\r\n url: string;\r\n /** ISO date the URL was actually read by a person or a check. */\r\n readOn: string;\r\n /** The sentence the fact rests on, quoted. Optional but strongly wanted. */\r\n quote?: string;\r\n};\r\n\r\nexport type Classified = {\r\n kind: FailureKind;\r\n detail: string;\r\n /** Seconds the provider asked us to wait, when it said so. */\r\n retryAfter?: number;\r\n};\r\n\r\n/**\r\n * Codes that prove the request never left, or left and was refused at the door.\r\n *\r\n * Matched on codes rather than message text, because message text is not an API\r\n * and differs between Node, Bun, Deno and an edge runtime.\r\n */\r\nconst UNREACHABLE_CODES = new Set([\r\n \"ECONNREFUSED\",\r\n \"ENOTFOUND\",\r\n \"EAI_AGAIN\",\r\n \"EHOSTUNREACH\",\r\n \"ENETUNREACH\",\r\n \"ECONNRESET\",\r\n \"EPIPE\",\r\n \"ERR_SOCKET_CONNECTION_TIMEOUT\",\r\n]);\r\n\r\n/** Codes and names that mean *we stopped waiting*, which is not the same as *it did not happen*. */\r\nconst AMBIGUOUS_CODES = new Set([\r\n \"ETIMEDOUT\",\r\n \"UND_ERR_HEADERS_TIMEOUT\",\r\n \"UND_ERR_BODY_TIMEOUT\",\r\n \"UND_ERR_CONNECT_TIMEOUT\",\r\n]);\r\n\r\nconst AMBIGUOUS_NAMES = new Set([\"AbortError\", \"TimeoutError\"]);\r\n\r\n/**\r\n * Classify a thrown transport failure.\r\n *\r\n * **An unrecognised error falls to `ambiguous`, never to `unreachable`.** That\r\n * asymmetry is deliberate: an unknown failure treated as unreachable would be\r\n * retried, and the one thing worse than a lost call is two calls nobody asked\r\n * for. Guessing in the safe-looking direction is how this goes wrong.\r\n */\r\nexport function classifyError(error: unknown): Classified {\r\n const detail = error instanceof Error ? error.message : String(error);\r\n const code =\r\n (error as { code?: string })?.code ?? (error as { cause?: { code?: string } })?.cause?.code ?? \"\";\r\n const name = error instanceof Error ? error.name : \"\";\r\n\r\n if (UNREACHABLE_CODES.has(code)) return { kind: \"unreachable\", detail: `${code}: ${detail}` };\r\n if (AMBIGUOUS_NAMES.has(name) || AMBIGUOUS_CODES.has(code)) {\r\n return { kind: \"ambiguous\", detail: `${name || code}: ${detail}` };\r\n }\r\n\r\n return { kind: \"ambiguous\", detail };\r\n}\r\n\r\n/**\r\n * Classify an HTTP status.\r\n *\r\n * A status means the provider answered, so it knows what it did. 429 and 5xx are\r\n * \"not now\"; everything else in the 4xx range is a real no, and retrying a real\r\n * no just spends someone's rate limit on the same rejection.\r\n *\r\n * 429 is decided **before** the 4xx sweep, because it is a 4xx and is the one\r\n * 4xx worth retrying — the other ordering marks every throttle permanent and\r\n * turns a busy minute into a failed run.\r\n */\r\nexport function classifyStatus(status: number, body = \"\", retryAfterHeader?: string | null): Classified {\r\n const detail = `${status}${body ? `: ${body.slice(0, 200)}` : \"\"}`;\r\n const seconds = retryAfterHeader === undefined || retryAfterHeader === null ? NaN : Number(retryAfterHeader);\r\n\r\n if (status === 429 || status >= 500) {\r\n return {\r\n kind: \"refused-explicitly\",\r\n detail,\r\n ...(Number.isFinite(seconds) ? { retryAfter: seconds } : {}),\r\n };\r\n }\r\n\r\n return { kind: \"rejected\", detail };\r\n}\r\n\r\nexport type RetryPolicy = {\r\n /** Total attempts, including the first. */\r\n attempts: number;\r\n /** First backoff in ms. Doubles each attempt. */\r\n baseDelayMs: number;\r\n /** Never wait longer than this between attempts. */\r\n maxDelayMs: number;\r\n /**\r\n * Whether the provider makes a repeated request harmless.\r\n *\r\n * `false` is the correct default for any connector that has not proven\r\n * otherwise, and \"proven\" means a test that publishes twice and compares the\r\n * result — not a comment.\r\n */\r\n idempotent: boolean;\r\n};\r\n\r\nexport const DEFAULT_RETRY: RetryPolicy = {\r\n attempts: 3,\r\n baseDelayMs: 500,\r\n maxDelayMs: 8_000,\r\n idempotent: false,\r\n};\r\n\r\n/**\r\n * May this failure be retried?\r\n *\r\n * The whole rule, in four lines, in one place. A connector that classified its\r\n * own statuses would eventually decide some 4xx was worth retrying, and the\r\n * reason retries are safe at all is that this decision is made once.\r\n */\r\nexport function shouldRetry(kind: FailureKind, policy: Pick<RetryPolicy, \"idempotent\">): boolean {\r\n if (kind === \"rejected\") return false;\r\n if (kind === \"unreachable\" || kind === \"refused-explicitly\") return true;\r\n\r\n return policy.idempotent;\r\n}\r\n\r\n/**\r\n * True where a retry is safe **whatever the connector is** — i.e. without\r\n * needing to know whether repeating the request is harmless.\r\n *\r\n * This is what `ConnectorError.retryable` answers, and it is deliberately the\r\n * narrow question. A caller that has an idempotency declaration should ask\r\n * `shouldRetry` instead and get the extra case.\r\n */\r\nexport function isUnconditionallyRetryable(kind: FailureKind): boolean {\r\n return kind === \"unreachable\" || kind === \"refused-explicitly\";\r\n}\r\n\r\nexport type Attempt = {\r\n attempt: number;\r\n kind: FailureKind;\r\n detail: string;\r\n /** How long we waited before the NEXT attempt. Absent on the last. */\r\n waitedMs?: number;\r\n};\r\n\r\nexport type DeliveryOutcome<T> = {\r\n ok: boolean;\r\n value?: T;\r\n /** Every attempt that failed, in order. Empty when the first one worked. */\r\n attempts: Attempt[];\r\n /** Why it stopped, when it failed. Written for a person who has to act. */\r\n gaveUp?: string;\r\n /** The classification of the last failure, so a host can route on it. */\r\n kind?: FailureKind;\r\n};\r\n\r\n/**\r\n * The message a person reads when an ambiguous failure could not be retried.\r\n *\r\n * Exported because a host will want to recognise it, and because the wording is\r\n * load-bearing: it has to say *go and look*, not *it failed*. Those prompt\r\n * different actions and only one of them is correct.\r\n */\r\nexport const AMBIGUOUS_REFUSAL =\r\n \"The request may or may not have gone through, and this connector offers no way to repeat it \" +\r\n \"harmlessly. Refusing to try again — a duplicate is worse than a call that needs doing by hand. \" +\r\n \"Check the provider before re-running.\";\r\n\r\n/**\r\n * Run ONE request, retrying only where retrying is provably safe.\r\n *\r\n * ## Retry wraps ONE request, never a sequence\r\n *\r\n * Wrapping a multi-post publish would re-send every earlier segment of a thread\r\n * when a later one failed — turning a partial send into a duplicated one. So the\r\n * unit is a single request and a chain composes above it, not below.\r\n *\r\n * `send` must throw on failure. An error carrying a `classified` property (see\r\n * `httpFailure`) is trusted; anything else is classified here. That indirection\r\n * lets each connector decide what counts as a failure for it — a Telegram\r\n * `200 OK` with `{\"ok\": false}` is a failure and only the connector knows that —\r\n * while the decision about *retrying* stays in one place for all of them.\r\n *\r\n * `sleep` is injectable so a test proves the real backoff schedule without\r\n * waiting for it. A test that actually slept would be slow enough that somebody\r\n * would eventually shorten the delays to speed it up, and then the thing under\r\n * test would be the shortened version.\r\n */\r\nexport async function deliver<T>(\r\n send: (attempt: number) => Promise<T>,\r\n policy: RetryPolicy = DEFAULT_RETRY,\r\n sleep: (ms: number) => Promise<void> = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\r\n): Promise<DeliveryOutcome<T>> {\r\n const attempts: Attempt[] = [];\r\n const budget = Math.max(1, policy.attempts);\r\n\r\n for (let attempt = 1; attempt <= budget; attempt++) {\r\n try {\r\n const value = await send(attempt);\r\n\r\n return { ok: true, value, attempts };\r\n } catch (error) {\r\n const classified = (error as { classified?: Classified }).classified ?? classifyError(error);\r\n\r\n if (!shouldRetry(classified.kind, policy)) {\r\n attempts.push({ attempt, kind: classified.kind, detail: classified.detail });\r\n\r\n return {\r\n ok: false,\r\n attempts,\r\n kind: classified.kind,\r\n gaveUp:\r\n classified.kind === \"ambiguous\"\r\n ? `${AMBIGUOUS_REFUSAL} (${classified.detail})`\r\n : classified.detail,\r\n };\r\n }\r\n\r\n if (attempt >= budget) {\r\n attempts.push({ attempt, kind: classified.kind, detail: classified.detail });\r\n\r\n return {\r\n ok: false,\r\n attempts,\r\n kind: classified.kind,\r\n gaveUp: `Gave up after ${attempt} attempts. ${classified.detail}`,\r\n };\r\n }\r\n\r\n // Exponential, but the provider's own number wins when it gave one. Ours\r\n // is a guess; theirs is an instruction, and ignoring it is how a rate\r\n // limit becomes a ban.\r\n const backoff = Math.min(policy.baseDelayMs * 2 ** (attempt - 1), policy.maxDelayMs);\r\n const waitedMs = classified.retryAfter ? Math.max(backoff, classified.retryAfter * 1000) : backoff;\r\n attempts.push({ attempt, kind: classified.kind, detail: classified.detail, waitedMs });\r\n await sleep(waitedMs);\r\n }\r\n }\r\n\r\n return { ok: false, attempts, gaveUp: \"Exhausted every attempt.\" };\r\n}\r\n\r\n/* ── Rate floors ──────────────────────────────────────────────────────────── */\r\n\r\nconst lastCallAt = new Map<string, number>();\r\n\r\n/**\r\n * Wait until this channel may be used again.\r\n *\r\n * In-process only, and that is the correct scope for a host running on one\r\n * machine. A host that fans out across processes owns the coordination, because\r\n * the package cannot see the other senders and pretending otherwise would be a\r\n * limiter that reports success while doing nothing.\r\n */\r\nexport async function respectRate(\r\n channel: string,\r\n minIntervalMs: number,\r\n now: () => number = () => Date.now(),\r\n sleep: (ms: number) => Promise<void> = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\r\n): Promise<{ waitedMs: number }> {\r\n if (minIntervalMs <= 0) return { waitedMs: 0 };\r\n\r\n const previous = lastCallAt.get(channel);\r\n const at = now();\r\n const waitedMs = previous === undefined ? 0 : Math.max(0, previous + minIntervalMs - at);\r\n\r\n if (waitedMs > 0) await sleep(waitedMs);\r\n lastCallAt.set(channel, at + waitedMs);\r\n\r\n return { waitedMs };\r\n}\r\n\r\n/** Only for tests — the map is module-global and would leak between them. */\r\nexport function resetRateState(): void {\r\n lastCallAt.clear();\r\n}\r\n","/**\r\n * The connector error taxonomy.\r\n *\r\n * A host that retries needs to know WHICH KIND of failure it is looking at.\r\n * \"Something went wrong\" is not enough information to choose between retrying,\r\n * pausing for a person, and stopping — so every failure is classified at the\r\n * point it is known, which is inside the connector, not three frames up where\r\n * the type has already been lost.\r\n *\r\n * | class | kind | retry? |\r\n * |---|---|---|\r\n * | `ConnectorConfigError` | `rejected` | never — nothing about a second attempt changes an unset key |\r\n * | `ConnectorAuthError` | `rejected` | never — the credential is wrong; hammering it locks accounts |\r\n * | `ConnectorRateLimited` | `refused-explicitly` | yes, after `retryAfter` |\r\n * | `ConnectorTransient` | `refused-explicitly` | yes — a 5xx is the provider saying it did nothing |\r\n * | `ConnectorAmbiguous` | `ambiguous` | **only** where the connector is idempotent |\r\n * | `ConnectorUnreachable` | `unreachable` | yes — it never arrived |\r\n * | `ConnectorRequestError` | `rejected` | never — a 4xx we caused fails the same way twice |\r\n *\r\n * ## What changed, and why it is a fix rather than a refactor\r\n *\r\n * `ConnectorTransient` used to mean *5xx OR a thrown transport*, with\r\n * `retryable = true`. Those are opposite cases. A 5xx is the provider reporting\r\n * that it did nothing; a thrown transport may be a socket that closed after the\r\n * bytes went out. Retrying the second on a connector with no idempotency key is\r\n * a silent double write — the failure this whole layer exists to prevent.\r\n *\r\n * So the two are separate classes now, `kind` is the primitive, and `retryable`\r\n * answers the narrow question *is this safe whatever the connector is?* An older\r\n * caller reading `.retryable` therefore becomes conservative rather than wrong.\r\n */\r\n\r\nimport {\r\n classifyError,\r\n classifyStatus,\r\n isUnconditionallyRetryable,\r\n type Classified,\r\n type FailureKind,\r\n} from \"./delivery\";\r\n\r\nexport type ConnectorErrorContext = {\r\n service: string;\r\n operation: string;\r\n /** HTTP status when there was one. */\r\n status?: number;\r\n /** The provider's own error code, when it publishes one. */\r\n providerCode?: string;\r\n};\r\n\r\n/** Base for everything below, so a host can catch the family in one clause. */\r\nexport class ConnectorError extends Error {\r\n readonly service: string;\r\n\r\n readonly operation: string;\r\n\r\n readonly status?: number;\r\n\r\n readonly providerCode?: string;\r\n\r\n /**\r\n * What kind of failure this is. The primitive every retry decision reads.\r\n *\r\n * Defaults to `ambiguous` on the base class deliberately: an unclassified\r\n * failure must not be assumed safe.\r\n */\r\n readonly kind: FailureKind = \"ambiguous\";\r\n\r\n constructor(message: string, ctx: ConnectorErrorContext) {\r\n super(message);\r\n this.name = new.target.name;\r\n this.service = ctx.service;\r\n this.operation = ctx.operation;\r\n this.status = ctx.status;\r\n this.providerCode = ctx.providerCode;\r\n }\r\n\r\n /**\r\n * Safe to retry **without knowing anything about the connector**.\r\n *\r\n * Ambiguous failures answer `false` here even though they may be retryable on\r\n * an idempotent connector — ask `shouldRetry(error.kind, { idempotent })` for\r\n * that. This getter is the conservative half on purpose.\r\n */\r\n get retryable(): boolean {\r\n return isUnconditionallyRetryable(this.kind);\r\n }\r\n\r\n /** The classification, in the shape `deliver()` reads off a thrown error. */\r\n get classified(): Classified {\r\n return { kind: this.kind, detail: this.message };\r\n }\r\n}\r\n\r\n/**\r\n * A required piece of configuration is missing or unusable.\r\n *\r\n * The message must name the exact key the consumer has to set. \"Stripe is not\r\n * configured\" sends someone reading source; \"no `secretKey` on the `stripe`\r\n * connection\" sends them to the line.\r\n */\r\nexport class ConnectorConfigError extends ConnectorError {\r\n override readonly kind: FailureKind = \"rejected\";\r\n}\r\n\r\n/** The provider rejected the credential. Retrying cannot help. */\r\nexport class ConnectorAuthError extends ConnectorError {\r\n override readonly kind: FailureKind = \"rejected\";\r\n}\r\n\r\n/** The provider asked us to slow down. It did nothing, and said so. */\r\nexport class ConnectorRateLimited extends ConnectorError {\r\n override readonly kind: FailureKind = \"refused-explicitly\";\r\n\r\n /** Seconds to wait, when the provider said. */\r\n readonly retryAfter?: number;\r\n\r\n constructor(message: string, ctx: ConnectorErrorContext & { retryAfter?: number }) {\r\n super(message, ctx);\r\n this.retryAfter = ctx.retryAfter;\r\n }\r\n\r\n override get classified(): Classified {\r\n return {\r\n kind: this.kind,\r\n detail: this.message,\r\n ...(this.retryAfter === undefined ? {} : { retryAfter: this.retryAfter }),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * A 5xx. The provider answered, and its answer was \"not now\".\r\n *\r\n * Named for what it is rather than for how it feels: this is an *explicit\r\n * refusal*, which is why it is always safe to repeat. A timeout feels the same\r\n * to the caller and is not the same thing — see `ConnectorAmbiguous`.\r\n */\r\nexport class ConnectorTransient extends ConnectorError {\r\n override readonly kind: FailureKind = \"refused-explicitly\";\r\n}\r\n\r\n/**\r\n * The request never reached the provider — DNS, refused connection, a socket\r\n * closed before anything went out.\r\n *\r\n * Always safe to repeat, and the only transport-level failure of which that is\r\n * true.\r\n */\r\nexport class ConnectorUnreachable extends ConnectorError {\r\n override readonly kind: FailureKind = \"unreachable\";\r\n}\r\n\r\n/**\r\n * Nobody can tell whether the provider acted on it. A timeout, an abort, an\r\n * error nothing recognises.\r\n *\r\n * **Retryable only where the connector declared that repeating a request is\r\n * harmless.** On anything else this is reported for a person, because the\r\n * alternative is a duplicate that nobody will ever trace back to a bug.\r\n */\r\nexport class ConnectorAmbiguous extends ConnectorError {\r\n override readonly kind: FailureKind = \"ambiguous\";\r\n}\r\n\r\n/** A 4xx we caused. The same request will fail the same way. */\r\nexport class ConnectorRequestError extends ConnectorError {\r\n override readonly kind: FailureKind = \"rejected\";\r\n}\r\n\r\n/**\r\n * Classify an HTTP response into the taxonomy.\r\n *\r\n * 429 before the 4xx sweep, deliberately — see `classifyStatus`.\r\n */\r\nexport function classifyHttp(\r\n status: number,\r\n ctx: ConnectorErrorContext,\r\n body: string,\r\n retryAfter?: number,\r\n): ConnectorError {\r\n const detail = body.trim() === \"\" ? \"\" : ` — ${truncate(body, 400)}`;\r\n const where = `${ctx.service}.${ctx.operation}`;\r\n\r\n if (status === 429) {\r\n return new ConnectorRateLimited(`${where}: rate limited by the provider${detail}`, {\r\n ...ctx,\r\n status,\r\n ...(retryAfter === undefined ? {} : { retryAfter }),\r\n });\r\n }\r\n\r\n if (status === 401 || status === 403) {\r\n return new ConnectorAuthError(\r\n `${where}: the provider rejected the credential (${status})${detail}. ` +\r\n \"Check the credentials and that they match the mode you are running in — \" +\r\n \"a live key in sandbox, or the reverse, fails exactly like this.\",\r\n { ...ctx, status },\r\n );\r\n }\r\n\r\n if (status >= 500) {\r\n return new ConnectorTransient(`${where}: provider returned ${status}${detail}`, { ...ctx, status });\r\n }\r\n\r\n return new ConnectorRequestError(`${where}: request rejected with ${status}${detail}`, { ...ctx, status });\r\n}\r\n\r\n/**\r\n * Turn a thrown transport failure into the right class.\r\n *\r\n * The default is `ConnectorAmbiguous`, not `ConnectorTransient`. That single\r\n * default is the difference between \"a flaky network costs us a retry\" and \"a\r\n * flaky network costs someone a duplicate charge\".\r\n */\r\nexport function classifyThrown(cause: unknown, ctx: ConnectorErrorContext): ConnectorError {\r\n const classified = classifyError(cause);\r\n const message = `${ctx.service}.${ctx.operation}: ${classified.detail}`;\r\n\r\n return classified.kind === \"unreachable\"\r\n ? new ConnectorUnreachable(message, ctx)\r\n : new ConnectorAmbiguous(message, ctx);\r\n}\r\n\r\n/**\r\n * An HTTP failure carrying its classification, for connectors that decide what\r\n * counts as a failure themselves.\r\n *\r\n * Telegram answers `200 OK` with `{\"ok\": false}` for a real refusal, so its\r\n * connector has to raise the failure itself — and when it does, it must be\r\n * classified the same way every other failure is. `httpFailure(400, …)` is how\r\n * it says \"this is a real no\" without inventing a second vocabulary.\r\n */\r\nexport function httpFailure(status: number, body: string, retryAfter?: string | null): Error {\r\n const classified = classifyStatus(status, body, retryAfter);\r\n const error = new Error(classified.detail) as Error & { classified: Classified };\r\n error.classified = classified;\r\n\r\n return error;\r\n}\r\n\r\nfunction truncate(value: string, max: number): string {\r\n return value.length <= max ? value : `${value.slice(0, max)}…`;\r\n}\r\n","/**\r\n * The faker layer — every connector ships one, always.\r\n *\r\n * ## Why every connector, and not just the ones without a sandbox\r\n *\r\n * A sandbox still needs an account, a key, a network, and a provider that is\r\n * up. That is four ways for someone evaluating the kit to get nothing working,\r\n * before they have learned anything about the kit. A faker removes all four:\r\n * vendor a node, drop it on a canvas, press run, see a shaped result.\r\n *\r\n * It is also what makes the golden fixtures honest. Fixtures run on both\r\n * runtimes and are the publish gate; if they needed a network they would either\r\n * be skipped in CI or be flaky, and a flaky gate is one people learn to\r\n * re-run rather than read.\r\n *\r\n * ## Deterministic, not random\r\n *\r\n * Same inputs, same output — always. A faker that returns a fresh uuid every\r\n * call cannot be asserted on, so its fixtures degrade to \"it did not throw\",\r\n * which is the assertion that catches nothing. Values here are derived from a\r\n * seed built out of the service, the operation and the request, so two runs\r\n * agree and two runtimes can be compared to each other.\r\n *\r\n * The values are obviously fake ON PURPOSE — `fake_`-prefixed ids, `example.test`\r\n * hosts, round numbers. Nobody should ever look at a faked result and wonder\r\n * whether it moved real money.\r\n */\r\n\r\n/** A faker: one function per service, switching on the operation. */\r\nexport type ConnectorFaker = (operation: string, request: FakeRequest) => unknown;\r\n\r\nexport type FakeRequest = {\r\n /** The node's resolved config. */\r\n config: Record<string, unknown>;\r\n /** The value on the incoming wire. */\r\n input?: unknown;\r\n /** Deterministic helpers, seeded from service + operation + config. */\r\n fake: FakeValues;\r\n};\r\n\r\n/**\r\n * Deterministic value helpers handed to a faker.\r\n *\r\n * Small on purpose. A faker's job is to return the SHAPE the provider returns —\r\n * the field names a downstream node will reference — not to simulate the\r\n * provider's business logic.\r\n */\r\nexport type FakeValues = {\r\n /** A stable id with the provider's usual prefix: `id(\"ch\")` → `ch_fake_1a2b3c`. */\r\n id: (prefix: string) => string;\r\n /** A stable integer in `[min, max]`. */\r\n int: (min: number, max: number) => number;\r\n /** Pick a stable element of a list. */\r\n pick: <T>(options: readonly T[]) => T;\r\n /** A fixed ISO-8601 instant, offset by whole seconds. Never `Date.now()`. */\r\n timestamp: (offsetSeconds?: number) => string;\r\n /** A stable lowercase hex string of `length` characters. */\r\n hex: (length: number) => string;\r\n};\r\n\r\n/**\r\n * The instant every faker counts from.\r\n *\r\n * A constant rather than the clock, because a fixture asserting on `createdAt`\r\n * must not start failing tomorrow. Chosen to be recognisably synthetic.\r\n */\r\nexport const FAKE_EPOCH = \"2026-01-01T00:00:00.000Z\";\r\n\r\n/** FNV-1a. Small, dependency-free, and stable across both runtimes. */\r\nexport function seedFrom(...parts: unknown[]): number {\r\n const text = parts.map((part) => (typeof part === \"string\" ? part : stableJson(part))).join(\"|\");\r\n let hash = 0x811c9dc5;\r\n\r\n for (let i = 0; i < text.length; i++) {\r\n hash ^= text.charCodeAt(i);\r\n // 32-bit FNV prime multiply, kept in range without BigInt.\r\n hash = Math.imul(hash, 0x01000193) >>> 0;\r\n }\r\n\r\n return hash >>> 0;\r\n}\r\n\r\n/**\r\n * Key order must not change a seed.\r\n *\r\n * `JSON.stringify` preserves insertion order, so `{a,b}` and `{b,a}` would hash\r\n * differently and the \"same inputs, same output\" promise would hold only for\r\n * objects that happened to be built in the same order — which is exactly the\r\n * kind of almost-true that survives review and fails in a fixture months later.\r\n */\r\nfunction stableJson(value: unknown): string {\r\n if (value === null || typeof value !== \"object\") return JSON.stringify(value) ?? \"null\";\r\n if (Array.isArray(value)) return `[${value.map(stableJson).join(\",\")}]`;\r\n\r\n const entries = Object.entries(value as Record<string, unknown>)\r\n .filter(([, v]) => v !== undefined)\r\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\r\n .map(([k, v]) => `${JSON.stringify(k)}:${stableJson(v)}`);\r\n\r\n return `{${entries.join(\",\")}}`;\r\n}\r\n\r\n/** Build the deterministic helpers for one faked call. */\r\nexport function fakeValues(seed: number): FakeValues {\r\n // A tiny xorshift, advanced per call so successive helpers differ while the\r\n // whole sequence stays a pure function of the seed.\r\n let state = seed || 0x9e3779b9;\r\n const next = (): number => {\r\n state ^= state << 13;\r\n state >>>= 0;\r\n state ^= state >>> 17;\r\n state ^= state << 5;\r\n state >>>= 0;\r\n\r\n return state;\r\n };\r\n\r\n const hex = (length: number): string => {\r\n let out = \"\";\r\n while (out.length < length) out += next().toString(16).padStart(8, \"0\");\r\n\r\n return out.slice(0, length);\r\n };\r\n\r\n return {\r\n hex,\r\n id: (prefix: string) => `${prefix}_fake_${hex(12)}`,\r\n int: (min: number, max: number) => min + (next() % Math.max(1, max - min + 1)),\r\n pick: <T>(options: readonly T[]): T => options[next() % options.length] as T,\r\n timestamp: (offsetSeconds = 0) =>\r\n new Date(Date.parse(FAKE_EPOCH) + offsetSeconds * 1000).toISOString(),\r\n };\r\n}\r\n\r\n/** Build the request handed to a faker, with helpers already seeded. */\r\nexport function fakeRequest(\r\n service: string,\r\n operation: string,\r\n config: Record<string, unknown>,\r\n input?: unknown,\r\n): FakeRequest {\r\n return { config, input, fake: fakeValues(seedFrom(service, operation, config)) };\r\n}\r\n","/**\r\n * Which copy of a provider a connector talks to — and who decides.\r\n *\r\n * Three modes, and the third is the one that makes this marketplace usable:\r\n *\r\n * | mode | talks to | needs credentials | needs network |\r\n * |-----------|-----------------------------|-------------------|---------------|\r\n * | `live` | the provider, for real | yes | yes |\r\n * | `sandbox` | the provider's test estate | yes (test ones) | yes |\r\n * | `fake` | the node's own faker | no | no |\r\n *\r\n * ## The environment is the DEFAULT, never the constraint\r\n *\r\n * A locally-hosted project defaults to sandbox. It is not *pinned* to sandbox:\r\n * an author who explicitly asks for `live` on their laptop gets `live`, because\r\n * the alternative — an environment that silently overrides a stated intention —\r\n * produces the worst outcome available here, which is a workflow that reports\r\n * success while having charged nobody. Equally, `sandbox` set explicitly stays\r\n * sandbox in production, which is how you stage a connector before cutting it\r\n * over.\r\n *\r\n * So: explicit beats the connection, the connection beats the environment.\r\n *\r\n * ## Why \"fake\" is a MODE and not a test double\r\n *\r\n * Every connector ships a faker (see `faker.ts`), and it is reachable the same\r\n * way in a test, in a demo and on a laptop with no credentials. Making it a\r\n * separate mechanism — a mock injected only under test — would mean the code\r\n * path a consumer develops against is not the one that runs, and the fixtures\r\n * would prove nothing about the executor.\r\n */\r\n\r\n/** The three environments a connector node can run against. */\r\nexport type ConnectorMode = \"fake\" | \"sandbox\" | \"live\";\r\n\r\n/** What a node's `mode` config field may say. `auto` means \"let the rules decide\". */\r\nexport type RequestedMode = ConnectorMode | \"auto\";\r\n\r\nexport const CONNECTOR_MODES: ConnectorMode[] = [\"fake\", \"sandbox\", \"live\"];\r\n\r\n/**\r\n * How a provider exposes its test estate — the four shapes that actually exist.\r\n *\r\n * This is data, not per-node code, because getting it wrong is the difference\r\n * between hitting a test ledger and hitting a real one. Verified per provider in\r\n * `.ai/plans/fancy-flow-connector-nodes.md`; a node declares which of the four\r\n * it is and the core does the rest.\r\n *\r\n * - `credential` — same base URL, a test-scoped key selects the estate\r\n * (Stripe's `sk_test_…`). The trap: pointing a live key at it works, so the\r\n * only guard is the key itself.\r\n * - `base-url` — a different host entirely (PayPal's `api-m.sandbox.paypal.com`).\r\n * - `separate-account` — a distinct tenant you must create, often with its own\r\n * login host (Salesforce sandbox orgs on `test.salesforce.com`).\r\n * - `none` — the provider has no test estate. `sandbox` is then not offered at\r\n * all, and the node's honest choices are `fake` and `live`.\r\n */\r\nexport type SandboxKind = \"credential\" | \"base-url\" | \"separate-account\" | \"none\";\r\n\r\n/** The host's view of where it is running. Kept tiny so it is trivial to fake. */\r\nexport type HostEnvironment = {\r\n /** True only in a real production deployment. Everything else is \"local\". */\r\n production: boolean;\r\n};\r\n\r\nexport type ModeInputs = {\r\n /** What the node's config asked for. `undefined` / `\"auto\"` means \"decide\". */\r\n requested?: RequestedMode | null;\r\n /** A mode pinned on the CONNECTION, if the host pinned one. */\r\n connectionMode?: ConnectorMode | null;\r\n /** Whether the provider has a test estate at all. */\r\n sandbox: SandboxKind;\r\n /** Whether the connection actually carries credentials for the sandbox estate. */\r\n hasSandboxCredentials: boolean;\r\n environment: HostEnvironment;\r\n};\r\n\r\n/**\r\n * Resolve the mode a connector call runs in.\r\n *\r\n * Deliberately a pure function of stated facts, so both runtimes can be held to\r\n * the same table of cases and a host can unit-test its own wiring without a\r\n * network or a provider.\r\n */\r\nexport function resolveConnectorMode(inputs: ModeInputs): ConnectorMode {\r\n const { requested, connectionMode, sandbox, hasSandboxCredentials, environment } = inputs;\r\n\r\n // 1. An explicit ask wins everywhere. This is the rule that makes the\r\n // environment a default rather than a cage.\r\n if (requested && requested !== \"auto\") {\r\n if (requested === \"sandbox\" && sandbox === \"none\") {\r\n throw new ConnectorModeError(\r\n 'This connector\\'s provider has no sandbox estate, so \"sandbox\" cannot be honoured. ' +\r\n 'Use \"fake\" to develop without credentials, or \"live\" to talk to the provider.',\r\n );\r\n }\r\n\r\n return requested;\r\n }\r\n\r\n // 2. A mode pinned on the connection. One place per service, which is the\r\n // whole reason connections exist as a separate thing.\r\n if (connectionMode) return connectionMode;\r\n\r\n // 3. Otherwise the environment decides.\r\n if (environment.production) return \"live\";\r\n\r\n // Local: prefer the provider's own test estate when it exists AND is wired.\r\n // Falling through to `fake` when it is not is what makes a freshly vendored\r\n // node runnable with no setup at all — the difference between a marketplace\r\n // you can try and one you can only read about.\r\n return sandbox !== \"none\" && hasSandboxCredentials ? \"sandbox\" : \"fake\";\r\n}\r\n\r\n/** Raised when a requested mode cannot be honoured. Never downgraded silently. */\r\nexport class ConnectorModeError extends Error {\r\n constructor(message: string) {\r\n super(message);\r\n this.name = \"ConnectorModeError\";\r\n }\r\n}\r\n\r\n/** True when the mode reaches the provider over the network. */\r\nexport function isRemote(mode: ConnectorMode): boolean {\r\n return mode !== \"fake\";\r\n}\r\n","/**\r\n * A CONNECTION — credentials plus an environment choice — configured once per\r\n * service and referenced by every node that uses it.\r\n *\r\n * ## Why this is not per-node config\r\n *\r\n * The obvious design is to put an API key field on each node. It is also wrong,\r\n * for three reasons that only show up later:\r\n *\r\n * 1. **Rotation.** A key on twelve nodes is twelve edits and one you will miss.\r\n * 2. **The sandbox/live switch has to live somewhere singular.** If each node\r\n * carries its own, a graph can be half in the test estate and half in the\r\n * real one, and nothing about the canvas shows it.\r\n * 3. **Credentials would be in the graph.** A `WorkflowSchema` is plain JSON\r\n * that gets exported, committed, MCP'd to an agent and pasted into issues.\r\n * Nothing secret may ever be in it.\r\n *\r\n * So a node's config stores a **connection id** — an opaque, non-secret name\r\n * like `\"stripe\"` or `\"stripe-eu\"` — and the host resolves it to credentials at\r\n * run time. This is the same arrangement fancy-flow already uses for LLM clients\r\n * and `fancy-git` uses for providers: the node cannot reach for a credential,\r\n * the host has to hand one over.\r\n *\r\n * ```ts\r\n * registerConnectionHost({\r\n * environment: { production: process.env.NODE_ENV === \"production\" },\r\n * connections: {\r\n * stripe: {\r\n * service: \"stripe\",\r\n * live: { secretKey: process.env.STRIPE_SECRET_KEY },\r\n * sandbox: { secretKey: process.env.STRIPE_TEST_SECRET_KEY },\r\n * },\r\n * },\r\n * });\r\n * ```\r\n *\r\n * Note what is NOT here: no key, no token, no placeholder that looks like one.\r\n * Credentials are configuration, and configuration comes from the consumer's\r\n * environment.\r\n */\r\n\r\nimport { ConnectorConfigError } from \"./errors\";\r\nimport {\r\n ConnectorModeError,\r\n resolveConnectorMode,\r\n type ConnectorMode,\r\n type HostEnvironment,\r\n type RequestedMode,\r\n type SandboxKind,\r\n} from \"./mode\";\r\n\r\n/**\r\n * Credentials for one estate. Values are opaque strings the node's auth\r\n * strategy understands — `secretKey`, `clientId`/`clientSecret`, `token`.\r\n */\r\nexport type ConnectionCredentials = Record<string, string | undefined>;\r\n\r\n/** One configured connection to one service. */\r\nexport type ConnectionSpec = {\r\n /** The service this connection is for — must match the node's `connector.service`. */\r\n service: string;\r\n /** Human label, shown in the picker. Defaults to the id. */\r\n label?: string;\r\n /**\r\n * Pin this connection to a mode. Optional, and usually absent: leaving it\r\n * unset is what lets the environment supply the default.\r\n */\r\n mode?: ConnectorMode;\r\n live?: ConnectionCredentials;\r\n sandbox?: ConnectionCredentials;\r\n /**\r\n * Override the base URL per mode. Only needed for a `separate-account`\r\n * provider whose tenant has its own host (a Salesforce instance URL); the\r\n * `base-url` providers carry theirs in the node's service descriptor.\r\n */\r\n baseUrl?: Partial<Record<ConnectorMode, string>>;\r\n};\r\n\r\nexport type ConnectionHost = {\r\n environment: HostEnvironment;\r\n connections: Record<string, ConnectionSpec>;\r\n};\r\n\r\nlet host: ConnectionHost | null = null;\r\n\r\n/**\r\n * Install the host's connections. Returns an unregister function.\r\n *\r\n * One registration for the whole app, matching how a person thinks about it:\r\n * you authorize Stripe once, then every node that needs Stripe already has it.\r\n */\r\nexport function registerConnectionHost(next: ConnectionHost | null): () => void {\r\n host = next;\r\n\r\n return () => {\r\n if (host === next) host = null;\r\n };\r\n}\r\n\r\nexport function getConnectionHost(): ConnectionHost | null {\r\n return host;\r\n}\r\n\r\n/** Connection ids the host has configured for a service — feeds the picker. */\r\nexport function connectionsFor(service: string): Array<{ id: string; label: string }> {\r\n const current = getConnectionHost();\r\n if (!current) return [];\r\n\r\n return Object.entries(current.connections)\r\n .filter(([, spec]) => spec.service === service)\r\n .map(([id, spec]) => ({ id, label: spec.label ?? id }));\r\n}\r\n\r\n/** What a node's executor actually needs, once everything has been resolved. */\r\nexport type ResolvedConnection = {\r\n id: string;\r\n service: string;\r\n mode: ConnectorMode;\r\n /** Empty in `fake` mode — there is nothing to authenticate against. */\r\n credentials: ConnectionCredentials;\r\n /** Undefined in `fake` mode. */\r\n baseUrl?: string;\r\n};\r\n\r\nexport type ResolveOptions = {\r\n service: string;\r\n operation: string;\r\n /** The connection id from host config. Defaults to the service name. */\r\n connectionId?: string | null;\r\n /** The mode the caller asked for. */\r\n requested?: RequestedMode | null;\r\n /**\r\n * Credentials handed straight in, bypassing the registry entirely.\r\n *\r\n * The primary path for a host that already owns credential storage and has no\r\n * wish to hand a package a second copy: it passes them per call, this resolver\r\n * does not consult any global, and there is nothing to initialise. The\r\n * registry below remains for hosts that prefer to configure once.\r\n *\r\n * An EMPTY object is not the same as `null`: it means \"the caller supplied\r\n * credentials and there are none\", which fails the `requires` check loudly\r\n * rather than falling through to the registry and silently using someone\r\n * else's.\r\n */\r\n credentials?: ConnectionCredentials | null;\r\n sandbox: SandboxKind;\r\n /** Base URL per mode, from the node's service descriptor. */\r\n baseUrls?: Partial<Record<ConnectorMode, string>>;\r\n /** Credential keys that must be present for a remote call. */\r\n requires?: string[];\r\n};\r\n\r\n/**\r\n * Resolve the connection and mode for one call.\r\n *\r\n * ## `fake` needs no host at all\r\n *\r\n * A freshly vendored connector runs before anything is configured, and that is\r\n * deliberate: it is the difference between a marketplace someone can try and\r\n * one they can only read about. So the no-host case resolves to `fake` rather\r\n * than throwing — UNLESS the author explicitly asked for a remote mode, in\r\n * which case the ask is honoured by failing loudly instead of quietly doing\r\n * something else.\r\n */\r\nexport function resolveConnection(options: ResolveOptions): ResolvedConnection {\r\n const { service, operation, sandbox } = options;\r\n const requestedMode = options.requested ?? \"auto\";\r\n\r\n // Checked FIRST, before anything about the host, because it is a static fact\r\n // about the SERVICE. Asking for a sandbox that does not exist should say so\r\n // whether or not a connection happens to be configured — otherwise the same\r\n // mistake produces two different errors depending on unrelated state, and the\r\n // more useful one is the one you never see.\r\n if (requestedMode === \"sandbox\" && sandbox === \"none\") {\r\n throw new ConnectorModeError(\r\n `${service} has no sandbox estate, so \"sandbox\" cannot be honoured. ` +\r\n 'Use \"fake\" to develop without credentials, or \"live\" to talk to the provider.',\r\n );\r\n }\r\n\r\n const id = options.connectionId?.trim() || service;\r\n\r\n // Explicit credentials win over everything. A host that passes them has\r\n // already made every decision this resolver would otherwise make, and reading\r\n // a global on top of that is how two sources of truth start disagreeing.\r\n if (options.credentials) {\r\n // Asking for the faker wins even here — an explicit `fake` is a statement,\r\n // and credentials sitting in the same call do not override it. Nothing\r\n // secret is carried into the fake path.\r\n if (requestedMode === \"fake\") return { id, service, mode: \"fake\", credentials: {} };\r\n\r\n // Otherwise: supplying credentials IS the statement that a real call is\r\n // intended, so `auto` resolves to `live` rather than consulting an\r\n // environment this resolver was told nothing about. `sandbox` is honoured\r\n // as asked.\r\n const mode: ConnectorMode = requestedMode === \"auto\" ? \"live\" : requestedMode;\r\n const missing = (options.requires ?? []).filter((key) => !options.credentials?.[key]);\r\n\r\n if (missing.length > 0) {\r\n throw new ConnectorConfigError(\r\n `${service}.${operation} was given credentials with no ${missing.join(\", \")}. ` +\r\n \"Nothing is inferred from an incomplete credential set — a partial one is a configuration mistake, \" +\r\n \"and quietly falling back to another source is how the wrong account gets written to.\",\r\n { service, operation },\r\n );\r\n }\r\n\r\n const explicitBase = options.baseUrls?.[mode];\r\n\r\n return {\r\n id,\r\n service,\r\n mode,\r\n credentials: options.credentials,\r\n ...(explicitBase ? { baseUrl: explicitBase } : {}),\r\n };\r\n }\r\n\r\n const current = getConnectionHost();\r\n const spec = current?.connections[id];\r\n\r\n if (spec && spec.service !== service) {\r\n throw new ConnectorConfigError(\r\n `connection \"${id}\" is configured for the \"${spec.service}\" service, but this node needs \"${service}\".`,\r\n { service, operation },\r\n );\r\n }\r\n\r\n if (!current || !spec) {\r\n if (requestedMode !== \"auto\" && requestedMode !== \"fake\") {\r\n throw new ConnectorConfigError(\r\n `no \"${id}\" connection is registered, so ${service}.${operation} cannot run in \"${requestedMode}\" mode. ` +\r\n \"Call registerConnectionHost({ connections: { \" +\r\n `${id}: { service: \"${service}\", live: { … } } } }) — the node has no credentials of its own ` +\r\n 'and must not invent any. Set the node\\'s mode to \"fake\" to develop without one.',\r\n { service, operation },\r\n );\r\n }\r\n\r\n return { id, service, mode: \"fake\", credentials: {} };\r\n }\r\n\r\n const sandboxCreds = spec.sandbox ?? {};\r\n const mode = resolveConnectorMode({\r\n requested: requestedMode,\r\n connectionMode: spec.mode ?? null,\r\n sandbox,\r\n hasSandboxCredentials: hasAll(sandboxCreds, options.requires ?? []),\r\n environment: current.environment,\r\n });\r\n\r\n if (mode === \"fake\") {\r\n return { id, service, mode, credentials: {} };\r\n }\r\n\r\n const credentials = (mode === \"sandbox\" ? spec.sandbox : spec.live) ?? {};\r\n const missing = (options.requires ?? []).filter((key) => !credentials[key]);\r\n\r\n if (missing.length > 0) {\r\n // Never silently degrade to the faker here. A run that \"succeeded\" because\r\n // it quietly stopped talking to the provider is the worst failure this\r\n // whole design exists to prevent — it is green, it is wrong, and nothing\r\n // reports it.\r\n throw new ConnectorConfigError(\r\n `connection \"${id}\" has no ${missing.join(\", \")} for ${mode} mode. ` +\r\n `Set connections.${id}.${mode}.${missing[0]}, or run this node in \"fake\" mode.`,\r\n { service, operation },\r\n );\r\n }\r\n\r\n const baseUrl = spec.baseUrl?.[mode] ?? options.baseUrls?.[mode];\r\n\r\n return { id, service, mode, credentials, ...(baseUrl ? { baseUrl } : {}) };\r\n}\r\n\r\nfunction hasAll(creds: ConnectionCredentials, keys: string[]): boolean {\r\n return keys.length > 0 && keys.every((key) => Boolean(creds[key]));\r\n}\r\n","/**\r\n * The one call path every connector uses.\r\n *\r\n * A connector never chooses between \"call the provider\" and \"call the faker\" —\r\n * it describes the request it wants and hands it here. That is the point: the\r\n * code path a consumer develops against with no credentials is the same code\r\n * path that runs in production, so fixtures prove something about the connector\r\n * rather than about a mock.\r\n *\r\n * ```ts\r\n * const charge = await callConnector(STRIPE, {\r\n * operation: \"charge_create\",\r\n * credentials,\r\n * request: { method: \"POST\", path: \"/v1/charges\", form: { amount, currency } },\r\n * idempotencyKey,\r\n * idempotent: true,\r\n * });\r\n * ```\r\n *\r\n * ## What changed from the first version of this runtime, and why\r\n *\r\n * The old loop caught a thrown transport, called it transient, and retried it.\r\n * A thrown transport includes a **timeout**, which may be a request the provider\r\n * received and acted on — so on a connector with no idempotency key that retry\r\n * is a silent double write. Retrying is now a function of the classification AND\r\n * the connector's declared idempotency, and `idempotent` defaults to `false`\r\n * because a connector that has not thought about it must not be assumed safe.\r\n *\r\n * See `delivery.ts` for the full table.\r\n */\r\n\r\nimport {\r\n DEFAULT_RETRY,\r\n deliver,\r\n type Attempt,\r\n type Classified,\r\n type DeliveryOutcome,\r\n type FailureKind,\r\n} from \"./delivery\";\r\nimport {\r\n classifyHttp,\r\n classifyThrown,\r\n ConnectorConfigError,\r\n ConnectorError,\r\n type ConnectorErrorContext,\r\n} from \"./errors\";\r\nimport { fakeRequest, type ConnectorFaker } from \"./faker\";\r\nimport type { ConnectorMode, RequestedMode, SandboxKind } from \"./mode\";\r\nimport {\r\n resolveConnection,\r\n type ConnectionCredentials,\r\n type ResolvedConnection,\r\n} from \"./connection\";\r\n\r\n/**\r\n * Everything true of a SERVICE rather than of one operation.\r\n *\r\n * One per provider, declared once and shared by that provider's connectors. It\r\n * is data — base URLs, required credential keys, how the sandbox is selected —\r\n * so that facts verified against provider documentation live in exactly one\r\n * reviewable place instead of being retyped into each connector.\r\n */\r\nexport type ServiceDescriptor = {\r\n service: string;\r\n title: string;\r\n sandbox: SandboxKind;\r\n /**\r\n * Base URL per mode. A `credential`-sandbox provider gives the same URL for\r\n * both, which is itself worth writing down: it is why a live key pointed at\r\n * \"sandbox\" quietly reaches production.\r\n */\r\n baseUrls: Partial<Record<ConnectorMode, string>>;\r\n /** Credential keys a remote call cannot proceed without. */\r\n requires: string[];\r\n /**\r\n * Apply the provider's auth scheme to an outgoing request.\r\n *\r\n * A FUNCTION rather than a declarative header name, because there is no common\r\n * shape: the key can be a header (under any of a dozen names), a Basic\r\n * username with a blank password, a query parameter, a body field, or a URL\r\n * path segment — and several providers need more than one at once. It receives\r\n * the resolved mode too, because for some providers auth and estate are the\r\n * same decision expressed in the URL.\r\n */\r\n authorize: (\r\n credentials: ConnectionCredentials,\r\n request: PreparedRequest,\r\n mode: ConnectorMode,\r\n ) => void | Promise<void>;\r\n /** The service's faker. Required — see `faker.ts`. */\r\n faker: ConnectorFaker;\r\n /** Header the provider uses for idempotency, when it has one. */\r\n idempotencyHeader?: string;\r\n};\r\n\r\nexport type ConnectorRequest = {\r\n method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\r\n path: string;\r\n query?: Record<string, string | number | boolean | undefined>;\r\n headers?: Record<string, string>;\r\n /** JSON body. Mutually exclusive with `form`. */\r\n json?: unknown;\r\n /** `application/x-www-form-urlencoded` body — what several payment APIs want. */\r\n form?: Record<string, string | number | boolean | undefined>;\r\n};\r\n\r\nexport type PreparedRequest = {\r\n method: string;\r\n url: string;\r\n headers: Record<string, string>;\r\n body?: string;\r\n};\r\n\r\n/** The HTTP seam. A host may swap it; nothing here assumes a browser or Node. */\r\nexport type Transport = (request: PreparedRequest) => Promise<TransportResponse>;\r\n\r\nexport type TransportResponse = {\r\n status: number;\r\n headers: Record<string, string>;\r\n body: string;\r\n};\r\n\r\nexport type CallOptions = {\r\n operation: string;\r\n /**\r\n * Credentials, passed IN.\r\n *\r\n * **Nothing in this package reads `process.env`.** A host stores values\r\n * wherever it stores values — a gitignored file behind a whitelist, a secret\r\n * manager, a Laravel config — and hands them over per call. A package that\r\n * reached for the environment itself would bypass that discipline entirely,\r\n * and a test in this repo asserts that it does not.\r\n *\r\n * Omit to resolve through a registered connection host instead; see\r\n * `connection.ts`.\r\n */\r\n credentials?: ConnectionCredentials;\r\n /** Which estate to use. Resolved from the connection host when omitted. */\r\n mode?: ConnectorMode;\r\n /** Connection id, for the registered-host path. */\r\n connectionId?: string | null;\r\n /**\r\n * The caller's own config for this call.\r\n *\r\n * Passed to the faker so its output is deterministic, and read for\r\n * `connection` and `mode` when those were not given explicitly — which is what\r\n * a workflow node stores them in. Explicit arguments always win; this is a\r\n * convenience for a host whose configuration IS a config object, not a second\r\n * source of truth.\r\n */\r\n config?: Record<string, unknown>;\r\n input?: unknown;\r\n request: ConnectorRequest;\r\n /**\r\n * Value for the provider's idempotency header. Derive it from the run and the\r\n * step, never a fresh uuid: the entire point is that a RETRY carries the same\r\n * key. See `idempotency.ts`.\r\n */\r\n idempotencyKey?: string;\r\n /**\r\n * Whether repeating this exact request is harmless.\r\n *\r\n * **Defaults to `false`.** The only thing that makes an ambiguous failure\r\n * retryable, so it is opt-in and has to be true because the provider makes it\r\n * true — not because a retry would be convenient.\r\n */\r\n idempotent?: boolean;\r\n /** Retry budget for retryable failures. Total attempts, including the first. */\r\n attempts?: number;\r\n /** Override the transport for this call. Otherwise the registered one, then `fetch`. */\r\n transport?: Transport;\r\n /**\r\n * Called for each FAILED attempt once the call has finished, so a host can\r\n * journal what actually happened. A call that worked on the third try is a\r\n * different operational fact from one that worked immediately.\r\n *\r\n * A callback rather than a field on the result, because the result is a\r\n * published shape and this is a diagnostic — see `ConnectorResult`.\r\n */\r\n onAttempt?: (attempt: Attempt) => void;\r\n};\r\n\r\n/**\r\n * What a connector call returns, alongside the provider's own payload.\r\n *\r\n * The mode is REPORTED, never inferred by the caller. A call that emitted the\r\n * provider's data alone would leave every downstream reader — a human, an agent,\r\n * a log — unable to tell a faked result from a real one, which is the single\r\n * most important fact about a connector run.\r\n *\r\n * **Retry history is deliberately NOT here.** It was, briefly, and the flow\r\n * node marketplace's cross-runtime fixtures caught it immediately: this object\r\n * is published on a node's output port and referenced by templates and agents,\r\n * and PHP's `ConnectorResult::toArray()` does not carry it. Two runtimes\r\n * publishing different shapes is the exact drift those fixtures exist to\r\n * prevent, and a diagnostic field is a poor reason to break it. Pass\r\n * `onAttempt` to observe retries.\r\n */\r\nexport type ConnectorResult<T = unknown> = {\r\n data: T;\r\n /** Which estate this actually ran against. Always reported, never inferred. */\r\n mode: ConnectorMode;\r\n connection: string;\r\n};\r\n\r\nlet registeredTransport: Transport | null = null;\r\n\r\n/** Install a transport. Only needed if the runtime has no global `fetch`. */\r\nexport function registerTransport(next: Transport | null): void {\r\n registeredTransport = next;\r\n}\r\n\r\nexport async function callConnector<T = unknown>(\r\n service: ServiceDescriptor,\r\n options: CallOptions,\r\n): Promise<ConnectorResult<T>> {\r\n const config = options.config ?? {};\r\n const connection = resolveConnection({\r\n service: service.service,\r\n operation: options.operation,\r\n connectionId: options.connectionId ?? readString(config, \"connection\"),\r\n requested: options.mode ?? (readString(config, \"mode\") as RequestedMode | null),\r\n credentials: options.credentials ?? null,\r\n sandbox: service.sandbox,\r\n baseUrls: service.baseUrls,\r\n requires: service.requires,\r\n });\r\n\r\n if (connection.mode === \"fake\") {\r\n return {\r\n data: service.faker(\r\n options.operation,\r\n fakeRequest(service.service, options.operation, config, options.input),\r\n ) as T,\r\n mode: \"fake\",\r\n connection: connection.id,\r\n };\r\n }\r\n\r\n const ctx: ConnectorErrorContext = { service: service.service, operation: options.operation };\r\n const prepared = await prepare(service, connection, options);\r\n const send = options.transport ?? registeredTransport ?? fetchTransport;\r\n\r\n const outcome = await deliver<unknown>(\r\n async () => {\r\n let response: TransportResponse;\r\n\r\n try {\r\n response = await send(prepared);\r\n } catch (cause) {\r\n // classifyThrown decides unreachable vs ambiguous. It never returns a\r\n // 5xx-shaped \"transient\", because nothing here reached the provider.\r\n throw classifyThrown(cause, ctx);\r\n }\r\n\r\n if (response.status < 400) {\r\n return response.body.trim() === \"\" ? null : parseJson(response.body, ctx);\r\n }\r\n\r\n throw classifyHttp(response.status, ctx, response.body, readRetryAfter(response.headers));\r\n },\r\n {\r\n ...DEFAULT_RETRY,\r\n attempts: options.attempts ?? DEFAULT_RETRY.attempts,\r\n idempotent: options.idempotent ?? false,\r\n },\r\n );\r\n\r\n if (options.onAttempt) {\r\n for (const attempt of outcome.attempts) options.onAttempt(attempt);\r\n }\r\n\r\n if (!outcome.ok) {\r\n throw failureFrom(outcome, ctx, options.idempotent ?? false);\r\n }\r\n\r\n return { data: outcome.value as T, mode: connection.mode, connection: connection.id };\r\n}\r\n\r\n/**\r\n * Turn an exhausted delivery into the error a host sees.\r\n *\r\n * An ambiguous failure on a non-idempotent connector gets a message that says\r\n * *go and look*, because that is the action. \"Request failed\" would send someone\r\n * to re-run it, which is the one thing that must not happen.\r\n */\r\nfunction failureFrom(\r\n outcome: DeliveryOutcome<unknown>,\r\n ctx: ConnectorErrorContext,\r\n idempotent: boolean,\r\n): ConnectorError {\r\n const last = outcome.attempts.at(-1);\r\n const kind: FailureKind = outcome.kind ?? last?.kind ?? \"ambiguous\";\r\n const error = new ConnectorError(outcome.gaveUp ?? `${ctx.service}.${ctx.operation} failed.`, ctx);\r\n\r\n // `kind` is readonly on the class; this is the one place that knows the\r\n // aggregate answer across attempts, so it is set here rather than guessed.\r\n Object.defineProperty(error, \"kind\", { value: kind, enumerable: false });\r\n Object.defineProperty(error, \"attempts\", { value: outcome.attempts, enumerable: false });\r\n Object.defineProperty(error, \"idempotent\", { value: idempotent, enumerable: false });\r\n\r\n return error;\r\n}\r\n\r\nasync function prepare(\r\n service: ServiceDescriptor,\r\n connection: ResolvedConnection,\r\n options: CallOptions,\r\n): Promise<PreparedRequest> {\r\n const base = connection.baseUrl ?? service.baseUrls[connection.mode];\r\n\r\n if (!base) {\r\n throw new ConnectorConfigError(\r\n `${service.service}: no base URL for \"${connection.mode}\" mode. ` +\r\n \"The service descriptor must declare one per mode it supports.\",\r\n { service: service.service, operation: options.operation },\r\n );\r\n }\r\n\r\n const { method, path, query, headers, json, form } = options.request;\r\n const url = new URL(path.replace(/^\\/+/, \"\"), base.endsWith(\"/\") ? base : `${base}/`);\r\n\r\n for (const [key, value] of Object.entries(query ?? {})) {\r\n if (value !== undefined) url.searchParams.set(key, String(value));\r\n }\r\n\r\n const prepared: PreparedRequest = {\r\n method,\r\n url: url.toString(),\r\n headers: { Accept: \"application/json\", ...headers },\r\n };\r\n\r\n if (form) {\r\n prepared.headers[\"Content-Type\"] = \"application/x-www-form-urlencoded\";\r\n prepared.body = encodeForm(form);\r\n } else if (json !== undefined) {\r\n prepared.headers[\"Content-Type\"] = \"application/json\";\r\n prepared.body = JSON.stringify(json);\r\n }\r\n\r\n if (options.idempotencyKey && service.idempotencyHeader) {\r\n prepared.headers[service.idempotencyHeader] = options.idempotencyKey;\r\n }\r\n\r\n await service.authorize(connection.credentials, prepared, connection.mode);\r\n\r\n return prepared;\r\n}\r\n\r\n/**\r\n * Form encoding, including the bracketed nesting several payment APIs use\r\n * (`metadata[order_id]=7`). Flat maps pass through unchanged.\r\n */\r\nfunction encodeForm(form: Record<string, unknown>): string {\r\n const pairs: string[] = [];\r\n\r\n const walk = (prefix: string, value: unknown): void => {\r\n if (value === undefined || value === null) return;\r\n\r\n if (Array.isArray(value)) {\r\n value.forEach((item, index) => walk(`${prefix}[${index}]`, item));\r\n\r\n return;\r\n }\r\n\r\n if (typeof value === \"object\") {\r\n for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {\r\n walk(prefix === \"\" ? key : `${prefix}[${key}]`, nested);\r\n }\r\n\r\n return;\r\n }\r\n\r\n pairs.push(`${encodeURIComponent(prefix)}=${encodeURIComponent(String(value))}`);\r\n };\r\n\r\n walk(\"\", form);\r\n\r\n return pairs.join(\"&\");\r\n}\r\n\r\nconst fetchTransport: Transport = async (request) => {\r\n const globalFetch = (globalThis as { fetch?: typeof fetch }).fetch;\r\n\r\n if (!globalFetch) {\r\n throw new Error(\r\n \"no global fetch in this runtime — call registerTransport() with one before running a connector remotely.\",\r\n );\r\n }\r\n\r\n const response = await globalFetch(request.url, {\r\n method: request.method,\r\n headers: request.headers,\r\n ...(request.body === undefined ? {} : { body: request.body }),\r\n });\r\n\r\n const headers: Record<string, string> = {};\r\n response.headers.forEach((value, key) => {\r\n headers[key.toLowerCase()] = value;\r\n });\r\n\r\n return { status: response.status, headers, body: await response.text() };\r\n};\r\n\r\nfunction parseJson(body: string, ctx: ConnectorErrorContext): unknown {\r\n try {\r\n return JSON.parse(body);\r\n } catch {\r\n // A body that is not JSON on a 2xx is a REJECTION, not something to retry:\r\n // the same request will produce the same unparseable body.\r\n const error = new ConnectorError(\r\n `${ctx.service}.${ctx.operation}: the provider returned a body that is not JSON.`,\r\n ctx,\r\n );\r\n Object.defineProperty(error, \"kind\", { value: \"rejected\", enumerable: false });\r\n\r\n throw error;\r\n }\r\n}\r\n\r\nfunction readRetryAfter(headers: Record<string, string>): number | undefined {\r\n const raw = headers[\"retry-after\"];\r\n if (!raw) return undefined;\r\n const seconds = Number(raw);\r\n\r\n return Number.isFinite(seconds) ? seconds : undefined;\r\n}\r\n\r\nexport type { Classified };\r\n\r\nfunction readString(config: Record<string, unknown>, key: string): string | null {\r\n const value = config[key];\r\n\r\n return typeof value === \"string\" && value.trim() !== \"\" ? value.trim() : null;\r\n}\r\n","/**\r\n * API drift — being told the provider changed, rather than discovering it.\r\n *\r\n * ## The failure this addresses\r\n *\r\n * A connector's worst failure is silent. The provider changes its API and\r\n * nothing says so until something breaks in production — and by then the person\r\n * who wrote the connector has moved on, the reason it worked is gone, and the\r\n * first symptom is usually a 400 that reads like bad input.\r\n *\r\n * ## What the system DOES on drift: it REPORTS. It never adapts.\r\n *\r\n * This is the load-bearing decision and it is deliberate.\r\n *\r\n * A connector that reshapes itself around a changed API is worse than one that\r\n * fails loudly, for three reasons that compound:\r\n *\r\n * 1. **It cannot be reviewed.** The bytes that go out stop being a function of\r\n * the source someone read. A host that guarantees \"what was approved is what\r\n * was sent\" cannot make that guarantee about code that rewrote itself.\r\n * 2. **Its behaviour depends on WHEN it ran**, because it depends on what the\r\n * spec said at that moment. Two runs of the same workflow on the same input\r\n * can differ, and nothing in the run record explains why.\r\n * 3. **Adaptation is only ever a guess at intent.** A renamed field might be a\r\n * rename or might be a different field with a similar name; a removed one\r\n * might be gone or moved. Getting it wrong writes wrong data confidently.\r\n *\r\n * ## And it never changes RUNTIME behaviour either\r\n *\r\n * A drift check runs out of band — on a schedule, in CI, on demand. It does not\r\n * fail a call that is working. If the spec changed and the call still succeeds,\r\n * failing the run would be a self-inflicted outage caused by a document. The\r\n * only thing that changes runtime behaviour is the provider actually refusing,\r\n * which the error taxonomy already handles.\r\n *\r\n * So: **drift reports; the provider decides; a person acts.**\r\n *\r\n * ## Two detection strategies, because most providers publish nothing\r\n *\r\n * 1. **Specs**, where a provider publishes a machine-readable one at a stable\r\n * URL. Checked against the connector's declared slice — see `ApiContract`.\r\n * 2. **Recorded shapes**, where it does not. A fixture of the response fields a\r\n * connector reads, compared against a live response on a schedule. Field\r\n * NAMES only; values are never recorded, because a recorded response from a\r\n * real account is a data leak wearing a test fixture's clothes.\r\n *\r\n * The second is the fallback and is designed anyway, because it is the majority\r\n * case: of the providers surveyed for this package, the ones publishing a\r\n * maintained spec are a minority, and publishing one is not the same as\r\n * maintaining it — `slackapi/slack-api-specs` has not been touched since 2021.\r\n */\r\n\r\n/* ── What a connector declares it depends on ─────────────────────────────── */\r\n\r\n/**\r\n * One operation a connector calls.\r\n *\r\n * The `path` is the TEMPLATE, matching the provider's own documentation, so it\r\n * can be looked up in a spec: `/v1/charges/{charge}`, not the interpolated URL.\r\n */\r\nexport type ContractOperation = {\r\n /** Stable name, matching the connector's own operation id. */\r\n operation: string;\r\n method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\r\n path: string;\r\n /** Request fields the connector SENDS. Drift here breaks writes. */\r\n sends?: string[];\r\n /** Response fields the connector READS. Drift here breaks reads, silently. */\r\n reads?: string[];\r\n};\r\n\r\n/**\r\n * Where a provider's machine-readable description lives.\r\n *\r\n * `kind` matters because \"machine-readable\" is not one format. AT Protocol\r\n * publishes **lexicons**, not OpenAPI, and a checker that assumed OpenAPI would\r\n * report every AT Protocol connector as unspecified — which is a different and\r\n * much worse answer than \"specified, in another format\".\r\n */\r\nexport type SpecSource =\r\n | { kind: \"openapi\"; url: string; note?: string }\r\n | { kind: \"lexicon\"; url: string; note?: string }\r\n | {\r\n kind: \"none\";\r\n /**\r\n * Why there is no spec, and what the fallback is. Required — \"none\" with\r\n * no reason is indistinguishable from \"nobody looked\".\r\n */\r\n note: string;\r\n };\r\n\r\n/** Everything a connector declares about the API it was written against. */\r\nexport type ApiContract = {\r\n connector: string;\r\n /** Base URL the operations hang off, for a human reading the report. */\r\n baseUrl?: string;\r\n spec: SpecSource;\r\n operations: ContractOperation[];\r\n /** When a person last read the provider's documentation for this contract. */\r\n reviewedOn: string;\r\n};\r\n\r\n/* ── What a check produces ───────────────────────────────────────────────── */\r\n\r\nexport type DriftFinding = {\r\n /**\r\n * `missing-operation` — the path or method is gone. A write will start failing.\r\n * `missing-request-field` — something we send is no longer accepted.\r\n * `missing-response-field` — something we read is gone. **Silent**: the code\r\n * keeps running and produces nothing, which is the dangerous one.\r\n * `unreadable-spec` — the spec URL moved or stopped parsing. Not drift in the\r\n * API, but drift in our ability to see it, and it must not read as \"clean\".\r\n * `stale-review` — nobody has looked at this contract in a long time.\r\n */\r\n kind:\r\n | \"missing-operation\"\r\n | \"missing-request-field\"\r\n | \"missing-response-field\"\r\n | \"unreadable-spec\"\r\n | \"stale-review\";\r\n operation?: string;\r\n detail: string;\r\n};\r\n\r\nexport type DriftReport = {\r\n connector: string;\r\n checkedAt: string;\r\n /** `unchecked` is a real outcome and never counts as clean. */\r\n outcome: \"clean\" | \"drifted\" | \"unchecked\";\r\n findings: DriftFinding[];\r\n /** How the check was done, so a report can be read without the code. */\r\n method: \"openapi\" | \"lexicon\" | \"recorded-shape\" | \"none\";\r\n};\r\n\r\n/* ── OpenAPI, read narrowly and on purpose ───────────────────────────────── */\r\n\r\n/**\r\n * The subset of OpenAPI this needs.\r\n *\r\n * Deliberately not a full parser and deliberately not a dependency. The general\r\n * problem — diff two OpenAPI documents — is large, and we do not have it. We\r\n * have a much smaller one: *for the operations this connector actually calls,\r\n * do the path, the method, and the fields we send and read still exist?*\r\n *\r\n * That projection is ours by definition, it is a few dozen lines, and it does\r\n * not drag a schema-validation library into every consumer's tree. A full diff\r\n * would also report thousands of changes to parts of the API nobody here calls,\r\n * which is the reliable way to make a drift report unread.\r\n */\r\ntype OpenApiDocument = {\r\n paths?: Record<string, Record<string, unknown>>;\r\n components?: { schemas?: Record<string, unknown> };\r\n};\r\n\r\n/** Resolve a local `$ref`. Remote refs are not followed — see `resolveRef`. */\r\nfunction resolveRef(document: OpenApiDocument, node: unknown, depth = 0): unknown {\r\n if (depth > 8 || node === null || typeof node !== \"object\") return node;\r\n\r\n const ref = (node as { $ref?: string }).$ref;\r\n if (typeof ref !== \"string\") return node;\r\n\r\n // Only same-document refs. A remote `$ref` would mean fetching arbitrary URLs\r\n // from a document we do not control, which is a request-forgery surface for a\r\n // check that runs on a schedule with nobody watching.\r\n if (!ref.startsWith(\"#/\")) return node;\r\n\r\n let current: unknown = document;\r\n for (const segment of ref.slice(2).split(\"/\")) {\r\n if (current === null || typeof current !== \"object\") return node;\r\n current = (current as Record<string, unknown>)[segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\")];\r\n }\r\n\r\n return resolveRef(document, current, depth + 1);\r\n}\r\n\r\n/** Property names on a schema, following local refs and one level of composition. */\r\nfunction propertiesOf(document: OpenApiDocument, schema: unknown, depth = 0): Set<string> {\r\n const names = new Set<string>();\r\n const resolved = resolveRef(document, schema, depth) as Record<string, unknown> | null;\r\n\r\n if (!resolved || typeof resolved !== \"object\" || depth > 6) return names;\r\n\r\n const properties = resolveRef(document, resolved.properties, depth + 1);\r\n if (properties && typeof properties === \"object\") {\r\n for (const key of Object.keys(properties as Record<string, unknown>)) names.add(key);\r\n }\r\n\r\n // `data` / `items` wrappers are near-universal and a connector reads through\r\n // them, so an envelope must not read as \"the field is gone\".\r\n for (const key of [\"items\", \"additionalProperties\"]) {\r\n const nested = resolved[key];\r\n if (nested) for (const name of propertiesOf(document, nested, depth + 1)) names.add(name);\r\n }\r\n\r\n for (const key of [\"allOf\", \"oneOf\", \"anyOf\"]) {\r\n const list = resolved[key];\r\n if (Array.isArray(list)) {\r\n for (const entry of list) for (const name of propertiesOf(document, entry, depth + 1)) names.add(name);\r\n }\r\n }\r\n\r\n return names;\r\n}\r\n\r\nfunction requestSchemaOf(document: OpenApiDocument, operation: Record<string, unknown>): unknown {\r\n const body = resolveRef(document, operation.requestBody) as\r\n | { content?: Record<string, { schema?: unknown }> }\r\n | undefined;\r\n\r\n if (!body?.content) return undefined;\r\n\r\n for (const media of Object.values(body.content)) {\r\n if (media?.schema) return media.schema;\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\nfunction responseSchemaOf(document: OpenApiDocument, operation: Record<string, unknown>): unknown {\r\n const responses = resolveRef(document, operation.responses) as Record<string, unknown> | undefined;\r\n if (!responses) return undefined;\r\n\r\n for (const status of Object.keys(responses)) {\r\n if (!/^2\\d\\d$/.test(status) && status !== \"default\") continue;\r\n const response = resolveRef(document, responses[status]) as\r\n | { content?: Record<string, { schema?: unknown }> }\r\n | undefined;\r\n if (!response?.content) continue;\r\n\r\n for (const media of Object.values(response.content)) {\r\n if (media?.schema) return media.schema;\r\n }\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\n/**\r\n * Check a contract against an OpenAPI document already fetched by the caller.\r\n *\r\n * The FETCH is the caller's, not this function's — so the check is a pure\r\n * function of two documents and can be tested against a fixture with no network,\r\n * and so a host chooses its own HTTP client, cache and proxy. A checker that\r\n * fetched would also be a checker that phones out on a schedule, which is\r\n * exactly the thing several hosts of this package refuse to have.\r\n */\r\nexport function checkAgainstOpenApi(contract: ApiContract, document: unknown): DriftReport {\r\n const checkedAt = new Date().toISOString();\r\n const findings: DriftFinding[] = [];\r\n\r\n if (!document || typeof document !== \"object\" || !(document as OpenApiDocument).paths) {\r\n return {\r\n connector: contract.connector,\r\n checkedAt,\r\n outcome: \"unchecked\",\r\n method: \"openapi\",\r\n findings: [\r\n {\r\n kind: \"unreadable-spec\",\r\n detail:\r\n \"the document has no `paths`, so it is not an OpenAPI description we can read. Reported as UNCHECKED \" +\r\n \"rather than clean — a checker that cannot see is not a checker that saw nothing wrong.\",\r\n },\r\n ],\r\n };\r\n }\r\n\r\n const spec = document as OpenApiDocument;\r\n\r\n for (const operation of contract.operations) {\r\n const path = spec.paths?.[operation.path];\r\n\r\n if (!path) {\r\n findings.push({\r\n kind: \"missing-operation\",\r\n operation: operation.operation,\r\n detail: `${operation.method} ${operation.path} is not in the spec at all. The path moved or was removed.`,\r\n });\r\n continue;\r\n }\r\n\r\n const method = path[operation.method.toLowerCase()] as Record<string, unknown> | undefined;\r\n\r\n if (!method) {\r\n findings.push({\r\n kind: \"missing-operation\",\r\n operation: operation.operation,\r\n detail: `${operation.path} exists but no longer accepts ${operation.method}.`,\r\n });\r\n continue;\r\n }\r\n\r\n if (operation.sends?.length) {\r\n const accepted = propertiesOf(spec, requestSchemaOf(spec, method));\r\n // An empty schema means \"we could not read it\", not \"it accepts nothing\".\r\n if (accepted.size > 0) {\r\n for (const field of operation.sends) {\r\n if (!accepted.has(field)) {\r\n findings.push({\r\n kind: \"missing-request-field\",\r\n operation: operation.operation,\r\n detail: `sends \\`${field}\\`, which the spec no longer lists on ${operation.method} ${operation.path}.`,\r\n });\r\n }\r\n }\r\n }\r\n }\r\n\r\n if (operation.reads?.length) {\r\n const returned = propertiesOf(spec, responseSchemaOf(spec, method));\r\n if (returned.size > 0) {\r\n for (const field of operation.reads) {\r\n if (!returned.has(field)) {\r\n findings.push({\r\n kind: \"missing-response-field\",\r\n operation: operation.operation,\r\n detail:\r\n `reads \\`${field}\\`, which the spec no longer returns from ${operation.method} ${operation.path}. ` +\r\n \"This is the silent kind: the code keeps running and produces nothing.\",\r\n });\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n findings.push(...staleReviewFindings(contract, checkedAt));\r\n\r\n return {\r\n connector: contract.connector,\r\n checkedAt,\r\n outcome: findings.some((finding) => finding.kind !== \"stale-review\") ? \"drifted\" : \"clean\",\r\n method: \"openapi\",\r\n findings,\r\n };\r\n}\r\n\r\n/* ── The no-spec fallback: recorded shapes ───────────────────────────────── */\r\n\r\n/**\r\n * The field NAMES a connector saw in a real response, per operation.\r\n *\r\n * Names only. A recorded response body from a real account is a data leak\r\n * wearing a test fixture's clothes, and the thing being checked is the shape.\r\n */\r\nexport type RecordedShape = {\r\n connector: string;\r\n recordedOn: string;\r\n operations: Record<string, string[]>;\r\n};\r\n\r\n/**\r\n * The field names present in a response, flattened to dotted paths.\r\n *\r\n * Arrays collapse to their first element's shape: a response with three items\r\n * has one shape, and recording `items.0.id`, `items.1.id`, `items.2.id` would\r\n * make the fixture depend on how much data the account happened to have.\r\n */\r\nexport function shapeOf(value: unknown, prefix = \"\", depth = 0): string[] {\r\n if (depth > 6 || value === null || typeof value !== \"object\") return prefix ? [prefix] : [];\r\n\r\n if (Array.isArray(value)) {\r\n return value.length === 0 ? [`${prefix}[]`] : shapeOf(value[0], `${prefix}[]`, depth + 1);\r\n }\r\n\r\n const names: string[] = [];\r\n for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {\r\n const path = prefix ? `${prefix}.${key}` : key;\r\n names.push(...shapeOf(nested, path, depth + 1));\r\n }\r\n\r\n return names.sort();\r\n}\r\n\r\n/**\r\n * Compare a live response's shape against what was recorded.\r\n *\r\n * **Only fields the connector READS are findings.** A provider adding fields is\r\n * normal and healthy; reporting it would bury the one line that matters under\r\n * every additive release, and a drift report nobody reads is worse than none\r\n * because it is evidence somebody is watching.\r\n */\r\nexport function checkAgainstRecordedShape(\r\n contract: ApiContract,\r\n operation: string,\r\n liveResponse: unknown,\r\n): DriftReport {\r\n const checkedAt = new Date().toISOString();\r\n const declared = contract.operations.find((entry) => entry.operation === operation);\r\n const present = new Set(shapeOf(liveResponse));\r\n const findings: DriftFinding[] = [];\r\n\r\n for (const field of declared?.reads ?? []) {\r\n // A dotted path matches its own name or any array-bearing variant of it, so\r\n // a connector reading `data.id` still matches `data[].id`.\r\n const found = [...present].some((name) => name === field || name.replace(/\\[\\]/g, \"\") === field);\r\n\r\n if (!found) {\r\n findings.push({\r\n kind: \"missing-response-field\",\r\n operation,\r\n detail:\r\n `reads \\`${field}\\`, which the live response no longer carries. Present instead: ` +\r\n `${[...present].slice(0, 20).join(\", \")}${present.size > 20 ? \", …\" : \"\"}`,\r\n });\r\n }\r\n }\r\n\r\n findings.push(...staleReviewFindings(contract, checkedAt));\r\n\r\n return {\r\n connector: contract.connector,\r\n checkedAt,\r\n outcome: findings.some((finding) => finding.kind !== \"stale-review\") ? \"drifted\" : \"clean\",\r\n method: \"recorded-shape\",\r\n findings,\r\n };\r\n}\r\n\r\n/** How long a contract may go unreviewed before that is itself a finding. */\r\nexport const REVIEW_STALE_DAYS = 180;\r\n\r\nfunction staleReviewFindings(contract: ApiContract, checkedAt: string): DriftFinding[] {\r\n const reviewed = Date.parse(contract.reviewedOn);\r\n if (!Number.isFinite(reviewed)) {\r\n return [{ kind: \"stale-review\", detail: `reviewedOn \"${contract.reviewedOn}\" is not a date.` }];\r\n }\r\n\r\n const days = Math.floor((Date.parse(checkedAt) - reviewed) / 86_400_000);\r\n\r\n return days > REVIEW_STALE_DAYS\r\n ? [\r\n {\r\n kind: \"stale-review\",\r\n detail:\r\n `nobody has read this provider's documentation in ${days} days. Not drift — an absence of looking, ` +\r\n \"which is the state a silent break grows in.\",\r\n },\r\n ]\r\n : [];\r\n}\r\n\r\n/**\r\n * A contract whose provider publishes nothing, checked as far as it can be.\r\n *\r\n * Reported as `unchecked` with the reason, never `clean`. Those are different\r\n * claims and the difference is the entire value of a report.\r\n */\r\nexport function unchecked(contract: ApiContract): DriftReport {\r\n const checkedAt = new Date().toISOString();\r\n\r\n return {\r\n connector: contract.connector,\r\n checkedAt,\r\n outcome: \"unchecked\",\r\n method: \"none\",\r\n findings: [\r\n {\r\n kind: \"unreadable-spec\",\r\n detail:\r\n contract.spec.kind === \"none\"\r\n ? contract.spec.note\r\n : \"no spec document was supplied to the checker, so nothing was compared.\",\r\n },\r\n ...staleReviewFindings(contract, checkedAt),\r\n ],\r\n };\r\n}\r\n","import { ConnectorConfigError, type ConnectorErrorContext } from \"./errors\";\r\n\r\n/**\r\n * A run, a position inside it, and how many times that position has been tried.\r\n *\r\n * Declared STRUCTURALLY rather than imported, and that is the whole point: this\r\n * package must be usable by a host that has never heard of a workflow engine.\r\n * `fancy-flow`'s `RunIdentity` class satisfies this shape exactly, so a flow\r\n * node passes `ctx.run` straight in with no adapter and no dependency in either\r\n * direction — and a host with its own notion of \"the same logical attempt\"\r\n * implements five members and gets the same guarantees.\r\n *\r\n * The identity is more than `(run, step)` for a reason: a step legitimately runs\r\n * many times in one run — once per loop iteration, once per nested invocation —\r\n * so the key carries the PATH that led to it. `attempt` is carried and\r\n * deliberately NOT part of the key, because a retry must produce the same key.\r\n */\r\nexport type RunIdentity = {\r\n /** Stable for the whole run: same across retries, resumes, workers and hosts. */\r\n readonly runKey: string;\r\n /** 1-based attempt of THIS logical step. Never part of the key. */\r\n readonly attempt: number;\r\n /** ISO-8601 instant of attempt 1 of this step. */\r\n readonly firstAttemptAt: string;\r\n /** The identity of one execution of one step, stable across its retries. */\r\n stepKey(stepId: string, occurrence?: number | null): string;\r\n /** May this attempt reuse the key and still be deduplicated? */\r\n isReplaySafe(windowSeconds: number | null | undefined, now?: Date | string): boolean;\r\n};\r\n\r\n/**\r\n * The idempotency key a writing connector sends, and when it must refuse to.\r\n *\r\n * ## What the engine now provides\r\n *\r\n * `fancy-flow` 0.46.0 carries a **run identity** on the execution context, so a\r\n * node can derive a key that is the same on every retry of one logical step and\r\n * different for every other execution of the same node.\r\n * `ctx.run.stepKey(ctx.node.id)` is that key. The equivalent landed in\r\n * `fancy-flow-php` 0.19.0 and the Python runtime, all three pinned by\r\n * `shared/flow-run-identity` in `@particle-academy/fancy-conformance`.\r\n *\r\n * Before that, neither engine could produce one, and both obvious substitutes\r\n * were worse than sending nothing:\r\n *\r\n * - **the node id alone** is stable across retries and also across RUNS, so two\r\n * legitimate payments share a key and the provider silently collapses the\r\n * second into the first — a payment that never happened, reported as success;\r\n * - **a fresh random value** is unique per run and also per ATTEMPT, so a retry\r\n * creates a second charge.\r\n *\r\n * So this used to return `null` and every writing node emitted a warning and\r\n * sent no key, staying pinned to one attempt. That is no longer the case where\r\n * a host supplies an identity — which durable runs do automatically.\r\n *\r\n * ## The part that is still a judgement call: the provider's window\r\n *\r\n * Providers forget idempotency keys. Stripe's window is **24 hours**. Past it,\r\n * resending the key creates a second charge and sending a fresh one creates a\r\n * second charge — there is no safe third option — so `idempotencyKeyFor`\r\n * **throws** rather than choosing. A loud stuck run that a person reconciles\r\n * beats a silent double write nobody ever sees.\r\n *\r\n * A first attempt is never refused, however long the run was parked: nothing\r\n * was sent on an earlier attempt, so there is nothing for the provider to have\r\n * forgotten. That is what lets an approval sit for a week and then charge.\r\n */\r\n\r\n/** Stripe's window, and the default for a provider that has not said otherwise. */\r\nexport const DEFAULT_IDEMPOTENCY_WINDOW_SECONDS = 24 * 60 * 60;\r\n\r\n/**\r\n * Longest key the strictest provider in the catalogue accepts.\r\n *\r\n * Stripe caps `Idempotency-Key` at 255 characters. A deep subflow path could\r\n * exceed that, and a provider rejecting the header is a 400 that looks like a\r\n * bug in the request body.\r\n */\r\nexport const MAX_IDEMPOTENCY_KEY_LENGTH = 255;\r\n\r\n/** Raised when a retry can no longer be made safe. Never retryable. */\r\nexport class ConnectorIdempotencyExpiredError extends ConnectorConfigError {}\r\n\r\n/** The run identity a host published, or `null`. Checked in order. */\r\nexport function runIdentityFrom(ctx: unknown): RunIdentity | null {\r\n const bag = ctx as { run?: RunIdentity } | null;\r\n\r\n return bag?.run ?? null;\r\n}\r\n\r\n/**\r\n * Where a host with no engine support may still publish a run key.\r\n *\r\n * Kept because a consumer on an older `fancy-flow` has no `ctx.run`, and\r\n * seeding `__runKey` into the run's initial inputs is the documented fallback.\r\n * It gives a per-run key with no attempt information, so the window check below\r\n * treats it as a first attempt — correct for a host that does not retry, and\r\n * the reason the engine-supplied identity is strictly better.\r\n */\r\nexport function runKeyFrom(ctx: unknown): string | null {\r\n const identity = runIdentityFrom(ctx);\r\n if (identity) return identity.runKey;\r\n\r\n const bag = ctx as Record<string, any> | null;\r\n const seeded = bag?.inputs?.__runKey;\r\n\r\n return typeof seeded === \"string\" && seeded.trim() !== \"\" ? seeded.trim() : null;\r\n}\r\n\r\nexport type IdempotencyOptions = {\r\n /**\r\n * How long the provider remembers a key. `null` means it never forgets; `0`\r\n * means it does not dedupe at all, so no retry may reuse one.\r\n */\r\n windowSeconds?: number | null;\r\n /** Distinguishes repeated executions of one node at the same level. */\r\n occurrence?: number | null;\r\n /** Injectable for tests. Never a network call. */\r\n now?: Date;\r\n /** For the error message when a key cannot be made safe. */\r\n context?: Partial<ConnectorErrorContext>;\r\n};\r\n\r\n/**\r\n * The idempotency key for this execution of this node, or `null`.\r\n *\r\n * `null` means the host published no run identity at all — send no header\r\n * rather than inventing one, and keep the node `unsafe-to-replay`.\r\n *\r\n * **Throws** `ConnectorIdempotencyExpiredError` when this is a RETRY and the\r\n * provider's window has elapsed. That is not a defensive check; it is the only\r\n * correct answer, because both alternatives write twice.\r\n */\r\nexport function idempotencyKeyFor(\r\n ctx: unknown,\r\n nodeId: string,\r\n options: IdempotencyOptions = {},\r\n): string | null {\r\n const identity = runIdentityFrom(ctx);\r\n const windowSeconds =\r\n options.windowSeconds === undefined ? DEFAULT_IDEMPOTENCY_WINDOW_SECONDS : options.windowSeconds;\r\n\r\n if (!identity) {\r\n // The legacy seeded-key path. No attempt information exists, so there is\r\n // nothing to window-check; a host on this path is one that does not retry.\r\n const runKey = runKeyFrom(ctx);\r\n return runKey === null ? null : fit(`${runKey}:${nodeId}`);\r\n }\r\n\r\n if (!identity.isReplaySafe(windowSeconds, options.now ?? new Date())) {\r\n throw new ConnectorIdempotencyExpiredError(\r\n `attempt ${identity.attempt} of this step began at ${identity.firstAttemptAt}, which is outside ` +\r\n `the provider's ${windowSeconds}s idempotency window. The provider has forgotten the original ` +\r\n `key, so resending it and sending a new one would BOTH write twice. Refusing, so a person can ` +\r\n `reconcile — check whether the first attempt landed before re-running this workflow.`,\r\n {\r\n service: options.context?.service ?? \"\",\r\n operation: options.context?.operation ?? \"\",\r\n ...options.context,\r\n },\r\n );\r\n }\r\n\r\n return fit(identity.stepKey(nodeId, options.occurrence));\r\n}\r\n\r\n/**\r\n * The warning a node emits when it is about to write without a key.\r\n *\r\n * Said out loud, at the moment it matters, because the failure it precedes is\r\n * invisible: the run succeeds, the retry succeeds, and there are two charges.\r\n */\r\nexport const NO_IDEMPOTENCY_KEY_WARNING =\r\n \"no run identity available, so this write is being sent WITHOUT an idempotency key. \" +\r\n \"A retry would repeat the effect rather than recover from it — the node is marked \" +\r\n \"unsafe-to-replay, so a durable runner will give it one attempt. Durable runs supply an \" +\r\n \"identity automatically; for a synchronous run, pass `run` in RunOptions.\";\r\n\r\n/**\r\n * Shorten an over-long key deterministically.\r\n *\r\n * The hash has to be stable across attempts AND across runtimes, so it is a\r\n * plain FNV-1a over the key rather than anything host-provided. The prefix is\r\n * kept so a key remains greppable against a run.\r\n */\r\nfunction fit(key: string): string {\r\n if (key.length <= MAX_IDEMPOTENCY_KEY_LENGTH) return key;\r\n\r\n const digest = fnv1a(key);\r\n const head = key.slice(0, MAX_IDEMPOTENCY_KEY_LENGTH - digest.length - 1);\r\n\r\n return `${head}~${digest}`;\r\n}\r\n\r\nfunction fnv1a(value: string): string {\r\n let hash = 0x811c9dc5;\r\n for (let i = 0; i < value.length; i++) {\r\n hash ^= value.charCodeAt(i);\r\n // 32-bit FNV prime, written as shifts so it stays exact in a double.\r\n hash = (hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24))) >>> 0;\r\n }\r\n\r\n return hash.toString(16).padStart(8, \"0\");\r\n}\r\n","/**\r\n * Making a declared metric shape checkable against the code that produces it.\r\n *\r\n * ## The bug this exists to catch, which shipped for weeks\r\n *\r\n * Two connectors in the reference implementation declared\r\n * `capabilities.metrics: true` while their metric fetch returned `[]`. A pull\r\n * therefore did not skip them — it asked, got nothing, and reported nothing,\r\n * which on a dashboard is indistinguishable from *\"we asked and nobody\r\n * engaged\"*. A capability flag had quietly turned an unimplemented feature into\r\n * a reported zero, on the one surface whose entire job is not making claims it\r\n * cannot stand behind.\r\n *\r\n * A declared shape and the code that produces the numbers are the classic pair\r\n * that agrees on the day it is written and drifts silently after. So the mapping\r\n * is extracted as a **pure function per connector**, and these helpers feed it a\r\n * synthetic response and compare the keys it returns against the keys declared.\r\n * **No credentials, no network.**\r\n *\r\n * ## And the rule underneath all of it\r\n *\r\n * **Absent stays absent.** A count the provider did not send is omitted, never\r\n * reported as zero. A zero says \"nothing happened\"; an absence says \"we don't\r\n * know\". A measurement surface that confuses those is worthless, and the\r\n * confusion is one `?? 0` away.\r\n */\r\n\r\nimport type { Connector, MetricDescriptor } from \"./seam\";\r\n\r\nexport type ShapeMismatch = {\r\n connector: string;\r\n /** Keys the mapping produced that nothing declared. */\r\n undeclared: string[];\r\n /** Keys declared that the mapping never produces. */\r\n unproduced: string[];\r\n};\r\n\r\n/**\r\n * Compare a declared shape against what a mapping actually returns.\r\n *\r\n * `produced` is the output of the connector's pure response→metrics function\r\n * given a synthetic response with EVERY field populated. Anything the mapping\r\n * can emit therefore appears, and anything it cannot is caught as a declaration\r\n * nobody can honour.\r\n */\r\nexport function compareShape(\r\n connectorId: string,\r\n declared: MetricDescriptor[] | undefined,\r\n produced: Record<string, number>,\r\n): ShapeMismatch | null {\r\n const declaredKeys = new Set((declared ?? []).map((metric) => metric.key));\r\n const producedKeys = new Set(Object.keys(produced));\r\n\r\n const undeclared = [...producedKeys].filter((key) => !declaredKeys.has(key)).sort();\r\n const unproduced = [...declaredKeys].filter((key) => !producedKeys.has(key)).sort();\r\n\r\n if (undeclared.length === 0 && unproduced.length === 0) return null;\r\n\r\n return { connector: connectorId, undeclared, unproduced };\r\n}\r\n\r\n/**\r\n * Every way a capability flag can outrun the code, as findings.\r\n *\r\n * Returns strings rather than throwing so a host can report all of them at once.\r\n * A check that stops at the first problem trains people to fix one thing and\r\n * re-run, which is how a list of six becomes six rounds.\r\n */\r\nexport function capabilityProblems(connector: Connector<never>): string[] {\r\n const problems: string[] = [];\r\n\r\n if (connector.capabilities.metrics) {\r\n if (!connector.metricShape || connector.metricShape.length === 0) {\r\n problems.push(\r\n `${connector.id} claims capabilities.metrics but declares no metricShape. A pull will therefore ask it, ` +\r\n \"get nothing, and report nothing — which reads as \\\"nobody engaged\\\" rather than \\\"not implemented\\\".\",\r\n );\r\n }\r\n if (!connector.fetchMetrics) {\r\n problems.push(`${connector.id} claims capabilities.metrics but has no fetchMetrics.`);\r\n }\r\n }\r\n\r\n if (!connector.capabilities.metrics && connector.metricShape) {\r\n problems.push(\r\n `${connector.id} declares a metricShape but capabilities.metrics is false. One of the two is wrong, and ` +\r\n \"the shape is the more believable half.\",\r\n );\r\n }\r\n\r\n if (connector.capabilities.feedback && !connector.fetchFeedback) {\r\n problems.push(`${connector.id} claims capabilities.feedback but has no fetchFeedback.`);\r\n }\r\n\r\n if (!connector.capabilities.feedback && connector.fetchFeedback) {\r\n problems.push(\r\n `${connector.id} implements fetchFeedback but does not claim the capability, so nothing will ever call it.`,\r\n );\r\n }\r\n\r\n for (const metric of connector.metricShape ?? []) {\r\n if (metric.means.trim().length < 10) {\r\n problems.push(`${connector.id}.${metric.key} does not say what it means.`);\r\n }\r\n }\r\n\r\n if (connector.delivery.idempotent && connector.delivery.why.trim().length < 10) {\r\n problems.push(\r\n `${connector.id} declares idempotent: true with no reason. That is the one claim whose failure is a public ` +\r\n \"duplicate, so it has to name the mechanism rather than restate the flag.\",\r\n );\r\n }\r\n\r\n if (connector.delivery.minIntervalMs > 0 && !connector.delivery.citation && connector.delivery.rateSource === \"documented\") {\r\n problems.push(\r\n `${connector.id} calls its rate limit \"documented\" but cites nothing. A number nobody can source gets quoted ` +\r\n \"as a platform fact; label it self-imposed or cite it.\",\r\n );\r\n }\r\n\r\n return problems;\r\n}\r\n\r\n/**\r\n * Drop keys whose value the provider did not actually send.\r\n *\r\n * The one function a connector's mapping should build on, so *absent stays\r\n * absent* is a property of the code rather than a habit. `0` survives; `null`,\r\n * `undefined` and non-numbers do not.\r\n */\r\nexport function reported(values: Record<string, unknown>): Record<string, number> {\r\n const out: Record<string, number> = {};\r\n\r\n for (const [key, value] of Object.entries(values)) {\r\n if (typeof value === \"number\" && Number.isFinite(value)) out[key] = value;\r\n }\r\n\r\n return out;\r\n}\r\n","/**\r\n * Dry-verify — proving the transport with a credential that cannot work.\r\n *\r\n * ## The discipline, and why it beats a fake server\r\n *\r\n * A connector written from documentation can be wrong in three ways that look\r\n * identical until a real credential arrives:\r\n *\r\n * 1. the URL is wrong,\r\n * 2. the request shape is wrong,\r\n * 3. the error handling never runs.\r\n *\r\n * **All three are invisible to a test with a fake server**, because a fake\r\n * server agrees with whatever the code does. That is the single most important\r\n * thing this module encodes: a green unit suite against a mock proves the code\r\n * is consistent with itself.\r\n *\r\n * So a probe calls the REAL API with a deliberately invalid credential and\r\n * requires an **auth-shaped refusal**. That proves the host resolved, the path\r\n * exists, the method was accepted, and a failure was recognised as a failure.\r\n *\r\n * ## What counts as a pass, precisely\r\n *\r\n * An auth-shaped refusal is `401`, `403`, or a `404` on a provider that answers\r\n * 404 for an unknown credential-in-the-path (a Discord webhook does exactly\r\n * this). The provider declares which, because guessing here would let a genuine\r\n * \"this endpoint moved\" 404 read as success — the one outcome a probe exists to\r\n * catch.\r\n *\r\n * ## Offline is SKIPPED, never failed\r\n *\r\n * A check that goes red on a train gets ignored, and then it is worth nothing\r\n * when it goes red for real. Unreachable is reported as skipped with the reason,\r\n * and the assertions that need no network still run.\r\n *\r\n * ## And nothing is ever sent\r\n *\r\n * Every probe is a READ. A probe that wrote something would be a send, and sends\r\n * are the host's to gate.\r\n */\r\n\r\nimport { classifyError, classifyStatus, type FailureKind } from \"./delivery\";\r\n\r\nexport type ProbeOutcome = \"pass\" | \"fail\" | \"skip\";\r\n\r\nexport type ProbeResult = {\r\n connector: string;\r\n outcome: ProbeOutcome;\r\n /** What happened, in the provider's own words where possible. */\r\n detail: string;\r\n status?: number;\r\n kind?: FailureKind;\r\n};\r\n\r\nexport type ProbeSpec = {\r\n /** Which connector this probes. */\r\n connector: string;\r\n /**\r\n * The read-only request, fully formed, carrying a credential that cannot be\r\n * valid. Built by the connector so the probe exercises the connector's own URL\r\n * and auth placement rather than a second copy of them.\r\n */\r\n request: () => Promise<{ status: number; body: string }>;\r\n /**\r\n * Statuses that count as an auth-shaped refusal for THIS provider.\r\n *\r\n * Declared rather than assumed. Discord answers `404` for an unknown webhook\r\n * id, and on a provider that does not, a 404 means the endpoint moved — the\r\n * exact drift a probe is for.\r\n */\r\n authStatuses: number[];\r\n /** Why those statuses, for whoever reads a failure at 3am. */\r\n why: string;\r\n};\r\n\r\n/**\r\n * Run one probe.\r\n *\r\n * Never throws. A probe that threw would take the suite down with it, and the\r\n * suite's whole value is that it runs everywhere including on a laptop with no\r\n * network.\r\n */\r\nexport async function probe(spec: ProbeSpec): Promise<ProbeResult> {\r\n let response: { status: number; body: string };\r\n\r\n try {\r\n response = await spec.request();\r\n } catch (error) {\r\n const classified = classifyError(error);\r\n\r\n // Unreachable is a fact about the machine, not about the connector.\r\n if (classified.kind === \"unreachable\") {\r\n return { connector: spec.connector, outcome: \"skip\", detail: `offline — ${classified.detail}` };\r\n }\r\n\r\n return {\r\n connector: spec.connector,\r\n outcome: \"fail\",\r\n detail: `the request failed before any status arrived — ${classified.detail}`,\r\n kind: classified.kind,\r\n };\r\n }\r\n\r\n const { status, body } = response;\r\n\r\n if (spec.authStatuses.includes(status)) {\r\n return {\r\n connector: spec.connector,\r\n outcome: \"pass\",\r\n status,\r\n detail: `${status} — the provider refused an invalid credential, which proves host, path, method and error handling. ${spec.why}`,\r\n };\r\n }\r\n\r\n if (status < 400) {\r\n // A success with a credential that cannot be valid means the request is not\r\n // doing what it appears to. Worse than a failure, because it is green.\r\n return {\r\n connector: spec.connector,\r\n outcome: \"fail\",\r\n status,\r\n detail:\r\n `${status} — the provider ACCEPTED a deliberately invalid credential. Either the credential is reaching ` +\r\n \"nothing (wrong header, wrong field) or this endpoint does not authenticate. Neither is safe to ship.\",\r\n };\r\n }\r\n\r\n const classified = classifyStatus(status, body);\r\n\r\n return {\r\n connector: spec.connector,\r\n outcome: \"fail\",\r\n status,\r\n kind: classified.kind,\r\n detail:\r\n `${status} — expected one of ${spec.authStatuses.join(\", \")}. A ${status} here usually means the endpoint ` +\r\n `moved or the path is wrong, which no amount of local mocking would ever have said. ${body.slice(0, 200)}`,\r\n };\r\n}\r\n\r\nexport type ProbeReport = {\r\n results: ProbeResult[];\r\n passed: number;\r\n failed: number;\r\n skipped: number;\r\n /** True when nothing failed. Skips do not fail a report. */\r\n ok: boolean;\r\n};\r\n\r\n/** Run a set of probes and summarise. Sequential: probes hit real providers. */\r\nexport async function runProbes(specs: ProbeSpec[]): Promise<ProbeReport> {\r\n const results: ProbeResult[] = [];\r\n\r\n for (const spec of specs) results.push(await probe(spec));\r\n\r\n const passed = results.filter((result) => result.outcome === \"pass\").length;\r\n const failed = results.filter((result) => result.outcome === \"fail\").length;\r\n const skipped = results.filter((result) => result.outcome === \"skip\").length;\r\n\r\n return { results, passed, failed, skipped, ok: failed === 0 };\r\n}\r\n","/**\r\n * Measuring and slicing text the way providers actually measure and slice it.\r\n *\r\n * ## Why a package owns this at all\r\n *\r\n * Two of the nastiest bugs in a connector catalogue are one-liners, and both\r\n * come from reaching for the obvious JavaScript idiom:\r\n *\r\n * 1. **`text.length` is not a count of anything a provider limits.** It counts\r\n * UTF-16 code units. `\"👍\".length` is 2 and `\"👨‍👩‍👧\".length` is 8, so it\r\n * overcounts exactly the posts most likely to sit near a limit — and it fails\r\n * in the worse direction, because a post that passes our check and is then\r\n * refused by the server reads as the network being flaky rather than as our\r\n * bug.\r\n * 2. **`indexOf` returns a character index, and rich-text formats want BYTES.**\r\n * AT Protocol facets are UTF-8 byte ranges. An implementation using character\r\n * indices is correct for ASCII and silently corrupts every post containing an\r\n * emoji or an accent — and it corrupts the *link*, so the post looks fine and\r\n * the link goes somewhere wrong.\r\n *\r\n * Neither is discoverable by testing with ASCII, which is what everyone tests\r\n * with. So the package makes the wrong thing unreachable: a `ByteRange` is a\r\n * type, not a convention, and it can only be produced by a function that\r\n * encoded the string.\r\n *\r\n * ## The unit is declared, never assumed\r\n *\r\n * Providers count different things — Bluesky counts graphemes, Discord and\r\n * Telegram count characters, some count bytes. `TextUnit` makes the choice\r\n * explicit at the point a limit is declared, so nobody has to remember which\r\n * network is which.\r\n */\r\n\r\n/** What a provider actually counts. Declared per connector, never guessed. */\r\nexport type TextUnit = \"graphemes\" | \"characters\" | \"utf8-bytes\";\r\n\r\n/**\r\n * A half-open range of UTF-8 BYTES.\r\n *\r\n * Named for the unit because the whole failure mode is somebody passing\r\n * character offsets into a field that wants bytes. The two are equal for ASCII,\r\n * which is why this survives review and fails in production.\r\n */\r\nexport type ByteRange = {\r\n byteStart: number;\r\n byteEnd: number;\r\n};\r\n\r\nconst encoder = new TextEncoder();\r\n\r\n/**\r\n * Count graphemes — what a person means by \"characters\".\r\n *\r\n * `Intl.Segmenter` is the correct tool and has been in Node since 16. The\r\n * fallback is the spread operator, which counts code points rather than UTF-16\r\n * units and so is wrong by less. Both are better than `.length`.\r\n */\r\nexport function graphemes(text: string): number {\r\n const Segmenter = (\r\n Intl as unknown as {\r\n Segmenter?: new (locale?: string, options?: object) => { segment(input: string): Iterable<unknown> };\r\n }\r\n ).Segmenter;\r\n\r\n if (!Segmenter) return [...text].length;\r\n\r\n let count = 0;\r\n for (const _ of new Segmenter(undefined, { granularity: \"grapheme\" }).segment(text)) count++;\r\n\r\n return count;\r\n}\r\n\r\n/** Length of `text` in UTF-8 bytes. */\r\nexport function utf8Length(text: string): number {\r\n return encoder.encode(text).length;\r\n}\r\n\r\n/**\r\n * Count `text` in whichever unit a provider enforces.\r\n *\r\n * The one function every limit check goes through, so a connector never has to\r\n * decide — and so `text.length` never appears in connector source at all.\r\n */\r\nexport function measure(text: string, unit: TextUnit): number {\r\n if (unit === \"graphemes\") return graphemes(text);\r\n if (unit === \"utf8-bytes\") return utf8Length(text);\r\n\r\n return text.length;\r\n}\r\n\r\n/**\r\n * The UTF-8 byte range of a substring that starts at a CHARACTER index.\r\n *\r\n * The bridge between the index a regex gives you and the offsets a provider\r\n * wants. Takes the character index because that is what `matchAll` produces, and\r\n * returns bytes because that is what goes on the wire — doing the conversion in\r\n * one place is the entire point.\r\n */\r\nexport function byteRangeOf(text: string, charIndex: number, substring: string): ByteRange {\r\n const byteStart = utf8Length(text.slice(0, charIndex));\r\n\r\n return { byteStart, byteEnd: byteStart + utf8Length(substring) };\r\n}\r\n\r\n/**\r\n * Extract the substring a byte range names.\r\n *\r\n * Exists so a test can prove a range points where it claims to, which is the\r\n * only way to catch an off-by-one that ASCII hides. A range that lands\r\n * mid-character yields the replacement character rather than throwing, because\r\n * the useful failure here is a visibly wrong string in an assertion, not an\r\n * exception three frames away.\r\n */\r\nexport function sliceByteRange(text: string, range: ByteRange): string {\r\n const bytes = encoder.encode(text);\r\n\r\n return new TextDecoder().decode(bytes.slice(range.byteStart, range.byteEnd));\r\n}\r\n\r\n/**\r\n * Bare URLs in `text`, with their UTF-8 byte ranges.\r\n *\r\n * Trailing punctuation is excluded: a link at the end of a sentence would\r\n * otherwise swallow the full stop and 404. Closing brackets go too, for the same\r\n * reason and because a URL in parentheses is common in prose.\r\n *\r\n * This is the generic half of a \"link facet\" — a connector maps the ranges into\r\n * whatever its provider's rich-text shape is. The package owns the offsets\r\n * because the offsets are what people get wrong; it does not own the provider's\r\n * schema, because that is the provider's.\r\n */\r\nexport function linkRanges(text: string): Array<ByteRange & { url: string }> {\r\n const found: Array<ByteRange & { url: string }> = [];\r\n\r\n for (const match of text.matchAll(/https?:\\/\\/[^\\s]+/g)) {\r\n if (match.index === undefined) continue;\r\n const url = match[0].replace(/[.,;:!?)\\]}'\"]+$/, \"\");\r\n found.push({ ...byteRangeOf(text, match.index, url), url });\r\n }\r\n\r\n return found;\r\n}\r\n","/**\r\n * Rendering — what a provider would ACTUALLY receive.\r\n *\r\n * ## Why a connector package owns this, and why it is dangerous\r\n *\r\n * One piece of content often has to reach several providers whose limits are\r\n * incompatible. Without adaptation the same text is wasteful on one and refused\r\n * by another. With naive adaptation, the bytes that reach the public are not the\r\n * bytes anyone approved — and every guarantee a host makes about approval\r\n * becomes a lie the moment adaptation exists.\r\n *\r\n * So rendering happens **before** whatever gate the host applies: the person\r\n * approving sees exactly what each provider will receive and approves *that*.\r\n * Nothing is invented after the decision.\r\n *\r\n * ## Which forces three properties\r\n *\r\n * 1. **Pure.** Rules in, payload out. No clock, no randomness, no network. If\r\n * the preview and the send could disagree, the approval means nothing.\r\n * Anything variable — an instance's configured limit — is resolved by the\r\n * CALLER and passed in as a rule, so it becomes part of what was approved.\r\n * 2. **Versioned.** Any change to how text is split, counted or faceted bumps\r\n * `RENDERER_VERSION`, and the version is **inside** the payload hash rather\r\n * than beside it, so it cannot be checked separately and forgotten.\r\n * 3. **Honest about loss.** Anything that could not be fitted is reported as a\r\n * problem, never quietly applied. A truncated URL is worse than a refused\r\n * post, because it looks deliberate and goes somewhere wrong.\r\n *\r\n * ## And one rule about where length lives\r\n *\r\n * **No length rule outside this module.** A validator and a renderer that both\r\n * judge length will disagree, and then the validator refuses content the\r\n * renderer had already solved. `validate()` on a connector checks things the\r\n * renderer cannot — media, alt text, a required field — and leaves length here.\r\n */\r\n\r\nimport { linkRanges, measure, type ByteRange, type TextUnit } from \"./text\";\r\n\r\n/**\r\n * Bump on ANY change to how text is split, counted, or faceted.\r\n *\r\n * A payload approved under `1` refuses to send once this reads `2`, because the\r\n * payload the approver saw is no longer the payload that would go out. That is\r\n * the correct outcome and the reason the field exists.\r\n */\r\nexport const RENDERER_VERSION = \"render@1\";\r\n\r\nexport type Segment = {\r\n /** The text of this message, exactly as it would be sent. */\r\n text: string;\r\n /** How many of the provider's unit this segment uses. */\r\n count: number;\r\n /** Link ranges, in UTF-8 bytes, **relative to this segment**. */\r\n links?: Array<ByteRange & { url: string }>;\r\n};\r\n\r\nexport type RenderedPayload = {\r\n /** One entry per message. More than one means a thread. */\r\n segments: Segment[];\r\n /** What the provider counts, so a preview can label the number. */\r\n unit: TextUnit;\r\n /**\r\n * What the provider allows, or **null where it imposes no limit**.\r\n *\r\n * Null rather than a very large number: `Number.MAX_SAFE_INTEGER` is\r\n * arithmetically fine and reads, on a person's screen, as\r\n * `157 / 9007199254740991` — a number pretending to be a fact. \"No limit\" and\r\n * \"a limit so large it cannot be hit\" are different statements.\r\n */\r\n limit: number | null;\r\n /** Why this could not be rendered, when it could not. Never silently repaired. */\r\n problems: string[];\r\n rendererVersion: string;\r\n};\r\n\r\n/**\r\n * The rules a provider imposes on a message, as DATA.\r\n *\r\n * Declarative on purpose. Across the providers this was extracted from, the\r\n * differences that mattered were all values — the limit, the unit, whether a\r\n * thread mechanism exists, whether links carry byte offsets. A provider whose\r\n * rendering genuinely needs code supplies its own `render` on the connector and\r\n * ignores this; the point of the rule set is that most do not have to.\r\n */\r\nexport type RenderRules = {\r\n /** The provider's limit per message, or null where it has none. */\r\n limit: number | null;\r\n unit: TextUnit;\r\n /**\r\n * Whether this provider can carry a sequence of connected messages.\r\n *\r\n * False means over-length content is REFUSED rather than split — splitting\r\n * would invent a structure the provider does not have, and a numbered sequence\r\n * of unconnected messages is worse than a refusal because it looks deliberate.\r\n */\r\n thread: boolean;\r\n /** Include UTF-8 link ranges per segment, for providers with rich-text spans. */\r\n links?: boolean;\r\n /** Human name, used in refusal messages. */\r\n label: string;\r\n /**\r\n * Suffix added to each part of a thread, given the 1-based index and total.\r\n *\r\n * Defaults to ` (i/n)`. Its cost is measured and subtracted from the limit\r\n * BEFORE splitting, because a suffix discovered afterwards is a bug that only\r\n * appears on long threads.\r\n */\r\n numbering?: (index: number, total: number) => string;\r\n};\r\n\r\nconst defaultNumbering = (index: number, total: number): string => ` (${index}/${total})`;\r\n\r\n/**\r\n * Where a sentence ends: after `.`, `!` or `?` **only when whitespace follows**,\r\n * or after a newline.\r\n *\r\n * The whitespace condition is the whole point. A regex that treats every `.` as\r\n * a terminator splits inside `https://example.test/x`, and the URL can then land\r\n * across two messages — a link that goes nowhere, from copy that looked fine in\r\n * review. Abbreviations survive for the same reason.\r\n */\r\nconst SENTENCE_BOUNDARY = /(?<=[.!?])(?=\\s)|(?<=\\n)/;\r\n\r\n/**\r\n * Split text into parts that each fit the limit.\r\n *\r\n * **Sentence boundaries first, then words, and never mid-grapheme.** A thread\r\n * that splits mid-argument is worse than one that splits a little unevenly, so\r\n * this prefers a natural break even when it wastes room.\r\n *\r\n * A single token that cannot fit at all is reported as a problem rather than\r\n * hard-cut — see the module note about truncated URLs.\r\n */\r\nexport function splitToFit(\r\n text: string,\r\n limit: number,\r\n unit: TextUnit,\r\n): { parts: string[]; problems: string[] } {\r\n const count = (value: string): number => measure(value, unit);\r\n const trimmed = text.trim();\r\n\r\n if (count(trimmed) <= limit) return { parts: trimmed === \"\" ? [] : [trimmed], problems: [] };\r\n\r\n const problems: string[] = [];\r\n const parts: string[] = [];\r\n\r\n // Sentence-ish units, kept WITH their punctuation: a fragment that loses its\r\n // full stop reads as truncated even when it is complete.\r\n //\r\n // **A terminator only ends a sentence when whitespace or the end follows it.**\r\n // The obvious sentence regex splits inside `https://example.test/x`, and a\r\n // URL can then land across two messages — a link that goes nowhere, from copy\r\n // that looked fine in review. Abbreviations survive for the same reason.\r\n // Newlines still split: a deliberate break is a better seam than any sentence.\r\n const sentences = trimmed.split(SENTENCE_BOUNDARY).filter((part) => part !== \"\");\r\n\r\n let current = \"\";\r\n const flush = (): void => {\r\n if (current.trim() !== \"\") parts.push(current.trim());\r\n current = \"\";\r\n };\r\n\r\n for (const sentence of sentences) {\r\n const candidate = current + sentence;\r\n\r\n if (count(candidate.trim()) <= limit) {\r\n current = candidate;\r\n continue;\r\n }\r\n\r\n flush();\r\n\r\n if (count(sentence.trim()) <= limit) {\r\n current = sentence;\r\n continue;\r\n }\r\n\r\n let buffer = \"\";\r\n for (const word of sentence.split(/(\\s+)/)) {\r\n if (count((buffer + word).trim()) <= limit) {\r\n buffer += word;\r\n continue;\r\n }\r\n\r\n if (buffer.trim() !== \"\") parts.push(buffer.trim());\r\n buffer = \"\";\r\n\r\n if (count(word.trim()) > limit) {\r\n problems.push(\r\n `\"${word.trim().slice(0, 40)}…\" is ${count(word.trim())} ${unit} on its own, longer than one whole ` +\r\n `message (${limit}). It cannot be split without breaking it, so this needs rewriting rather than adapting.`,\r\n );\r\n continue;\r\n }\r\n\r\n buffer = word;\r\n }\r\n\r\n current = buffer;\r\n }\r\n\r\n flush();\r\n\r\n return { parts: parts.filter((part) => part !== \"\"), problems };\r\n}\r\n\r\n/**\r\n * Render text against a provider's rule set.\r\n *\r\n * The one entry point. A connector that needs nothing bespoke declares rules and\r\n * calls this; the shared behaviour — numbering cost, per-segment link offsets,\r\n * refusal where there is no thread mechanism — is then identical everywhere,\r\n * which is what makes a catalogue feel like one thing.\r\n */\r\nexport function render(text: string, rules: RenderRules): RenderedPayload {\r\n const { limit, unit, label } = rules;\r\n const count = (value: string): number => measure(value, unit);\r\n const trimmed = text.trim();\r\n\r\n if (limit === null || count(trimmed) <= limit) {\r\n return {\r\n segments: [{ text: trimmed, count: count(trimmed), ...(rules.links ? { links: linkRanges(trimmed) } : {}) }],\r\n unit,\r\n limit,\r\n problems: [],\r\n rendererVersion: RENDERER_VERSION,\r\n };\r\n }\r\n\r\n if (!rules.thread) {\r\n return {\r\n segments: [{ text: trimmed, count: count(trimmed), ...(rules.links ? { links: linkRanges(trimmed) } : {}) }],\r\n unit,\r\n limit,\r\n problems: [\r\n `${count(trimmed)} ${unit}; ${label}'s limit is ${limit}, and it has no thread mechanism to split into. ` +\r\n \"This needs shortening rather than adapting.\",\r\n ],\r\n rendererVersion: RENDERER_VERSION,\r\n };\r\n }\r\n\r\n const numbering = rules.numbering ?? defaultNumbering;\r\n const first = splitToFit(text, limit, unit);\r\n const willNumber = first.parts.length > 1;\r\n\r\n // Re-split against a reduced limit when numbering will be added, so the suffix\r\n // is accounted for rather than discovered afterwards. `99/99` is the widest\r\n // realistic suffix; using the real total would need the split that depends on\r\n // it, which is circular.\r\n const suffixCost = willNumber ? count(numbering(99, 99)) : 0;\r\n const { parts, problems } = willNumber ? splitToFit(text, limit - suffixCost, unit) : first;\r\n\r\n const segments: Segment[] = parts.map((part, index) => {\r\n const withNumber = parts.length > 1 ? `${part}${numbering(index + 1, parts.length)}` : part;\r\n\r\n return {\r\n text: withNumber,\r\n count: count(withNumber),\r\n // Computed per segment and never sliced from the original — an offset into\r\n // the whole text is meaningless once the text has been split.\r\n ...(rules.links ? { links: linkRanges(withNumber) } : {}),\r\n };\r\n });\r\n\r\n const over = segments.filter((segment) => segment.count > limit);\r\n if (over.length > 0) {\r\n problems.push(\r\n `${over.length} segment(s) still exceed ${limit} ${unit} after splitting. That is a renderer defect rather ` +\r\n \"than a content problem — the split should not produce an over-length message.\",\r\n );\r\n }\r\n\r\n return { segments, unit, limit, problems, rendererVersion: RENDERER_VERSION };\r\n}\r\n\r\n/**\r\n * A stable fingerprint of what would actually be sent.\r\n *\r\n * This is what an approval covers. A host recomputes it at dispatch and refuses\r\n * on mismatch: if the rendering changed for ANY reason — edited text, a changed\r\n * limit, a new renderer version — the hashes differ, because the approver\r\n * approved a payload rather than an intention.\r\n *\r\n * Async because it uses WebCrypto, which is the only hash available on every\r\n * runtime the suite supports. A host that wants it synchronously should hold the\r\n * value rather than recompute it.\r\n */\r\nexport async function payloadHash(payload: RenderedPayload): Promise<string> {\r\n const subtle = globalThis.crypto?.subtle;\r\n\r\n if (!subtle) {\r\n throw new Error(\r\n \"WebCrypto is not available in this runtime, so a payload hash cannot be computed. \" +\r\n \"Refusing rather than returning a weaker fingerprint that a host would trust equally.\",\r\n );\r\n }\r\n\r\n const canonical = JSON.stringify({\r\n // Inside the hash, deliberately — see RENDERER_VERSION.\r\n v: payload.rendererVersion,\r\n segments: payload.segments.map((segment) => segment.text),\r\n });\r\n\r\n const digest = await subtle.digest(\"SHA-256\", new TextEncoder().encode(canonical));\r\n\r\n return Array.from(new Uint8Array(digest))\r\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\r\n .join(\"\");\r\n}\r\n\r\n/**\r\n * Refuse empty content, centrally.\r\n *\r\n * The sha of the empty string is a valid sha, so an empty payload passes every\r\n * byte-verification a host can write. Refusing it in one place is what stops\r\n * every one of those checks from being hollow.\r\n */\r\nexport function isEmptyPayload(payload: RenderedPayload): boolean {\r\n return payload.segments.every((segment) => segment.text.trim() === \"\");\r\n}\r\n","/**\r\n * Verifying an inbound provider webhook.\r\n *\r\n * An unverified webhook endpoint is a public, unauthenticated way to start a\r\n * workflow in someone's account — which is to say, a way for a stranger to make\r\n * your app refund an order or post to your Slack. So verification is part of the\r\n * connector pattern rather than an exercise for the host: a trigger node that\r\n * cannot verify its deliveries must say so, and a host must not be able to mount\r\n * one by accident.\r\n *\r\n * ## The three things every provider's scheme actually needs\r\n *\r\n * Providers differ in header names and in what exactly gets signed, and almost\r\n * nothing else:\r\n *\r\n * 1. **The RAW body.** Signatures are computed over bytes. Re-serialising parsed\r\n * JSON changes key order and whitespace and produces a signature mismatch\r\n * that looks like a wrong secret. The host must pass the body it received.\r\n * 2. **A constant-time comparison.** `===` on a signature leaks, through timing,\r\n * which prefix was right. That is a real forgery path, not a theoretical one.\r\n * 3. **A timestamp tolerance.** Without it a valid signature is valid forever,\r\n * so anyone who ever saw one delivery can replay it whenever they like.\r\n *\r\n * WebCrypto rather than `node:crypto`, so the same file runs on Node, Bun, Deno\r\n * and an edge runtime. Node 22 is the suite's floor and has it globally.\r\n */\r\n\r\nexport type WebhookVerification =\r\n | { ok: true }\r\n | { ok: false; reason: string };\r\n\r\nexport type HmacScheme = {\r\n /** Hash to use. */\r\n algorithm: \"SHA-256\" | \"SHA-1\" | \"SHA-512\";\r\n /**\r\n * Build the exact string that gets signed. Providers differ here more than\r\n * anywhere else — Stripe signs `${timestamp}.${body}`, Slack signs\r\n * `v0:${timestamp}:${body}`, GitHub signs the body alone.\r\n */\r\n payload: (raw: string, timestamp?: string) => string;\r\n /** Seconds a delivery stays acceptable. */\r\n tolerance?: number;\r\n /** Encoding of the signature the provider sends. */\r\n encoding?: \"hex\" | \"base64\";\r\n};\r\n\r\n/**\r\n * Verify an HMAC-signed delivery.\r\n *\r\n * Returns a RESULT rather than throwing, and the failure carries a reason: a\r\n * host wants to log which check failed (stale? wrong secret? no header?) while\r\n * still answering the provider with an opaque 400.\r\n */\r\nexport async function verifyHmac(options: {\r\n raw: string;\r\n signature: string | undefined;\r\n secret: string | undefined;\r\n scheme: HmacScheme;\r\n timestamp?: string;\r\n /** Seconds since the epoch. Injected so tests are not clock-dependent. */\r\n now?: number;\r\n}): Promise<WebhookVerification> {\r\n const { raw, signature, secret, scheme, timestamp } = options;\r\n\r\n if (!secret) {\r\n // Never \"accept when unconfigured\". An endpoint that verifies nothing\r\n // because nobody set a secret is strictly worse than one that is off: it\r\n // looks protected.\r\n return { ok: false, reason: \"no signing secret configured for this trigger\" };\r\n }\r\n if (!signature) return { ok: false, reason: \"delivery carried no signature header\" };\r\n\r\n if (scheme.tolerance !== undefined) {\r\n if (!timestamp) return { ok: false, reason: \"delivery carried no timestamp header\" };\r\n\r\n const sent = Number(timestamp);\r\n if (!Number.isFinite(sent)) return { ok: false, reason: \"timestamp header is not a number\" };\r\n\r\n const now = options.now ?? Math.floor(Date.now() / 1000);\r\n if (Math.abs(now - sent) > scheme.tolerance) {\r\n return { ok: false, reason: `delivery is outside the ${scheme.tolerance}s replay window` };\r\n }\r\n }\r\n\r\n const expected = await hmac(secret, scheme.payload(raw, timestamp), scheme.algorithm, scheme.encoding ?? \"hex\");\r\n\r\n return constantTimeEquals(expected, signature)\r\n ? { ok: true }\r\n : { ok: false, reason: \"signature did not match\" };\r\n}\r\n\r\n/** HMAC of `payload` under `secret`, hex or base64 encoded. */\r\nexport async function hmac(\r\n secret: string,\r\n payload: string,\r\n algorithm: HmacScheme[\"algorithm\"],\r\n encoding: \"hex\" | \"base64\" = \"hex\",\r\n): Promise<string> {\r\n const subtle = globalThis.crypto?.subtle;\r\n\r\n if (!subtle) {\r\n throw new Error(\r\n \"WebCrypto is not available in this runtime, so webhook signatures cannot be verified. \" +\r\n \"Refusing to accept deliveries unverified.\",\r\n );\r\n }\r\n\r\n const encoder = new TextEncoder();\r\n const key = await subtle.importKey(\r\n \"raw\",\r\n encoder.encode(secret),\r\n { name: \"HMAC\", hash: algorithm },\r\n false,\r\n [\"sign\"],\r\n );\r\n const bytes = new Uint8Array(await subtle.sign(\"HMAC\", key, encoder.encode(payload)));\r\n\r\n if (encoding === \"base64\") {\r\n let binary = \"\";\r\n bytes.forEach((byte) => {\r\n binary += String.fromCharCode(byte);\r\n });\r\n\r\n return btoa(binary);\r\n }\r\n\r\n return Array.from(bytes)\r\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\r\n .join(\"\");\r\n}\r\n\r\n/**\r\n * Compare two strings without leaking where they diverge.\r\n *\r\n * The length check first is deliberate and safe: a signature's length is fixed\r\n * by its algorithm and is not a secret, so revealing a mismatch there tells an\r\n * attacker nothing they could not compute.\r\n */\r\nexport function constantTimeEquals(a: string, b: string): boolean {\r\n if (a.length !== b.length) return false;\r\n\r\n let diff = 0;\r\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\r\n\r\n return diff === 0;\r\n}\r\n\r\n/**\r\n * Pull one header case-insensitively.\r\n *\r\n * Header case is not preserved consistently across proxies, frameworks and\r\n * runtimes; a connector that read `req.headers[\"Stripe-Signature\"]` would work\r\n * behind one server and reject every delivery behind another.\r\n */\r\nexport function header(headers: Record<string, string | string[] | undefined>, name: string): string | undefined {\r\n const wanted = name.toLowerCase();\r\n\r\n for (const [key, value] of Object.entries(headers)) {\r\n if (key.toLowerCase() !== wanted) continue;\r\n\r\n return Array.isArray(value) ? value[0] : value;\r\n }\r\n\r\n return undefined;\r\n}\r\n","/**\r\n * How a connector trigger learns that something happened.\r\n *\r\n * **Not every provider pushes, and assuming webhooks is the single most common\r\n * way a connector catalogue ends up lying.** Google Drive and Microsoft Graph\r\n * want a *subscription* you must renew before it expires. Salesforce wants a\r\n * Pub/Sub or CometD stream. S3 wants an event notification routed to\r\n * infrastructure the consumer provisions. Reddit wants you to poll. Each of\r\n * those is a different obligation on the host, and a node that declared\r\n * \"webhook\" for all of them would install cleanly and then never fire.\r\n *\r\n * So a trigger declares its delivery mechanism, and the host can refuse to mount\r\n * one it has no machinery for — at author time, not at 3am.\r\n */\r\n\r\nimport type { ResolvedConnection } from \"./connection\";\r\nimport type { ConnectorFaker } from \"./faker\";\r\nimport { fakeRequest } from \"./faker\";\r\nimport type { HmacScheme, WebhookVerification } from \"./webhook\";\r\nimport { header, verifyHmac } from \"./webhook\";\r\n\r\n/**\r\n * The delivery mechanisms that actually exist in the wild.\r\n *\r\n * - `webhook` — the provider POSTs to a URL you own. Verify the signature.\r\n * - `subscription` — a webhook that EXPIRES. Someone has to renew it, forever,\r\n * and if nobody does the workflow stops firing without an error anywhere.\r\n * Distinct from `webhook` precisely because that ongoing duty is invisible.\r\n * - `poll` — you ask, on a schedule, and track a cursor.\r\n * - `pubsub` — the provider publishes to a broker (Google Pub/Sub, Salesforce\r\n * Pub/Sub API, SNS/SQS) that the consumer must provision.\r\n * - `stream` — a long-lived connection the host maintains (websocket, gRPC,\r\n * SSE, CometD).\r\n */\r\nexport type DeliveryMechanism = \"webhook\" | \"subscription\" | \"poll\" | \"pubsub\" | \"stream\";\r\n\r\nexport type TriggerDescriptor = {\r\n service: string;\r\n operation: string;\r\n delivery: DeliveryMechanism;\r\n /**\r\n * What the host has to do before this fires, in one sentence, aimed at the\r\n * person who has to do it. Surfaced by the editor and by the MCP, because a\r\n * trigger whose setup is undocumented is a trigger that silently never fires.\r\n */\r\n setup: string;\r\n /** For `subscription`: how long the provider keeps it alive, in seconds. */\r\n subscriptionTtl?: number;\r\n /** For `webhook` / `subscription`: how a delivery is authenticated. */\r\n verification?: WebhookVerificationSpec;\r\n /** For `poll`: the provider's smallest sane interval, in seconds. */\r\n minPollSeconds?: number;\r\n /** A faked sample event, so the node is runnable before any of the above. */\r\n faker: ConnectorFaker;\r\n};\r\n\r\nexport type WebhookVerificationSpec = {\r\n /** Header carrying the signature. */\r\n signatureHeader: string;\r\n /** Header carrying the timestamp, when the scheme signs one. */\r\n timestampHeader?: string;\r\n scheme: HmacScheme;\r\n /**\r\n * Some schemes pack the timestamp INTO the signature header\r\n * (Stripe: `t=…,v1=…`). Given the raw header value, return the parts.\r\n */\r\n parse?: (raw: string) => { signature?: string; timestamp?: string };\r\n};\r\n\r\nexport type InboundDelivery = {\r\n /** The body EXACTLY as received. Not re-serialised. See `webhook.ts`. */\r\n raw: string;\r\n headers: Record<string, string | string[] | undefined>;\r\n};\r\n\r\n/**\r\n * Verify an inbound delivery against a trigger's declared scheme.\r\n *\r\n * A trigger with no `verification` REFUSES rather than accepts. That asymmetry\r\n * is the whole safety property: an unverifiable endpoint is a stranger's button\r\n * for starting workflows in your account, and defaulting to \"allow\" would make\r\n * every misconfiguration into an open door that looks shut.\r\n */\r\nexport async function verifyDelivery(\r\n trigger: TriggerDescriptor,\r\n delivery: InboundDelivery,\r\n secret: string | undefined,\r\n now?: number,\r\n): Promise<WebhookVerification> {\r\n const spec = trigger.verification;\r\n\r\n if (!spec) {\r\n return {\r\n ok: false,\r\n reason: `${trigger.service}.${trigger.operation} declares no signature scheme, so a delivery cannot be trusted.`,\r\n };\r\n }\r\n\r\n const rawHeader = header(delivery.headers, spec.signatureHeader);\r\n const parsed = spec.parse && rawHeader ? spec.parse(rawHeader) : { signature: rawHeader };\r\n const timestamp = parsed.timestamp\r\n ?? (spec.timestampHeader ? header(delivery.headers, spec.timestampHeader) : undefined);\r\n\r\n return verifyHmac({\r\n raw: delivery.raw,\r\n signature: parsed.signature,\r\n secret,\r\n scheme: spec.scheme,\r\n timestamp,\r\n ...(now === undefined ? {} : { now }),\r\n });\r\n}\r\n\r\n/**\r\n * The event a trigger node publishes when it runs.\r\n *\r\n * Three sources, in order, and the third is why a connector trigger is usable\r\n * on day one:\r\n *\r\n * 1. A verified delivery the host injected on the `in` port.\r\n * 2. Nothing injected, but the connection is remote — that is a host wiring\r\n * error, and it fails loudly rather than emitting a plausible blank.\r\n * 3. `fake` mode — the trigger emits its own sample event, so an author can\r\n * press Run, see the real field names, and wire the downstream nodes against\r\n * them before the provider has ever been contacted.\r\n */\r\nexport function triggerEvent(\r\n trigger: TriggerDescriptor,\r\n connection: ResolvedConnection,\r\n injected: unknown,\r\n config: Record<string, unknown>,\r\n): unknown {\r\n if (hasDelivery(injected)) return injected;\r\n\r\n if (connection.mode === \"fake\") {\r\n return trigger.faker(\r\n trigger.operation,\r\n fakeRequest(trigger.service, trigger.operation, config),\r\n );\r\n }\r\n\r\n throw new Error(\r\n `${trigger.service}.${trigger.operation}: no event was delivered to this trigger. ` +\r\n `Its delivery mechanism is \"${trigger.delivery}\" — ${trigger.setup} ` +\r\n 'Set the node\\'s mode to \"fake\" to design against a sample event instead.',\r\n );\r\n}\r\n\r\n/**\r\n * Whether a value on the `in` port is an actual delivery.\r\n *\r\n * An EMPTY object or array counts as nothing delivered, and that is not\r\n * fussiness: a manually started run seeds `{}` on the trigger's input, so a\r\n * bare null check would hand the executor an empty envelope and call it an\r\n * event. Downstream every field would be `null`, the run would be green, and\r\n * nothing would say the trigger never fired.\r\n */\r\nfunction hasDelivery(value: unknown): boolean {\r\n if (value === undefined || value === null) return false;\r\n if (Array.isArray(value)) return value.length > 0;\r\n if (typeof value === \"object\") return Object.keys(value as object).length > 0;\r\n\r\n return true;\r\n}\r\n"],"mappings":"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","names":["mode","missing","classified","encoder"]}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@particle-academy/fancy-connector-core",
3
+ "version": "0.0.0",
4
+ "description": "The runtime under every Fancy connector: modes and estates, connections, one call path, a failure taxonomy that separates \"it never arrived\" from \"nobody can tell\", deterministic fakers, webhook verification, pure versioned rendering, declared metric shapes, credential-free probes, and API-drift reporting. Owns the wire; never the gates.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Particle-Academy/fancy-connector-core.git"
8
+ },
9
+ "homepage": "https://github.com/Particle-Academy/fancy-connector-core#readme",
10
+ "bugs": "https://github.com/Particle-Academy/fancy-connector-core/issues",
11
+ "type": "module",
12
+ "main": "./dist/index.cjs",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "import": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "require": {
22
+ "types": "./dist/index.d.cts",
23
+ "default": "./dist/index.cjs"
24
+ }
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "CHANGELOG.md",
32
+ "LICENSE"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "dev": "tsup --watch",
37
+ "lint": "tsc --noEmit",
38
+ "test": "node --import tsx --test tests/*.test.ts",
39
+ "clean": "rm -rf dist",
40
+ "prepublishOnly": "tsup"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^26.2.0",
44
+ "tsup": "^8.5.0",
45
+ "tsx": "^4.19.0",
46
+ "typescript": "^5.8.0"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "keywords": [
52
+ "connectors",
53
+ "api",
54
+ "integrations",
55
+ "webhooks",
56
+ "idempotency",
57
+ "retry",
58
+ "openapi",
59
+ "api-drift",
60
+ "fancy-ui",
61
+ "particle-academy"
62
+ ],
63
+ "license": "MIT",
64
+ "sideEffects": false,
65
+ "overrides": {
66
+ "esbuild": "^0.28.1"
67
+ },
68
+ "engines": {
69
+ "node": ">=22"
70
+ }
71
+ }