@revenexx/integrations-node-sdk 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conditions.d.ts +16 -0
- package/dist/credentialType.d.ts +17 -0
- package/dist/credentials.d.ts +162 -0
- package/dist/errors.d.ts +5 -0
- package/dist/extract.d.ts +5 -0
- package/dist/fetch.d.ts +133 -0
- package/dist/images.d.ts +28 -0
- package/dist/index.cjs +62 -7
- package/dist/index.d.ts +14 -1254
- package/dist/index.js +62 -7
- package/dist/localized.d.ts +15 -0
- package/dist/manifest.d.ts +83 -0
- package/dist/nodeErrors.d.ts +61 -0
- package/dist/operators.d.ts +27 -0
- package/dist/retry.d.ts +75 -0
- package/dist/ssrf.d.ts +88 -0
- package/dist/types.d.ts +620 -0
- package/package.json +4 -4
- package/dist/index.d.cts +0 -1254
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IShowIfCondition } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a setting applies, given what the author has filled in so far.
|
|
4
|
+
*
|
|
5
|
+
* A setting with no condition always applies — which is what makes `showIf`
|
|
6
|
+
* additive: a node that says nothing gets the behaviour it has today, and an
|
|
7
|
+
* editor that does not know the key draws every field as before.
|
|
8
|
+
*
|
|
9
|
+
* The value the condition reads is taken from the config as it stands mid-edit,
|
|
10
|
+
* so it is routinely `undefined`: the moment a node is dropped on the canvas
|
|
11
|
+
* nothing is filled in. That is why `equals` against a missing value is `false`
|
|
12
|
+
* rather than an error — the setting does not apply *yet*.
|
|
13
|
+
*/
|
|
14
|
+
export declare function settingApplies(field: {
|
|
15
|
+
showIf?: IShowIfCondition;
|
|
16
|
+
}, config: Record<string, unknown>): boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reduce an `IConfigField.credentialType` (a single slug or an array of slugs)
|
|
3
|
+
* to a normalized, deduplicated `string[]`.
|
|
4
|
+
*
|
|
5
|
+
* The union `string | string[]` exists so node authors can write the common
|
|
6
|
+
* single-type case as a bare string, but every consumer (the editor that
|
|
7
|
+
* lists tenant credential instances, the Laravel side) needs a list to
|
|
8
|
+
* iterate. This centralizes the
|
|
9
|
+
* "is it a string or an array?" branch — analogous to {@link normalizeLocalized}
|
|
10
|
+
* for `LocalizedString` — so the rules are defined once:
|
|
11
|
+
*
|
|
12
|
+
* - a single non-empty slug becomes a one-element array;
|
|
13
|
+
* - an array is trimmed of blank entries and deduplicated, preserving order;
|
|
14
|
+
* - `undefined`, an empty string, or an empty/all-blank array yields `[]`, so
|
|
15
|
+
* the caller can treat "accepts nothing" uniformly.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeCredentialType(value: string | string[] | undefined): string[];
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { ICredential, ICredentialContext, ICredentialDescription, ICredentialOAuthAuthorize, ICredentialResolveResult, ICredentialTestResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reusable credential base classes. Concrete credential types (in node
|
|
4
|
+
* packages) extend one of these and only supply field mappings / endpoints —
|
|
5
|
+
* the auth strategy itself lives here so it is implemented once.
|
|
6
|
+
*
|
|
7
|
+
* All of this runs in the credentials broker (a Node side-container), never in
|
|
8
|
+
* workflow code, so wall-clock time and network I/O are fine.
|
|
9
|
+
*/
|
|
10
|
+
type Config = Record<string, unknown>;
|
|
11
|
+
type DurableCreds = Record<string, unknown> | null;
|
|
12
|
+
export interface OAuthTokenResponse {
|
|
13
|
+
access_token?: string;
|
|
14
|
+
refresh_token?: string;
|
|
15
|
+
expires_in?: number;
|
|
16
|
+
token_type?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Provider-specific base URL the access token must target (e.g. Pipedrive
|
|
19
|
+
* returns the company-scoped `api_domain` on both the initial exchange and
|
|
20
|
+
* every refresh). Absent for providers that use a single global API host.
|
|
21
|
+
*/
|
|
22
|
+
api_domain?: string;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The base for every credential type. Stores the published description and
|
|
27
|
+
* provides a small HTTP helper. `resolve` is abstract; `test` defaults to a
|
|
28
|
+
* presence check and should be overridden where a real connection test exists.
|
|
29
|
+
*/
|
|
30
|
+
export declare abstract class BaseCredential implements ICredential {
|
|
31
|
+
abstract readonly description: ICredentialDescription;
|
|
32
|
+
abstract resolve(ctx: ICredentialContext, config: Config, durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
|
|
33
|
+
test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
34
|
+
/**
|
|
35
|
+
* POST `application/x-www-form-urlencoded` and parse a JSON token response.
|
|
36
|
+
*
|
|
37
|
+
* Goes through {@link safeFetch}, not a raw `fetch` (PO-185). The token
|
|
38
|
+
* endpoint is not a constant: `tokenUrl(config)` derives it from credential
|
|
39
|
+
* config, i.e. from a value someone types into a credential form. A raw fetch
|
|
40
|
+
* would happily POST the client secret at `http://169.254.169.254/…` or any
|
|
41
|
+
* other internal address, which is precisely what `assertPublicUrl` exists to
|
|
42
|
+
* prevent. Every OAuth token exchange in this file — client-credentials,
|
|
43
|
+
* auth-code, refresh — funnels through here, so this is the one call site that
|
|
44
|
+
* has to hold.
|
|
45
|
+
*
|
|
46
|
+
* Two behaviours safeFetch adds, both wanted here: a request budget
|
|
47
|
+
* (`DEFAULT_TIMEOUT_MS`) on top of `ctx.signal`, and manual redirect handling
|
|
48
|
+
* that re-checks each hop and drops `Authorization` across an origin boundary.
|
|
49
|
+
* Note that a 301/302 answer to this POST is downgraded to a bodiless GET —
|
|
50
|
+
* per the redirect rules, not a quirk of ours. Real token endpoints do not
|
|
51
|
+
* redirect; one that does was never going to complete the exchange anyway.
|
|
52
|
+
*
|
|
53
|
+
* The third protection is the caller's own: safeFetch hands back a `Response`
|
|
54
|
+
* and reading it is not its job, so the answer goes through {@link readText}
|
|
55
|
+
* under {@link MAX_TOKEN_RESPONSE_BYTES} rather than a bare `res.text()`.
|
|
56
|
+
* Both branches below read that same string, so the cap covers the refusal as
|
|
57
|
+
* well as the token — an error body is the less predictable of the two.
|
|
58
|
+
*/
|
|
59
|
+
protected postForm(ctx: ICredentialContext, url: string, form: Record<string, string | undefined>, headers?: Record<string, string>): Promise<OAuthTokenResponse>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* "Simple value retrieval": the validated config fields ARE the access data
|
|
63
|
+
* handed to the node (e.g. SMTP host/port/user/password, SFTP host/key). No
|
|
64
|
+
* token, no expiry. Override `test` to add a real connection check.
|
|
65
|
+
*/
|
|
66
|
+
export declare abstract class SimpleValueCredential extends BaseCredential {
|
|
67
|
+
resolve(_ctx: ICredentialContext, config: Config, _durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* API-key credential. Reads the key from the configured field and hands it to
|
|
71
|
+
* the node under that same name; override {@link credentialShape} where the
|
|
72
|
+
* node needs something else, or more than the key alone.
|
|
73
|
+
*/
|
|
74
|
+
export declare abstract class ApiKeyCredential extends BaseCredential {
|
|
75
|
+
/**
|
|
76
|
+
* Which field of the credential form the key is typed into. Override it to
|
|
77
|
+
* name the form field this provider calls its key — `botToken`, `authKey`,
|
|
78
|
+
* `webhookUrl`.
|
|
79
|
+
*
|
|
80
|
+
* **What this does not do** (PO-497): it does not decide what a node looks
|
|
81
|
+
* the key up under at run time. It used to do only half of that — it changed
|
|
82
|
+
* which field was *read* while {@link credentialShape} went on emitting
|
|
83
|
+
* `apiKey` — so a credential that renamed its form field alone handed the
|
|
84
|
+
* node a key under a name it was never told to expect, and the node refused
|
|
85
|
+
* a perfectly good key as a missing one. The default shape now follows this
|
|
86
|
+
* name, so overriding this alone is correct on both sides. Overriding
|
|
87
|
+
* {@link credentialShape} as well is still how a credential emits a
|
|
88
|
+
* different name, or more than the key.
|
|
89
|
+
*
|
|
90
|
+
* Note that no node test can see a mismatch between the two: a mocked node
|
|
91
|
+
* context is seeded with the resolved blob directly and never calls
|
|
92
|
+
* `resolve`. Only a credential test asserting the whole resolved shape
|
|
93
|
+
* catches it, and asserting it loosely still misses — a partial match is
|
|
94
|
+
* happy with the wrong key present beside the right one.
|
|
95
|
+
*/
|
|
96
|
+
protected apiKeyField(): string;
|
|
97
|
+
/**
|
|
98
|
+
* What the node receives. Defaults to the key under the name
|
|
99
|
+
* {@link apiKeyField} returns, so the two cannot drift apart by default.
|
|
100
|
+
*
|
|
101
|
+
* Override it where the node needs a different name than the form uses, or
|
|
102
|
+
* needs more than the key — a base URL derived from it, say.
|
|
103
|
+
*/
|
|
104
|
+
protected credentialShape(apiKey: string): Record<string, unknown>;
|
|
105
|
+
resolve(_ctx: ICredentialContext, config: Config, _durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* HTTP Basic auth credential. Exposes `{ username, password }` by default.
|
|
109
|
+
*/
|
|
110
|
+
export declare abstract class BasicAuthCredential extends BaseCredential {
|
|
111
|
+
protected usernameField(): string;
|
|
112
|
+
protected passwordField(): string;
|
|
113
|
+
resolve(_ctx: ICredentialContext, config: Config, _durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* OAuth2 client-credentials grant (service-to-service, no user, no
|
|
117
|
+
* refresh_token). Concrete types supply the token endpoint + client creds via
|
|
118
|
+
* the config field map. Robust to downtime: a fresh access token is minted on
|
|
119
|
+
* every resolve when the broker cache misses.
|
|
120
|
+
*/
|
|
121
|
+
export declare abstract class OAuth2ClientCredentialsCredential extends BaseCredential {
|
|
122
|
+
protected abstract tokenUrl(config: Config): string;
|
|
123
|
+
protected abstract clientId(config: Config): string;
|
|
124
|
+
protected abstract clientSecret(config: Config): string;
|
|
125
|
+
protected scope(_config: Config): string | undefined;
|
|
126
|
+
/** Map the raw token response to the node-facing credentials. */
|
|
127
|
+
protected credentialShape(token: OAuthTokenResponse): Record<string, unknown>;
|
|
128
|
+
resolve(ctx: ICredentialContext, config: Config, _durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
|
|
129
|
+
test(ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* OAuth2 authorization-code grant (3-legged, interactive consent). Implements
|
|
133
|
+
* the consent dance (with PKCE by default), code exchange, and refresh-token
|
|
134
|
+
* based resolution. The long-lived `refresh_token` is stored in durableCreds;
|
|
135
|
+
* if the provider rotates it, the new one is persisted via the context.
|
|
136
|
+
*/
|
|
137
|
+
export declare abstract class OAuth2AuthCodeCredential extends BaseCredential implements ICredentialOAuthAuthorize {
|
|
138
|
+
protected abstract authorizeUrl(config: Config): string;
|
|
139
|
+
protected abstract tokenUrl(config: Config): string;
|
|
140
|
+
protected abstract clientId(config: Config): string;
|
|
141
|
+
protected abstract clientSecret(config: Config): string;
|
|
142
|
+
protected scope(_config: Config): string | undefined;
|
|
143
|
+
protected usePkce(): boolean;
|
|
144
|
+
protected credentialShape(token: OAuthTokenResponse): Record<string, unknown>;
|
|
145
|
+
buildAuthorizeUrl(_ctx: ICredentialContext, config: Config, params: {
|
|
146
|
+
redirectUri: string;
|
|
147
|
+
state: string;
|
|
148
|
+
}): Promise<{
|
|
149
|
+
authorizeUrl: string;
|
|
150
|
+
codeVerifier?: string;
|
|
151
|
+
}>;
|
|
152
|
+
exchangeCode(ctx: ICredentialContext, config: Config, params: {
|
|
153
|
+
code: string;
|
|
154
|
+
redirectUri: string;
|
|
155
|
+
codeVerifier?: string;
|
|
156
|
+
}): Promise<{
|
|
157
|
+
durableCreds: Record<string, unknown>;
|
|
158
|
+
}>;
|
|
159
|
+
resolve(ctx: ICredentialContext, config: Config, durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
|
|
160
|
+
test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
161
|
+
}
|
|
162
|
+
export {};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ICredential, ICredentialDescription, INode, INodeDescription } from './types.js';
|
|
2
|
+
export declare function extractManifest(node: INode): INodeDescription;
|
|
3
|
+
export declare function extractManifests(nodes: INode[]): INodeDescription[];
|
|
4
|
+
export declare function extractCredentialManifest(credential: ICredential): ICredentialDescription;
|
|
5
|
+
export declare function extractCredentialManifests(credentials: ICredential[]): ICredentialDescription[];
|
package/dist/fetch.d.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { IConfigField } from './types.js';
|
|
2
|
+
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
3
|
+
export declare const MAX_TIMEOUT_MS = 120000;
|
|
4
|
+
/** Maximum number of redirect hops `safeFetch` follows before giving up. */
|
|
5
|
+
export declare const MAX_REDIRECTS = 5;
|
|
6
|
+
/**
|
|
7
|
+
* Whether a request header with this name goes on to the next hop when
|
|
8
|
+
* {@link safeFetch} follows a redirect to another origin — `true` for `Accept`,
|
|
9
|
+
* `Accept-Encoding`, `Accept-Language`, `Content-Encoding`, `Content-Language`,
|
|
10
|
+
* `Content-Type`, `Idempotency-Key`, `If-Match`, `If-Modified-Since`,
|
|
11
|
+
* `If-None-Match`, `If-Range`, `If-Unmodified-Since`, `Range`, `SOAPAction` and
|
|
12
|
+
* `User-Agent`, in any case, and `false` for every other name. The keep-list is by
|
|
13
|
+
* name, so it can only keep a secret out of the headers it drops: a credential
|
|
14
|
+
* whose header name the author chooses must refuse a name this answers `true`
|
|
15
|
+
* for, or its key would cross with the harmless headers.
|
|
16
|
+
*/
|
|
17
|
+
export declare function headerCrossesOrigin(name: string): boolean;
|
|
18
|
+
export declare const DEFAULT_RETRY_ATTEMPTS = 0;
|
|
19
|
+
export declare const MAX_RETRY_ATTEMPTS = 5;
|
|
20
|
+
export declare const DEFAULT_RETRY_DELAY_MS = 1000;
|
|
21
|
+
/**
|
|
22
|
+
* Default cap for response bodies read via {@link readArrayBuffer} /
|
|
23
|
+
* {@link readText} / {@link readJsonOrText}. Guards the (shared) worker process
|
|
24
|
+
* against a single oversized response exhausting its memory.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DEFAULT_MAX_RESPONSE_BYTES: number;
|
|
27
|
+
/**
|
|
28
|
+
* Hard upper ceiling for any per-node `maxBytes`. Even a permissive node author
|
|
29
|
+
* (who calls {@link maxBytesConfigField} without an explicit `max`, or passes a
|
|
30
|
+
* large `maxBytes` to the `read*` helpers) can never lift the cap above this, so
|
|
31
|
+
* the shared worker's memory stays bounded. Analogous to {@link MAX_TIMEOUT_MS}.
|
|
32
|
+
*/
|
|
33
|
+
export declare const MAX_RESPONSE_BYTES: number;
|
|
34
|
+
/** Clamp a requested `maxBytes` into `[1, MAX_RESPONSE_BYTES]`. */
|
|
35
|
+
export declare function clampResponseBytes(maxBytes: number): number;
|
|
36
|
+
export interface SafeFetchRetry {
|
|
37
|
+
attempts: number;
|
|
38
|
+
delayMs?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Binds a request to the one origin its credential belongs to (PO-688).
|
|
42
|
+
*
|
|
43
|
+
* Setting it declares the whole request secret-bearing — headers, query, path
|
|
44
|
+
* and body alike — so nothing has to name which part carries the secret: a
|
|
45
|
+
* request to any other origin is refused with `CREDENTIAL_ORIGIN`, on the first
|
|
46
|
+
* hop and on every redirect, before that origin is looked up or sent anything.
|
|
47
|
+
*/
|
|
48
|
+
export interface SafeFetchCredential {
|
|
49
|
+
/** The only origin this request may reach, e.g. `https://api.pipedrive.com`. */
|
|
50
|
+
origin: string | URL;
|
|
51
|
+
/**
|
|
52
|
+
* What a redirect off that origin does. `'refuse'` (the default) fails the
|
|
53
|
+
* call. `'strip'` follows it the way an unbound request would — every header
|
|
54
|
+
* not known to be harmless dropped, a body refused — for a download that is
|
|
55
|
+
* redirected to a CDN or a signed storage link. A secret in the query or the
|
|
56
|
+
* path is not protected on such a hop.
|
|
57
|
+
*/
|
|
58
|
+
crossOriginRedirect?: 'refuse' | 'strip';
|
|
59
|
+
}
|
|
60
|
+
export interface SafeFetchOptions extends RequestInit {
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
/** Pass `ctx.signal` so the workflow engine can cancel the request. */
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
retry?: SafeFetchRetry;
|
|
65
|
+
/** Bind the request to the origin of the credential it carries. */
|
|
66
|
+
credential?: SafeFetchCredential;
|
|
67
|
+
}
|
|
68
|
+
export declare function safeFetch(url: string | URL, options?: SafeFetchOptions): Promise<Response>;
|
|
69
|
+
/**
|
|
70
|
+
* Read a response body into an ArrayBuffer while enforcing a hard byte cap.
|
|
71
|
+
*
|
|
72
|
+
* The `Content-Length` header is used as a fast-reject (bail before downloading
|
|
73
|
+
* anything), but the limit is *also* enforced while streaming, since the header
|
|
74
|
+
* can be absent or lie. On overrun the stream is cancelled and a
|
|
75
|
+
* `NodeError('RESPONSE_TOO_LARGE', …, { status })` is thrown. Both refusal paths
|
|
76
|
+
* cancel the body first, so neither leaves the connection held.
|
|
77
|
+
*/
|
|
78
|
+
export declare function readArrayBuffer(res: Response, maxBytes?: number): Promise<ArrayBuffer>;
|
|
79
|
+
/** Read a response body as UTF-8 text, capped at `maxBytes` (see {@link readArrayBuffer}). */
|
|
80
|
+
export declare function readText(res: Response, maxBytes?: number): Promise<string>;
|
|
81
|
+
/**
|
|
82
|
+
* Read a response body as JSON when the `Content-Type` denotes JSON (see
|
|
83
|
+
* {@link isJsonContentType}), otherwise as text — the content-type sniff
|
|
84
|
+
* previously duplicated across the HTTP/Upload/DeepL node sinks. Capped at
|
|
85
|
+
* `maxBytes` (see {@link readArrayBuffer}).
|
|
86
|
+
*
|
|
87
|
+
* A malformed JSON body surfaces as `NodeError('RESPONSE_PARSE_ERROR', …, { status })`
|
|
88
|
+
* rather than a raw `SyntaxError`, keeping to the SDK error contract.
|
|
89
|
+
*/
|
|
90
|
+
export declare function readJsonOrText(res: Response, maxBytes?: number): Promise<unknown>;
|
|
91
|
+
/**
|
|
92
|
+
* The setting a node offers its workflow author for the response size cap,
|
|
93
|
+
* ready to spread into `description.config`.
|
|
94
|
+
*
|
|
95
|
+
* **What the node still has to do** (PO-497): declaring the setting does not
|
|
96
|
+
* wire it up. Read the configured value in `execute` and pass it on — the
|
|
97
|
+
* helpers here only produce the declaration, and a node that spreads this and
|
|
98
|
+
* never reads `config.maxBytes` offers a setting that does nothing.
|
|
99
|
+
*
|
|
100
|
+
* Everything an author reads is supplied in both languages the node packages
|
|
101
|
+
* write, so spreading this unchanged already satisfies the rule they hold.
|
|
102
|
+
* Nothing is left to the caller on that side: a node that wants different
|
|
103
|
+
* words replaces the field rather than completing it.
|
|
104
|
+
*/
|
|
105
|
+
export declare function maxBytesConfigField(opts?: {
|
|
106
|
+
default?: number;
|
|
107
|
+
max?: number;
|
|
108
|
+
}): IConfigField;
|
|
109
|
+
/**
|
|
110
|
+
* The setting a node offers its workflow author for the request budget, ready
|
|
111
|
+
* to spread into `description.config`.
|
|
112
|
+
*
|
|
113
|
+
* **What the node still has to do** (PO-497): pass the configured value to
|
|
114
|
+
* `safeFetch` as `timeoutMs`. See {@link maxBytesConfigField} for the same note
|
|
115
|
+
* and for why the label and description are not the caller's to complete.
|
|
116
|
+
*/
|
|
117
|
+
export declare function timeoutConfigField(opts?: {
|
|
118
|
+
default?: number;
|
|
119
|
+
max?: number;
|
|
120
|
+
}): IConfigField;
|
|
121
|
+
/**
|
|
122
|
+
* The two settings a node offers its workflow author for the retry budget,
|
|
123
|
+
* ready to spread into `description.config`.
|
|
124
|
+
*
|
|
125
|
+
* **What the node still has to do** (PO-497): pass both configured values to
|
|
126
|
+
* `safeFetch` as `retry.attempts` and `retry.delayMs`. See
|
|
127
|
+
* {@link maxBytesConfigField} for the same note and for why the labels and
|
|
128
|
+
* descriptions are not the caller's to complete.
|
|
129
|
+
*/
|
|
130
|
+
export declare function retryConfigFields(opts?: {
|
|
131
|
+
defaultAttempts?: number;
|
|
132
|
+
defaultDelayMs?: number;
|
|
133
|
+
}): IConfigField[];
|
package/dist/images.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IImage } from './types.js';
|
|
2
|
+
/** Something that may carry an `images` array (a node/credential/template manifest). */
|
|
3
|
+
interface WithImages {
|
|
4
|
+
images?: IImage[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Collect the unique `src` of every image declared across all nodes,
|
|
8
|
+
* credential types, and templates in a manifest.
|
|
9
|
+
*/
|
|
10
|
+
export declare function collectImageSources(manifest: {
|
|
11
|
+
nodes?: WithImages[];
|
|
12
|
+
credentials?: WithImages[];
|
|
13
|
+
templates?: WithImages[];
|
|
14
|
+
}): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Copy declared image files into `dist/`, preserving their sub-path
|
|
17
|
+
* (`images/x.png` → `dist/images/x.png`) so `npm pack` picks them up via the
|
|
18
|
+
* package's `files: ["dist"]`. Sources are resolved relative to `rootDir`
|
|
19
|
+
* (the package root, `process.cwd()`).
|
|
20
|
+
*
|
|
21
|
+
* Every problematic declaration warns and is skipped rather than throwing, so a
|
|
22
|
+
* stale manifest never breaks the build: this covers a missing file, a `src`
|
|
23
|
+
* that is a directory rather than a file, and — defensively — an unsafe `src`
|
|
24
|
+
* that is absolute or escapes the package root via `..` traversal (which would
|
|
25
|
+
* otherwise read/write outside `dist/`).
|
|
26
|
+
*/
|
|
27
|
+
export declare function copyImages(sources: string[], outDir: string, rootDir: string): void;
|
|
28
|
+
export {};
|
package/dist/index.cjs
CHANGED
|
@@ -542,6 +542,26 @@ function clampResponseBytes(maxBytes) {
|
|
|
542
542
|
if (!Number.isFinite(maxBytes) || maxBytes < 1) return MAX_RESPONSE_BYTES;
|
|
543
543
|
return Math.min(maxBytes, MAX_RESPONSE_BYTES);
|
|
544
544
|
}
|
|
545
|
+
function boundOrigin(credential) {
|
|
546
|
+
let parsed;
|
|
547
|
+
try {
|
|
548
|
+
parsed = new URL(credential.origin);
|
|
549
|
+
} catch {
|
|
550
|
+
parsed = void 0;
|
|
551
|
+
}
|
|
552
|
+
if (!parsed || parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
553
|
+
throw new NodeError("CREDENTIAL_ORIGIN", "The origin a credential is bound to is not an http(s) address", { status: 0 });
|
|
554
|
+
}
|
|
555
|
+
return parsed.origin;
|
|
556
|
+
}
|
|
557
|
+
function credentialOriginRefusal(target, bound, hop) {
|
|
558
|
+
const what = hop === void 0 ? "a request" : `a ${hop} redirect`;
|
|
559
|
+
return new NodeError(
|
|
560
|
+
"CREDENTIAL_ORIGIN",
|
|
561
|
+
`Refused ${what} to ${target}: the credential it carries is bound to ${bound}`,
|
|
562
|
+
{ status: 0 }
|
|
563
|
+
);
|
|
564
|
+
}
|
|
545
565
|
async function timedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
|
|
546
566
|
const ac = new AbortController();
|
|
547
567
|
const timer = setTimeout(() => ac.abort(), effectiveMs);
|
|
@@ -581,7 +601,16 @@ function hasContent(body) {
|
|
|
581
601
|
if (body instanceof URLSearchParams) return body.toString().length > 0;
|
|
582
602
|
return true;
|
|
583
603
|
}
|
|
584
|
-
|
|
604
|
+
function refuseUserinfo(url, status = 0) {
|
|
605
|
+
if (url.username !== "" || url.password !== "") {
|
|
606
|
+
throw new NodeError(
|
|
607
|
+
"INVALID_URL",
|
|
608
|
+
status === 0 ? "Refused a URL with a user name or password in it: pass them in a credential, not in the address" : `Refused a ${status} redirect to a URL with a user name or password in it`,
|
|
609
|
+
{ status: 0 }
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding) {
|
|
585
614
|
let currentUrl = url;
|
|
586
615
|
let currentInit = { ...fetchOptions, redirect: "manual" };
|
|
587
616
|
await guardUrl(currentUrl, ctxSignal, effectiveMs);
|
|
@@ -610,6 +639,10 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
|
|
|
610
639
|
} catch {
|
|
611
640
|
throw new NodeError("BLOCKED_ADDRESS", "Blocked redirect with an invalid Location header", { status: 0 });
|
|
612
641
|
}
|
|
642
|
+
refuseUserinfo(nextUrl, redirectStatus);
|
|
643
|
+
if (binding && binding.crossOriginRedirect === "refuse" && nextUrl.origin !== binding.origin) {
|
|
644
|
+
throw credentialOriginRefusal(nextUrl.origin, binding.origin, redirectStatus);
|
|
645
|
+
}
|
|
613
646
|
if (base.protocol === "https:" && nextUrl.protocol !== "https:") {
|
|
614
647
|
throw new NodeError("BLOCKED_ADDRESS", "Blocked https\u2192http downgrade on redirect", { status: 0 });
|
|
615
648
|
}
|
|
@@ -630,7 +663,9 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
|
|
|
630
663
|
);
|
|
631
664
|
}
|
|
632
665
|
await guardUrl(nextUrl, ctxSignal, effectiveMs);
|
|
633
|
-
if (nextUrl.origin !==
|
|
666
|
+
if (binding && nextUrl.origin !== binding.origin) {
|
|
667
|
+
for (const name of [...headers.keys()]) headers.delete(name);
|
|
668
|
+
} else if (nextUrl.origin !== base.origin) {
|
|
634
669
|
for (const name of [...headers.keys()]) {
|
|
635
670
|
if (!headerCrossesOrigin(name)) headers.delete(name);
|
|
636
671
|
}
|
|
@@ -640,7 +675,7 @@ async function guardedFetch(url, fetchOptions, ctxSignal, effectiveMs) {
|
|
|
640
675
|
}
|
|
641
676
|
}
|
|
642
677
|
async function safeFetch(url, options = {}) {
|
|
643
|
-
const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, ...fetchOptions } = options;
|
|
678
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, credential, ...fetchOptions } = options;
|
|
644
679
|
const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
645
680
|
const rawAttempts = retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS;
|
|
646
681
|
const safeAttempts = Number.isFinite(rawAttempts) ? Math.min(Math.max(0, rawAttempts), MAX_RETRY_ATTEMPTS) : DEFAULT_RETRY_ATTEMPTS;
|
|
@@ -648,14 +683,26 @@ async function safeFetch(url, options = {}) {
|
|
|
648
683
|
const retryDelayMs = retry?.delayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
649
684
|
if (ctxSignal?.aborted) throw ctxSignal.reason;
|
|
650
685
|
if (fetchOptions.headers !== void 0) fetchOptions.headers = sendableHeaders(fetchOptions.headers);
|
|
686
|
+
let parsed;
|
|
687
|
+
try {
|
|
688
|
+
parsed = url instanceof URL ? url : new URL(url);
|
|
689
|
+
} catch {
|
|
690
|
+
throw new NodeError("INVALID_URL", "Refused an address that cannot be read as a URL", { status: 0 });
|
|
691
|
+
}
|
|
692
|
+
refuseUserinfo(parsed);
|
|
693
|
+
let binding;
|
|
694
|
+
if (credential) {
|
|
695
|
+
binding = { origin: boundOrigin(credential), crossOriginRedirect: credential.crossOriginRedirect ?? "refuse" };
|
|
696
|
+
if (parsed.origin !== binding.origin) throw credentialOriginRefusal(parsed.origin, binding.origin);
|
|
697
|
+
}
|
|
651
698
|
let lastError;
|
|
652
699
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
653
700
|
if (ctxSignal?.aborted) throw ctxSignal.reason;
|
|
654
701
|
try {
|
|
655
|
-
return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs);
|
|
702
|
+
return await guardedFetch(url, fetchOptions, ctxSignal, effectiveMs, binding);
|
|
656
703
|
} catch (err) {
|
|
657
704
|
if (ctxSignal?.aborted) throw ctxSignal.reason;
|
|
658
|
-
if (err instanceof NodeError && (err.code === "BLOCKED_ADDRESS" || err.code === "TOO_MANY_REDIRECTS")) {
|
|
705
|
+
if (err instanceof NodeError && (err.code === "BLOCKED_ADDRESS" || err.code === "TOO_MANY_REDIRECTS" || err.code === "CREDENTIAL_ORIGIN" || err.code === "INVALID_URL")) {
|
|
659
706
|
throw err;
|
|
660
707
|
}
|
|
661
708
|
lastError = err;
|
|
@@ -971,15 +1018,23 @@ var BaseCredential = class {
|
|
|
971
1018
|
});
|
|
972
1019
|
const text = await readText(res, MAX_TOKEN_RESPONSE_BYTES);
|
|
973
1020
|
if (!res.ok) {
|
|
974
|
-
throw new Error(`token endpoint ${url} -> ${res.status}${formatOAuthError(text)}`);
|
|
1021
|
+
throw new Error(`token endpoint ${endpointForMessage(url)} -> ${res.status}${formatOAuthError(text)}`);
|
|
975
1022
|
}
|
|
976
1023
|
try {
|
|
977
1024
|
return JSON.parse(text);
|
|
978
1025
|
} catch {
|
|
979
|
-
throw new Error(`token endpoint ${url} returned non-JSON body`);
|
|
1026
|
+
throw new Error(`token endpoint ${endpointForMessage(url)} returned non-JSON body`);
|
|
980
1027
|
}
|
|
981
1028
|
}
|
|
982
1029
|
};
|
|
1030
|
+
function endpointForMessage(url) {
|
|
1031
|
+
try {
|
|
1032
|
+
const u = new URL(url);
|
|
1033
|
+
return `${u.origin}${u.pathname}`;
|
|
1034
|
+
} catch {
|
|
1035
|
+
return "(unreadable address)";
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
983
1038
|
function formatOAuthError(body) {
|
|
984
1039
|
try {
|
|
985
1040
|
const parsed = JSON.parse(body);
|