@hostwebhook/node-sdk 0.1.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.
Files changed (59) hide show
  1. package/dist/code-runner.d.ts +20 -0
  2. package/dist/code-runner.js +138 -0
  3. package/dist/contratos.d.ts +121 -0
  4. package/dist/contratos.js +24 -0
  5. package/dist/dto/output-node.dto.d.ts +19 -0
  6. package/dist/dto/output-node.dto.js +96 -0
  7. package/dist/ensure-meta.d.ts +22 -0
  8. package/dist/ensure-meta.js +35 -0
  9. package/dist/execute-with-iteration.d.ts +18 -0
  10. package/dist/execute-with-iteration.js +66 -0
  11. package/dist/filter-utils.d.ts +22 -0
  12. package/dist/filter-utils.js +178 -0
  13. package/dist/handler-helpers.d.ts +21 -0
  14. package/dist/handler-helpers.js +53 -0
  15. package/dist/index.d.ts +51 -0
  16. package/dist/index.js +73 -0
  17. package/dist/log-metadata.d.ts +191 -0
  18. package/dist/log-metadata.js +375 -0
  19. package/dist/node-dispatch.registry.d.ts +32 -0
  20. package/dist/node-dispatch.registry.js +45 -0
  21. package/dist/node-executors.d.ts +299 -0
  22. package/dist/node-executors.js +555 -0
  23. package/dist/node-lifecycle.d.ts +399 -0
  24. package/dist/node-lifecycle.js +782 -0
  25. package/dist/normalize-nodes.d.ts +18 -0
  26. package/dist/normalize-nodes.js +22 -0
  27. package/dist/output-node-ref.schema.d.ts +82 -0
  28. package/dist/output-node-ref.schema.js +90 -0
  29. package/dist/output-webhook-scope.d.ts +36 -0
  30. package/dist/output-webhook-scope.js +42 -0
  31. package/dist/payload-preview.d.ts +10 -0
  32. package/dist/payload-preview.js +39 -0
  33. package/dist/pipeline.constants.d.ts +29 -0
  34. package/dist/pipeline.constants.js +51 -0
  35. package/dist/pre-request-pool.d.ts +58 -0
  36. package/dist/pre-request-pool.js +308 -0
  37. package/dist/pre-request-runner-source.d.ts +28 -0
  38. package/dist/pre-request-runner-source.js +411 -0
  39. package/dist/regex-de-inquilino.d.ts +15 -0
  40. package/dist/regex-de-inquilino.js +98 -0
  41. package/dist/request-context.d.ts +18 -0
  42. package/dist/request-context.js +34 -0
  43. package/dist/retry-transient.d.ts +54 -0
  44. package/dist/retry-transient.js +67 -0
  45. package/dist/retry-utils.d.ts +17 -0
  46. package/dist/retry-utils.js +23 -0
  47. package/dist/schema-validator-utils.d.ts +9 -0
  48. package/dist/schema-validator-utils.js +140 -0
  49. package/dist/ssrf-guard.d.ts +202 -0
  50. package/dist/ssrf-guard.js +917 -0
  51. package/dist/swallow.d.ts +52 -0
  52. package/dist/swallow.js +55 -0
  53. package/dist/template-render.d.ts +33 -0
  54. package/dist/template-render.js +43 -0
  55. package/dist/try-parse.d.ts +41 -0
  56. package/dist/try-parse.js +69 -0
  57. package/dist/workspace-payloads.d.ts +66 -0
  58. package/dist/workspace-payloads.js +496 -0
  59. package/package.json +35 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Catch handlers for work whose failure must not abort the caller.
