@revenexx/integrations-node-sdk 1.9.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ import type { LocalizedString } from './types.js';
2
+ /**
3
+ * Reduce a {@link LocalizedString} to a single plain string.
4
+ *
5
+ * Every consumer (the UI, the Laravel side) repeats the same "is it a
6
+ * string or a localized map?" branch to render a name/label; this centralizes
7
+ * it so the rules are defined once:
8
+ *
9
+ * - a plain string is trimmed and returned (or `undefined` if blank);
10
+ * - a localized map prefers `fallbackLang` (default `en`), then the first
11
+ * non-empty value;
12
+ * - a missing, empty, or blank-only value yields `undefined`, so the caller
13
+ * can apply its own fallback.
14
+ */
15
+ export declare function normalizeLocalized(value: LocalizedString | undefined | null, fallbackLang?: string): string | undefined;
@@ -0,0 +1,83 @@
1
+ import type { ICredential, ICredentialDescription, INode, INodeDescription, ITemplateDescription } from './types.js';
2
+ /**
3
+ * Envelope version expected by the integrations server-side
4
+ * `TarballInspector`. Earlier (pre-registry) builds emitted a bare array
5
+ * without any `manifestVersion` field; `v0-draft` is currently the only
6
+ * accepted value (see `SchemaServiceProvider::DOMAIN_NODE` in the
7
+ * integrations service). Keeping it versioned lets the registry evolve the
8
+ * shape without breaking older publishers.
9
+ */
10
+ export declare const MANIFEST_VERSION = "v0-draft";
11
+ export interface NodeManifest {
12
+ manifestVersion: typeof MANIFEST_VERSION;
13
+ nodes: INodeDescription[];
14
+ /**
15
+ * Credential types this package publishes. Optional and additive: packages
16
+ * that ship no credentials omit it (older publishers never set it).
17
+ */
18
+ credentials?: ICredentialDescription[];
19
+ /**
20
+ * Workflow templates this package publishes. Optional and additive: packages
21
+ * that ship no templates omit it. Templates are plain data (no executable
22
+ * code), so they are carried verbatim from the package's `TEMPLATES` export.
23
+ */
24
+ templates?: ITemplateDescription[];
25
+ }
26
+ /**
27
+ * The registry-relevant fields of a node package's `package.json`, as read by
28
+ * the CLI. `name`/`version` identify the package; `displayName` is the
29
+ * human-readable bundle label shown in the editor's node palette (e.g.
30
+ * „Business Central"); `icon` marks its folder there. All four are read straight
31
+ * from `package.json` by the integrations server on upload — the CLI reads them
32
+ * only to warn about a missing label, to annotate the manifest log line, and to
33
+ * stop on an icon the server would refuse.
34
+ *
35
+ * The label lives under a namespaced `revenexx` group in `package.json`
36
+ * (`{ "revenexx": { "displayName": "…" } }`), not a bespoke top-level key, so
37
+ * it can't collide with unrelated tooling that squats on `displayName`.
38
+ */
39
+ export interface NodePackageMeta {
40
+ name: string;
41
+ version: string;
42
+ /** Human-readable bundle label (e.g. „Business Central"); optional. */
43
+ displayName?: string;
44
+ /**
45
+ * The glyph that marks the package's folder in the studio's palette, as
46
+ * `lucide:<kebab-name>` (e.g. `lucide:bell`); optional. Declared by a package
47
+ * holding several services, which has no one vendor logo to mark its folder
48
+ * with — a package that is one vendor leaves it out. Trimmed, and a value that
49
+ * is not a string counts as none, as the server reads it; whether it is well
50
+ * formed is {@link packageIconProblem}'s question.
51
+ */
52
+ icon?: string;
53
+ }
54
+ /**
55
+ * Why a declared `revenexx.icon` would be refused by the registry, or
56
+ * `undefined` when it is fine (or absent). Lets the build stop where the author
57
+ * can still fix it, rather than at upload. Takes the icon as
58
+ * {@link parsePackageMeta} returns it — trimmed as the registry trims it; an
59
+ * untrimmed raw value can be refused here although the registry accepts it.
60
+ */
61
+ export declare function packageIconProblem(icon: string | undefined): string | undefined;
62
+ /**
63
+ * Extracts {@link NodePackageMeta} from parsed `package.json` contents, keeping
64
+ * only the registry-relevant fields and coercing anything malformed to a safe
65
+ * shape. All four fields are trimmed — the icon of ASCII whitespace only, as the
66
+ * server's PHP `trim()` does, so a pasted no-break space is refused here as it
67
+ * is there rather than trimmed away; a blank or whitespace-only value becomes
68
+ * `''` (`name`/`version`) or `undefined` (`displayName`, `icon`, matching how the
69
+ * server treats them), so whitespace can't masquerade as a present value in
70
+ * tooling. The bundle label and the folder icon are read from the `revenexx`
71
+ * group (`revenexx.displayName`, `revenexx.icon`).
72
+ * Does not validate that `name`/`version` are present — the integrations server
73
+ * enforces that on upload; this is a typed, lenient read for tooling.
74
+ */
75
+ export declare function parsePackageMeta(raw: unknown): NodePackageMeta;
76
+ /**
77
+ * Builds the manifest envelope from a package's `NODES` (and optional
78
+ * `CREDENTIALS` / `TEMPLATES`) exports. The result is what gets written to
79
+ * `dist/manifest.json` and uploaded to the registry inside the tarball. The
80
+ * bundle label is NOT carried here — the integrations server reads it straight
81
+ * from `package.json` (`revenexx.displayName`).
82
+ */
83
+ export declare function buildManifest(nodes: INode[], credentials?: ICredential[], templates?: ITemplateDescription[]): NodeManifest;
@@ -0,0 +1,61 @@
1
+ import type { INodeResult, IOutputPort } from './types.js';
2
+ /**
3
+ * Canonical error-output shape emitted on a node's `error` port. It lives here
4
+ * rather than in a node package because all four of them were building it
5
+ * separately — `integrations-nodes-core` in an internal module, `deepl` in a
6
+ * copy of that module, Business Central in `bc-fetch.ts` and Pipedrive in
7
+ * `pipedrive-client.ts` — so the workflow editor's guarantee of one
8
+ * `{ code, message, status }` payload rested on four files agreeing by hand
9
+ * (PO-442; asked for by PO-134 in June and not done then).
10
+ */
11
+ export interface NodeErrorOutput {
12
+ /** Machine-readable error code, e.g. `HTTP_ERROR`, `TIMEOUT`, `REQUEST_FAILED`. */
13
+ code: string;
14
+ message: string;
15
+ /** HTTP status code, or `0` for timeouts / network / non-HTTP errors. */
16
+ status: number;
17
+ /** Optional response body — HTTP-specific. */
18
+ body?: unknown;
19
+ }
20
+ /**
21
+ * Normalize any caught value into the canonical {@link NodeErrorOutput} shape.
22
+ * Use this in a node's `catch` block before routing to the `error` port.
23
+ */
24
+ export declare function toErrorOutput(err: unknown): NodeErrorOutput;
25
+ /** Wrap an error output into an `INodeResult` routed to the `error` port. */
26
+ export declare function errorResult(output: NodeErrorOutput, port?: string): INodeResult;
27
+ /**
28
+ * Catch-block shortcut: normalize any thrown value and route it to the failure
29
+ * output. The one helper every node's `catch` funnels into, so the
30
+ * `{ code, message, status }` mapping lives in a single place instead of being
31
+ * re-implemented per protocol (FTP/SFTP) or per package.
32
+ *
33
+ * `port` is the output taken, `error` unless the node declares its failure
34
+ * output under another name (`{ ...errorPort(), name: 'failed' }`) — pass the
35
+ * same name here, or the branch names a port that node does not have.
36
+ */
37
+ export declare function toErrorResult(err: unknown, port?: string): INodeResult;
38
+ /**
39
+ * Catch-block shortcut for the HTTP-style nodes whose failure output additionally
40
+ * declares `body`: the canonical `{ code, message, status }` mapping plus the
41
+ * response body a {@link NodeError} carries in `meta.body` (`null` when the
42
+ * failure never reached a response). `port` names the output as above.
43
+ */
44
+ export declare function httpErrorResult(err: unknown, port?: string): INodeResult;
45
+ /**
46
+ * Shared `error` output port — the uniform `{ code, message, status }` triple
47
+ * declared on every node that has an `error` branch. Pass `body: true` for
48
+ * HTTP-style nodes that additionally emit a response body on the port.
49
+ *
50
+ * The sentences travel with the port. The three values mean the same thing
51
+ * behind every node, so they are written here once instead of per node; a node
52
+ * whose failures are worth naming more precisely spreads the port and replaces
53
+ * only its `description` (`{ ...errorPort(), description: … }`).
54
+ *
55
+ * The second sentence of the description is the rule this port exists under,
56
+ * and it is promised in `specs/error-handling.md`: a fault in the author's own
57
+ * configuration ends the run, a fault met while running is offered here.
58
+ */
59
+ export declare function errorPort(opts?: {
60
+ body?: boolean;
61
+ }): IOutputPort;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The comparison vocabulary, in one place because it is read in three:
3
+ * a node that compares two values (a condition, a filter), a settings condition
4
+ * saying when a field applies (`IConfigField.showIf`), and the editor drawing
5
+ * that field. An author who has met these words in one of the three must not
6
+ * have to learn a second set for the others.
7
+ *
8
+ * The names are the vocabulary; what each one *means* is {@link evaluate}, and
9
+ * the answer table in `operators.test.ts` is what a second implementation of it
10
+ * — the editor, the platform's workflow validator — is checked against.
11
+ */
12
+ export declare const OPERATORS: readonly ['equals', 'notEquals', 'contains', 'notContains', 'startsWith', 'endsWith', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'exists', 'notExists', 'isEmpty', 'isNotEmpty'];
13
+ export type Operator = (typeof OPERATORS)[number];
14
+ /** The four that read whether a value is there rather than what it is. */
15
+ export declare const VALUELESS_OPERATORS: readonly Operator[];
16
+ export declare function isOperator(value: unknown): value is Operator;
17
+ /** Whether an operator reads a second value at all, or only the first. */
18
+ export declare function takesValue(op: Operator): boolean;
19
+ /**
20
+ * What an operator means.
21
+ *
22
+ * Equality is deliberately lenient across the string/number line: a `select`
23
+ * carries `1` where a text box carries `'1'`, and an author means one thing by
24
+ * both. A second implementation has to reproduce that — and has to be careful
25
+ * with booleans, whose default stringification differs between languages.
26
+ */
27
+ export declare function evaluate(left: unknown, op: Operator, right: unknown): boolean;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Transport-agnostic retry/backoff primitive (PO-139).
3
+ *
4
+ * Wraps *any* async operation — raw `fetch`, an SDK client (axios), a future
5
+ * transport — with exponential backoff + full jitter, honouring a
6
+ * server-dictated delay (HTTP `Retry-After`) when the caller supplies one.
7
+ *
8
+ * The retry decision lives with the connector: it throws a {@link RetryableError}
9
+ * only when an attempt failed *and* is worth retrying. Everything else is
10
+ * rethrown as-is, and terminal API errors that connectors model as *values*
11
+ * (e.g. a `{ kind: 'http-error' }` union) simply flow back as the return value.
12
+ *
13
+ * This is deliberately NOT a circuit breaker, rate limiter, or HTTP client, and
14
+ * it does not replace Temporal activity-level retries — it is the fine-grained,
15
+ * idempotency-aware, `Retry-After`-honouring layer inside a single activity.
16
+ */
17
+ /** Thrown by the wrapped fn to signal "this attempt failed but is retryable". */
18
+ export declare class RetryableError extends Error {
19
+ /** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
20
+ readonly retryAfterMs?: number;
21
+ constructor(message: string, opts?: {
22
+ retryAfterMs?: number;
23
+ cause?: unknown;
24
+ });
25
+ }
26
+ export interface RetryPolicy {
27
+ /** Total tries including the first. */
28
+ maxAttempts: number;
29
+ /** Base delay for the exponential backoff. */
30
+ baseDelayMs: number;
31
+ /** Upper bound for any single computed delay. */
32
+ maxDelayMs: number;
33
+ /** Exponential growth factor. */
34
+ factor: number;
35
+ /** Apply full jitter to the computed backoff. */
36
+ jitter: boolean;
37
+ }
38
+ export declare const DEFAULT_RETRY_POLICY: RetryPolicy;
39
+ export interface RetryHooks {
40
+ /** Pass `ctx.signal` so cancelling a workflow cancels the wait (and stops retrying) immediately. */
41
+ signal: AbortSignal;
42
+ logger?: {
43
+ warn(msg: string, meta?: Record<string, unknown>): void;
44
+ };
45
+ onRetry?(info: {
46
+ attempt: number;
47
+ delayMs: number;
48
+ error: unknown;
49
+ }): void;
50
+ }
51
+ /**
52
+ * Pure backoff math, exported for testing/reuse. Returns the delay before the
53
+ * retry that follows the given (1-based) failed `attempt`: an exponential term
54
+ * `baseDelayMs * factor^(attempt-1)` capped at `maxDelayMs`, with full jitter
55
+ * (`random() * cap`) when `policy.jitter` is set.
56
+ */
57
+ export declare function backoffDelay(attempt: number, policy: RetryPolicy): number;
58
+ /**
59
+ * Abort-aware sleep. Resolves after `ms`, or rejects with the signal's reason
60
+ * (a DOMException `AbortError` by default) as soon as `signal` aborts — so a
61
+ * caller's retry loop stops cleanly instead of finishing the wait.
62
+ */
63
+ export declare function sleepWithSignal(ms: number, signal: AbortSignal): Promise<void>;
64
+ /**
65
+ * Runs `fn`; if it throws a {@link RetryableError} and attempts remain, waits
66
+ * (`RetryableError.retryAfterMs` when present, else the computed
67
+ * {@link backoffDelay}) respecting `signal`, then retries. Any other throw is
68
+ * rethrown immediately; on the final attempt the RetryableError itself is
69
+ * thrown (preserving its `retryAfterMs`/`cause`), not its `.cause`.
70
+ *
71
+ * Distinct from `safeFetch`'s built-in fixed-delay `retry` option, which is
72
+ * unchanged: that is a simple in-fetch retry; this is the richer, transport-
73
+ * agnostic, `Retry-After`-aware primitive connectors wrap around any operation.
74
+ */
75
+ export declare function withRetry<T>(fn: (attempt: number) => Promise<T>, policy: Partial<RetryPolicy>, hooks: RetryHooks): Promise<T>;
package/dist/ssrf.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { NodeError } from './errors.js';
2
+ /**
3
+ * A resolved DNS address, mirroring the shape of Node's `dns.LookupAddress`.
4
+ * `family` is `4` or `6`; only `address` is consulted by the guard.
5
+ */
6
+ export interface LookupAddress {
7
+ address: string;
8
+ family: number;
9
+ }
10
+ /**
11
+ * Resolves a hostname to every address it maps to. Modelled on
12
+ * `dns.lookup(host, { all: true })`. Injectable so tests can drive the guard
13
+ * deterministically without real DNS — see {@link ssrfResolver}.
14
+ */
15
+ export type LookupFn = (hostname: string) => Promise<LookupAddress[]>;
16
+ /**
17
+ * The DNS resolver the SSRF guard uses when a caller does not pass an explicit
18
+ * `lookup`. It is a mutable holder (rather than a bare function) purely so tests
19
+ * can swap `ssrfResolver.lookup` via a spy and restore it afterwards — the guard
20
+ * itself always runs. **Do not repoint this in production.**
21
+ */
22
+ export declare const ssrfResolver: {
23
+ lookup: LookupFn;
24
+ };
25
+ /**
26
+ * Return `true` when `ip` (a literal IPv4/IPv6 address) points at a private,
27
+ * loopback, link-local, carrier-grade NAT, multicast, broadcast, reserved,
28
+ * unallocated or otherwise non-public target that a server-side fetch must never be
29
+ * steered to.
30
+ *
31
+ * The verdict is an allow-list: the address is refused unless the classification
32
+ * calls its range public unicast and — for IPv6, where that verdict is a default
33
+ * rather than a determination — it also sits inside the block IANA has allocated
34
+ * (see {@link GLOBAL_UNICAST_V6}). The IPv6 forms that embed an IPv4 address are
35
+ * unwrapped and the embedded address judged in its place, so a public host stays
36
+ * reachable when it is written that way. An address we cannot parse is treated as
37
+ * blocked (fail-closed).
38
+ *
39
+ * Promised behaviour: specs/ssrf-guard.md (AC-9, AC-11, AC-19).
40
+ */
41
+ export declare function isBlockedAddress(ip: string): boolean;
42
+ /**
43
+ * Put `url`'s host **and port** under the connect-time guard and return the
44
+ * release for it. Ref-counted, so concurrent calls to one target do not release
45
+ * each other. Host and port together are as narrow as the scope can be made: the
46
+ * `connected` message carries the target and not the caller, so this says which
47
+ * connections are *candidates* for judgement, never which one is ours.
48
+ *
49
+ * The window is the call, not the connection. A connect that completes after the
50
+ * release — the fetch it belonged to having already timed out or aborted — finds
51
+ * its target unregistered and is not judged; see the gap in
52
+ * `specs/ssrf-guard.md`.
53
+ *
54
+ * Deliberately **not** re-exported from `index.ts`. The guard is engaged by
55
+ * `safeFetch`, which is the one sanctioned way out to the network (PO-185); an
56
+ * exported handle would be a second one, and one that is easy to hold wrongly —
57
+ * the judgement it enables lasts exactly as long as the registration does.
58
+ */
59
+ export declare function guardConnectionsTo(url: URL): () => void;
60
+ /**
61
+ * The refusal this guard raised on a socket, as it comes back out of `fetch`.
62
+ * undici reports a destroyed connection as `TypeError('fetch failed')` carrying
63
+ * the destroy reason as its `cause`, so the caller sees a generic network
64
+ * failure — which `safeFetch` would then *retry*. Unwrap it, so a blocked target
65
+ * surfaces as the deterministic `BLOCKED_ADDRESS` it is.
66
+ */
67
+ export declare function connectionRefusal(err: unknown): NodeError | undefined;
68
+ /**
69
+ * Assert that `url` is safe for a server-side fetch: an http(s) URL whose target
70
+ * resolves only to public addresses. Rejects non-http(s) protocols, empty hosts
71
+ * and `localhost`, checks literal-IP hosts directly, and otherwise resolves the
72
+ * hostname (via the injectable `lookup`, defaulting to {@link ssrfResolver}) and
73
+ * rejects if **any** resolved address is private/reserved. Throws
74
+ * `NodeError('BLOCKED_ADDRESS', …, { status: 0 })` on rejection.
75
+ *
76
+ * This is the pre-flight half of the guard: the connection that follows resolves
77
+ * the name again on its own, so `safeFetch` also judges the address the socket
78
+ * actually reached (PO-184). A caller that opens its own request after
79
+ * `assertPublicUrl` gets the check and not that second half.
80
+ *
81
+ * Pass `signal` (the per-request timeout/cancellation budget) so a hung or
82
+ * hostile DNS resolve cannot block the guard past that budget — see
83
+ * {@link resolveHost}.
84
+ */
85
+ export declare function assertPublicUrl(url: string | URL, opts?: {
86
+ lookup?: LookupFn;
87
+ signal?: AbortSignal;
88
+ }): Promise<void>;