3
+ *
4
+ * `.catch(() => {})` says "this may fail and that is fine". It is almost
5
+ * never true. What it actually says is "this may fail and nobody will ever
6
+ * know" — and this codebase has paid for that repeatedly: a TTL index that
7
+ * was silently discarded and expired nothing for four months, an S3 adapter
8
+ * built without a bucket whose every delete failed while uploads kept
9
+ * working, a merge that waited forever because no timeout job was enqueued.
10
+ * Each one was found by accident, long after it started costing something.
11
+ *
12
+ * There is a real category of work that genuinely should not abort a
13
+ * request: cache writes, `lastUsed` touches, best-effort cleanup. The
14
+ * problem was never that they are swallowed, it is that they are swallowed
15
+ * *anonymously*. These helpers keep the non-blocking behaviour and make the
16
+ * failure land in the log with a name attached.
17
+ *
18
+ * redis.set(k, v).catch(onFailure(this.logger, `cache write ${k}`));
19
+ * const limits = await plans.getOrgPlanLimits(orgId)
20
+ * .catch(onFailureReturn(this.logger, `plan limits for org ${orgId}`, null));
21
+ *
22
+ * NOT for use where the failure changes the answer. If a lookup failing
23
+ * means the caller returns 404 instead of 500, the fix is to let it throw,
24
+ * not to log it and carry on.
25
+ */
26
+ import type { Logger } from '@nestjs/common';
27
+ /**
28
+ * Log and continue, yielding `undefined`.
29
+ *
30
+ * `what` should name the resource, not the operation — "cache write for
31
+ * workspace 69ab…" beats "redis failed", because the first one can be
32
+ * searched for and the second cannot.
33
+ */
34
+ export declare function onFailure(logger: Logger, what: string): (err: unknown) => undefined;
35
+ /**
36
+ * Log and continue with a fallback value.
37
+ *
38
+ * Use when the fallback is a genuine default rather than a disguise for the
39
+ * error — plan limits falling back to `null` is fine as long as the log says
40
+ * the tier could not be read, because otherwise a user silently gets the
41
+ * wrong ceiling and nothing anywhere says why.
42
+ */
43
+ export declare function onFailureReturn<T>(logger: Logger, what: string, fallback: T): (err: unknown) => T;
44
+ /**
45
+ * Report the rejections inside an already-settled batch.
46
+ *
47
+ * `Promise.allSettled` never rejects, so `await Promise.allSettled(x).catch(…)`
48
+ * is dead code — three of those existed here, each looking like error
49
+ * handling while every individual rejection went unread. This awaits the
50
+ * batch and logs whichever entries failed.
51
+ */
52
+ export declare function settleAll(promises: Promise<unknown>[], logger: Logger, what: string): Promise<void>;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.onFailure = onFailure;
4
+ exports.onFailureReturn = onFailureReturn;
5
+ exports.settleAll = settleAll;
6
+ function describe(err) {
7
+ if (err instanceof Error)
8
+ return err.stack ?? `${err.name}: ${err.message}`;
9
+ return String(err);
10
+ }
11
+ /**
12
+ * Log and continue, yielding `undefined`.
13
+ *
14
+ * `what` should name the resource, not the operation — "cache write for
15
+ * workspace 69ab…" beats "redis failed", because the first one can be
16
+ * searched for and the second cannot.
17
+ */
18
+ function onFailure(logger, what) {
19
+ return (err) => {
20
+ logger.warn(`${what} failed (continuing): ${describe(err)}`);
21
+ return undefined;
22
+ };
23
+ }
24
+ /**
25
+ * Log and continue with a fallback value.
26
+ *
27
+ * Use when the fallback is a genuine default rather than a disguise for the
28
+ * error — plan limits falling back to `null` is fine as long as the log says
29
+ * the tier could not be read, because otherwise a user silently gets the
30
+ * wrong ceiling and nothing anywhere says why.
31
+ */
32
+ function onFailureReturn(logger, what, fallback) {
33
+ return (err) => {
34
+ logger.warn(`${what} failed (using fallback): ${describe(err)}`);
35
+ return fallback;
36
+ };
37
+ }
38
+ /**
39
+ * Report the rejections inside an already-settled batch.
40
+ *
41
+ * `Promise.allSettled` never rejects, so `await Promise.allSettled(x).catch(…)`
42
+ * is dead code — three of those existed here, each looking like error
43
+ * handling while every individual rejection went unread. This awaits the
44
+ * batch and logs whichever entries failed.
45
+ */
46
+ async function settleAll(promises, logger, what) {
47
+ if (promises.length === 0)
48
+ return;
49
+ const results = await Promise.allSettled(promises);
50
+ const failed = results.filter((r) => r.status === 'rejected');
51
+ if (failed.length > 0) {
52
+ logger.warn(`${what}: ${failed.length}/${results.length} failed — ` +
53
+ failed.map((f) => describe(f.reason)).join(' | '));
54
+ }
55
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Centralized template render factory.
3
+ *
4
+ * Creates a render function bound to a specific execution context
5
+ * (payload, headers, meta, workspacePayloads). Ensures $() cross-node
6
+ * references always resolve when workspacePayloads is available.
7
+ *
8
+ * Usage:
9
+ * const { render } = createRender({ payload, workspacePayloads, eventMeta });
10
+ * const url = render(entity.url);
11
+ * const body = render(entity.body);
12
+ *
13
+ * // Async version with _file resolution:
14
+ * const { renderAsync } = createRender({ payload, fileResolver });
15
+ * const body = await renderAsync(entity.body);
16
+ */
17
+ import { type TemplateContext, type TemplateOptions, type FileResolver } from '@hostwebhook/template-engine';
18
+ export interface RenderContext {
19
+ payload: Record<string, unknown>;
20
+ headers?: Record<string, string>;
21
+ meta?: Record<string, string>;
22
+ eventMeta?: Record<string, string>;
23
+ workspacePayloads?: Record<string, Record<string, unknown>>;
24
+ /** Async file resolver — generates presigned download URLs for _file references */
25
+ fileResolver?: FileResolver;
26
+ }
27
+ export declare function createRender(rCtx: RenderContext, extraOpts?: Partial<TemplateOptions>): {
28
+ ctx: TemplateContext;
29
+ /** Sync render — does NOT resolve _file downloadUrl (use renderAsync for that) */
30
+ render: (t: string) => string;
31
+ /** Async render — pre-resolves _file references with presigned download URLs */
32
+ renderAsync: (t: string) => Promise<string>;
33
+ };
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Centralized template render factory.
4
+ *
5
+ * Creates a render function bound to a specific execution context
6
+ * (payload, headers, meta, workspacePayloads). Ensures $() cross-node
7
+ * references always resolve when workspacePayloads is available.
8
+ *
9
+ * Usage:
10
+ * const { render } = createRender({ payload, workspacePayloads, eventMeta });
11
+ * const url = render(entity.url);
12
+ * const body = render(entity.body);
13
+ *
14
+ * // Async version with _file resolution:
15
+ * const { renderAsync } = createRender({ payload, fileResolver });
16
+ * const body = await renderAsync(entity.body);
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.createRender = createRender;
20
+ const template_engine_1 = require("@hostwebhook/template-engine");
21
+ function createRender(rCtx, extraOpts) {
22
+ const ctx = {
23
+ payload: rCtx.payload,
24
+ headers: rCtx.headers ?? {},
25
+ meta: rCtx.eventMeta ?? rCtx.meta ?? {},
26
+ };
27
+ const opts = extraOpts || rCtx.workspacePayloads || rCtx.fileResolver
28
+ ? {
29
+ ...extraOpts,
30
+ ...(rCtx.workspacePayloads
31
+ ? { workspacePayloads: rCtx.workspacePayloads }
32
+ : {}),
33
+ ...(rCtx.fileResolver ? { fileResolver: rCtx.fileResolver } : {}),
34
+ }
35
+ : undefined;
36
+ return {
37
+ ctx,
38
+ /** Sync render — does NOT resolve _file downloadUrl (use renderAsync for that) */
39
+ render: (t) => (0, template_engine_1.renderTemplate)(t, ctx, opts).output,
40
+ /** Async render — pre-resolves _file references with presigned download URLs */
41
+ renderAsync: async (t) => (await (0, template_engine_1.renderTemplateAsync)(t, ctx, opts)).output,
42
+ };
43
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Parsers used as tests rather than as conversions.
3
+ *
4
+ * `try { JSON.parse(x) } catch {}` is not an error being swallowed — the throw
5
+ * IS the answer, and the caller wants "is this JSON?" rather than a failure.
6
+ * That makes it the one shape `common/swallow.ts` deliberately does not cover:
7
+ * logging it would emit a line per non-JSON payload, which is noise.
8
+ *
9
+ * But it was written out longhand 39 times across the codebase, each with its
10
+ * own comment explaining the same thing, and each leaving an empty `catch` for
11
+ * a reviewer to classify from scratch. The intent belongs in a name.
12
+ *
13
+ * const parsed = tryParseJson<Shape>(result.responseBody);
14
+ * if (parsed?._operationOutput) return parsed.output;
15
+ *
16
+ * The distinction that matters, and the reason this is a separate file from
17
+ * swallow.ts: use these ONLY where a parse failure is an expected, ordinary
18
+ * branch. If a payload that fails to parse means something is actually wrong —
19
+ * a webhook that should always be JSON, a config file, an API contract — let
20
+ * it throw, or catch it and say so. Reaching for `tryParseJson` to make an
21
+ * error quiet is the same mistake as `.catch(() => {})`, wearing a better name.
22
+ */
23
+ /**
24
+ * Parse JSON, or `null` when the input is not JSON.
25
+ *
26
+ * Returns `null` for `null`/`undefined`/empty input too, so callers do not
27
+ * need a separate emptiness check before asking.
28
+ */
29
+ export declare function tryParseJson<T = unknown>(raw: unknown): T | null;
30
+ /**
31
+ * Parse JSON expecting an object, or `null`.
32
+ *
33
+ * `JSON.parse('4')` and `JSON.parse('"x"')` both succeed, so a bare
34
+ * `tryParseJson` followed by `parsed.someField` is a silent `undefined` on
35
+ * perfectly valid JSON that simply is not a record. Most call sites here want
36
+ * a record; this makes that explicit instead of relying on the field lookup
37
+ * to fail quietly.
38
+ */
39
+ export declare function tryParseJsonObject<T extends object = Record<string, unknown>>(raw: unknown): T | null;
40
+ /** Parse a URL, or `null` when the string is not one. */
41
+ export declare function tryParseUrl(raw: unknown): URL | null;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * Parsers used as tests rather than as conversions.
4
+ *
5
+ * `try { JSON.parse(x) } catch {}` is not an error being swallowed — the throw
6
+ * IS the answer, and the caller wants "is this JSON?" rather than a failure.
7
+ * That makes it the one shape `common/swallow.ts` deliberately does not cover:
8
+ * logging it would emit a line per non-JSON payload, which is noise.
9
+ *
10
+ * But it was written out longhand 39 times across the codebase, each with its
11
+ * own comment explaining the same thing, and each leaving an empty `catch` for
12
+ * a reviewer to classify from scratch. The intent belongs in a name.
13
+ *
14
+ * const parsed = tryParseJson<Shape>(result.responseBody);
15
+ * if (parsed?._operationOutput) return parsed.output;
16
+ *
17
+ * The distinction that matters, and the reason this is a separate file from
18
+ * swallow.ts: use these ONLY where a parse failure is an expected, ordinary
19
+ * branch. If a payload that fails to parse means something is actually wrong —
20
+ * a webhook that should always be JSON, a config file, an API contract — let
21
+ * it throw, or catch it and say so. Reaching for `tryParseJson` to make an
22
+ * error quiet is the same mistake as `.catch(() => {})`, wearing a better name.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.tryParseJson = tryParseJson;
26
+ exports.tryParseJsonObject = tryParseJsonObject;
27
+ exports.tryParseUrl = tryParseUrl;
28
+ /**
29
+ * Parse JSON, or `null` when the input is not JSON.
30
+ *
31
+ * Returns `null` for `null`/`undefined`/empty input too, so callers do not
32
+ * need a separate emptiness check before asking.
33
+ */
34
+ function tryParseJson(raw) {
35
+ if (typeof raw !== 'string' || raw.length === 0)
36
+ return null;
37
+ try {
38
+ return JSON.parse(raw);
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ /**
45
+ * Parse JSON expecting an object, or `null`.
46
+ *
47
+ * `JSON.parse('4')` and `JSON.parse('"x"')` both succeed, so a bare
48
+ * `tryParseJson` followed by `parsed.someField` is a silent `undefined` on
49
+ * perfectly valid JSON that simply is not a record. Most call sites here want
50
+ * a record; this makes that explicit instead of relying on the field lookup
51
+ * to fail quietly.
52
+ */
53
+ function tryParseJsonObject(raw) {
54
+ const parsed = tryParseJson(raw);
55
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
56
+ return null;
57
+ return parsed;
58
+ }
59
+ /** Parse a URL, or `null` when the string is not one. */
60
+ function tryParseUrl(raw) {
61
+ if (typeof raw !== 'string' || raw.length === 0)
62
+ return null;
63
+ try {
64
+ return new URL(raw);
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Load lastPayload for referenced nodes in a workspace.
3
+ *
4
+ * Used by $("node name") in templates — provides synchronous
5
+ * lookup of any node's output within the same workspace.
6
+ *
7
+ * Optimized with Redis caching:
8
+ * - Extract $() refs from entity → only load referenced nodes
9
+ * - Redis MGET first (~1ms) → MongoDB fallback only for cache misses
10
+ * - cacheNodePayload warms the cache → subsequent lookups are instant
11
+ */
12
+ import type { Db } from 'mongodb';
13
+ import type { Connection } from 'mongoose';
14
+ /** Get the shared Redis client (or null if not initialized) */
15
+ export declare function getPayloadRedis(): any;
16
+ /** Get or create the shared Redis client for payload caching */
17
+ export declare function initPayloadRedis(redisUrl?: string): void;
18
+ /** Extract node names referenced by $("name") or $('name') in the entity */
19
+ export declare function extractCrossNodeRefs(entity: unknown): Set<string>;
20
+ /** Write a node's payload to Redis cache (fire-and-forget, 5 min TTL) */
21
+ export declare function cacheNodePayload(wsId: string, name: string, payload: Record<string, unknown>): void;
22
+ /**
23
+ * Drop a deleted node's payload row and its Redis mirror.
24
+ *
25
+ * `saveLastPayload` creates the row and nothing ever removed it. Since the
26
+ * payload moved out of the entity and into `nodepayloads`, deleting a node
27
+ * left its output behind permanently: measured on production 2026-08-05,
28
+ * 25 of 192 rows (13%) belonged to nodes that no longer existed, spread
29
+ * across 13 node types, the oldest from 2026-07-31.
30
+ *
31
+ * The stale Redis key is the sharper edge. `$("node name")` resolves by
32
+ * NAME, not by id, so a new node reusing a deleted one's name would read the
33
+ * dead payload until the 5-minute TTL expired.
34
+ *
35
+ * Best-effort: the entity delete has already happened and a leftover row is
36
+ * inert, so failures are logged rather than thrown back at the caller.
37
+ */
38
+ export declare function deleteNodePayload(connection: Connection, nodeId: string): Promise<void>;
39
+ /** Invalidate canvas-data Redis cache for an org+workspace (fire-and-forget) */
40
+ export declare function invalidateCanvasCache(orgId: string, workspaceId?: string): void;
41
+ /**
42
+ * Propagate a node rename to the `nodepayloads` collection and the
43
+ * Redis hot cache. Without this, cross-node `$("name")` lookups break
44
+ * silently after a rename: the template engine resolves $() by NAME
45
+ * (the data-picker emits names, not ObjectIds), but the persisted
46
+ * nodepayloads row keeps the old auto-generated name until the next
47
+ * dispatch overwrites it. The user-visible bug is "node fired, payload
48
+ * shows in the detail page, but downstream `$()` references render
49
+ * empty" — which produces hours of debugging chasing ghost workspace
50
+ * mismatches.
51
+ *
52
+ * Also wipes the old Redis cache key so a stale read doesn't shadow
53
+ * the new name, and re-warms under the new name when the row has a
54
+ * payload (so the very next $() lookup hits Redis, not Mongo).
55
+ *
56
+ * Fire-and-forget on individual failures — caller already returned
57
+ * the updated entity. Best-effort: the nodepayloads row is overwritten
58
+ * on the next dispatch anyway, so a one-off failure is recoverable.
59
+ */
60
+ export declare function propagateNodeRename(connection: Connection, nodeId: string, previousName: string | undefined, newName: string, workspaceId: string | undefined): Promise<void>;
61
+ /**
62
+ * Load workspace payloads for the given node names.
63
+ * Redis first (MGET, ~1ms), MongoDB fallback only for cache misses.
64
+ * If nodeNames is empty or undefined, returns {} instantly.
65
+ */
66
+ export declare function loadWorkspacePayloads(db: Db, workspaceId: string, orgId: string, nodeNames?: Set<string>): Promise<Record<string, Record<string, unknown>>>;