@hostwebhook/node-sdk 0.3.0 → 0.5.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/acceso-al-workspace.d.ts +51 -0
- package/dist/acceso-al-workspace.js +99 -0
- package/dist/base-node.service.d.ts +120 -0
- package/dist/base-node.service.js +409 -0
- package/dist/cascade-cleanup.d.ts +13 -0
- package/dist/cascade-cleanup.js +93 -0
- package/dist/credential-references.d.ts +131 -0
- package/dist/credential-references.js +210 -0
- package/dist/dto-utils.d.ts +11 -0
- package/dist/dto-utils.js +22 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/limpiar-marcador-detach.d.ts +63 -0
- package/dist/limpiar-marcador-detach.js +92 -0
- package/dist/olvidar-la-colocacion.d.ts +23 -0
- package/dist/olvidar-la-colocacion.js +60 -0
- package/dist/sync-connections.d.ts +73 -0
- package/dist/sync-connections.js +120 -0
- package/dist/unique-name.d.ts +6 -0
- package/dist/unique-name.js +32 -0
- package/package.json +22 -14
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Connection, Types } from 'mongoose';
|
|
2
|
+
/**
|
|
3
|
+
* Corta si `userId` no está en la lista del workspace al que pertenece el
|
|
4
|
+
* recurso.
|
|
5
|
+
*
|
|
6
|
+
* Las tres salidas por arriba son deliberadas y copian, una a una, las que ya
|
|
7
|
+
* tomaba el guardia — el objetivo es aplicar la MISMA regla en más sitios, no
|
|
8
|
+
* una regla nueva:
|
|
9
|
+
*
|
|
10
|
+
* - sin workspace, no hay lista que aplicar (un nodo suelto);
|
|
11
|
+
* - sin `userId` no hay contra qué comparar. Ojo con lo que ESO no es: una
|
|
12
|
+
* API key SÍ trae identidad. Las dos rutas que la validan
|
|
13
|
+
* —`api-key.strategy.ts` y `api-key-validator.service.ts`— cargan el
|
|
14
|
+
* usuario dueño de la llave con `userModel.findById(apiKey.userId)`, así
|
|
15
|
+
* que `req.user._id` viene puesto y la lista se aplica igual que en una
|
|
16
|
+
* sesión. Esta salida cubre a un llamante interno sin usuario resuelto,
|
|
17
|
+
* no a las API keys;
|
|
18
|
+
* - lista vacía sigue significando «toda la organización».
|
|
19
|
+
*
|
|
20
|
+
* Y una que el guardia no tenía: si el workspace ya no existe, se deja pasar.
|
|
21
|
+
* El recurso se buscó con el `organizationId` de quien llama, así que es suyo;
|
|
22
|
+
* negarle su propio nodo porque el workspace se borró debajo sería romperlo
|
|
23
|
+
* por un dato que falta, no cerrar un agujero.
|
|
24
|
+
*/
|
|
25
|
+
export declare function asegurarAccesoAlWorkspace(connection: Connection, workspaceId: string | Types.ObjectId | null | undefined, userId: string | undefined): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Corta si el workspace no es de esta organización.
|
|
28
|
+
*
|
|
29
|
+
* Al crear un nodo, `workspaceId` llega del cuerpo y se escribía tal cual. El
|
|
30
|
+
* guardia tampoco lo frenaba: un id de OTRA organización no aparece en su
|
|
31
|
+
* consulta —que filtra por organización— y su regla para «no encontrado» es
|
|
32
|
+
* dejar contestar al manejador, para que un id equivocado siga dando 404 en
|
|
33
|
+
* vez de delatar su existencia con un 403. Sólo que el manejador tampoco
|
|
34
|
+
* miraba, así que el nodo acababa guardado apuntando al workspace de otro.
|
|
35
|
+
*
|
|
36
|
+
* Aquí «no encontrado» sí puede negar sin delatar nada: quien crea el nodo
|
|
37
|
+
* escribió ese id, y de un id que no es de su organización no aprende nada
|
|
38
|
+
* nuevo al recibir un 403.
|
|
39
|
+
*/
|
|
40
|
+
export declare function asegurarWorkspaceDeLaOrganizacion(connection: Connection, workspaceId: string | Types.ObjectId | null | undefined, orgId: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Los workspaces de la organización que este usuario puede ver.
|
|
43
|
+
*
|
|
44
|
+
* Devuelve `null` cuando NO hay nada que recortar —ningún workspace de la
|
|
45
|
+
* organización tiene lista, o no hay usuario resuelto—, y ese `null` importa: significa «no filtres», así que la consulta
|
|
46
|
+
* y su clave de caché salen exactamente como salían. Sin eso, una lectura
|
|
47
|
+
* filtrada por usuario compartiría entrada de caché con una sin filtrar y
|
|
48
|
+
* acabaría sirviéndole a alguien la vista de otro, que es un agujero peor que
|
|
49
|
+
* el que se está cerrando.
|
|
50
|
+
*/
|
|
51
|
+
export declare function workspacesVisibles(connection: Connection, orgId: string, userId: string | undefined): Promise<Types.ObjectId[] | null>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.asegurarAccesoAlWorkspace = asegurarAccesoAlWorkspace;
|
|
4
|
+
exports.asegurarWorkspaceDeLaOrganizacion = asegurarWorkspaceDeLaOrganizacion;
|
|
5
|
+
exports.workspacesVisibles = workspacesVisibles;
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const mongoose_1 = require("mongoose");
|
|
8
|
+
async function leerWorkspace(connection, workspaceId) {
|
|
9
|
+
if (!mongoose_1.Types.ObjectId.isValid(workspaceId))
|
|
10
|
+
return null;
|
|
11
|
+
return connection
|
|
12
|
+
.collection('workspaces')
|
|
13
|
+
.findOne({ _id: new mongoose_1.Types.ObjectId(workspaceId) }, { projection: { allowedUserIds: 1, organizationId: 1, name: 1 } });
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Corta si `userId` no está en la lista del workspace al que pertenece el
|
|
17
|
+
* recurso.
|
|
18
|
+
*
|
|
19
|
+
* Las tres salidas por arriba son deliberadas y copian, una a una, las que ya
|
|
20
|
+
* tomaba el guardia — el objetivo es aplicar la MISMA regla en más sitios, no
|
|
21
|
+
* una regla nueva:
|
|
22
|
+
*
|
|
23
|
+
* - sin workspace, no hay lista que aplicar (un nodo suelto);
|
|
24
|
+
* - sin `userId` no hay contra qué comparar. Ojo con lo que ESO no es: una
|
|
25
|
+
* API key SÍ trae identidad. Las dos rutas que la validan
|
|
26
|
+
* —`api-key.strategy.ts` y `api-key-validator.service.ts`— cargan el
|
|
27
|
+
* usuario dueño de la llave con `userModel.findById(apiKey.userId)`, así
|
|
28
|
+
* que `req.user._id` viene puesto y la lista se aplica igual que en una
|
|
29
|
+
* sesión. Esta salida cubre a un llamante interno sin usuario resuelto,
|
|
30
|
+
* no a las API keys;
|
|
31
|
+
* - lista vacía sigue significando «toda la organización».
|
|
32
|
+
*
|
|
33
|
+
* Y una que el guardia no tenía: si el workspace ya no existe, se deja pasar.
|
|
34
|
+
* El recurso se buscó con el `organizationId` de quien llama, así que es suyo;
|
|
35
|
+
* negarle su propio nodo porque el workspace se borró debajo sería romperlo
|
|
36
|
+
* por un dato que falta, no cerrar un agujero.
|
|
37
|
+
*/
|
|
38
|
+
async function asegurarAccesoAlWorkspace(connection, workspaceId, userId) {
|
|
39
|
+
if (!workspaceId || !userId)
|
|
40
|
+
return;
|
|
41
|
+
const workspace = await leerWorkspace(connection, workspaceId);
|
|
42
|
+
if (!workspace)
|
|
43
|
+
return;
|
|
44
|
+
const permitidos = workspace.allowedUserIds ?? [];
|
|
45
|
+
if (permitidos.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
if (permitidos.some((id) => id.toString() === userId))
|
|
48
|
+
return;
|
|
49
|
+
throw new common_1.ForbiddenException('You do not have access to this workspace.');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Corta si el workspace no es de esta organización.
|
|
53
|
+
*
|
|
54
|
+
* Al crear un nodo, `workspaceId` llega del cuerpo y se escribía tal cual. El
|
|
55
|
+
* guardia tampoco lo frenaba: un id de OTRA organización no aparece en su
|
|
56
|
+
* consulta —que filtra por organización— y su regla para «no encontrado» es
|
|
57
|
+
* dejar contestar al manejador, para que un id equivocado siga dando 404 en
|
|
58
|
+
* vez de delatar su existencia con un 403. Sólo que el manejador tampoco
|
|
59
|
+
* miraba, así que el nodo acababa guardado apuntando al workspace de otro.
|
|
60
|
+
*
|
|
61
|
+
* Aquí «no encontrado» sí puede negar sin delatar nada: quien crea el nodo
|
|
62
|
+
* escribió ese id, y de un id que no es de su organización no aprende nada
|
|
63
|
+
* nuevo al recibir un 403.
|
|
64
|
+
*/
|
|
65
|
+
async function asegurarWorkspaceDeLaOrganizacion(connection, workspaceId, orgId) {
|
|
66
|
+
if (!workspaceId)
|
|
67
|
+
return;
|
|
68
|
+
const workspace = await leerWorkspace(connection, workspaceId);
|
|
69
|
+
if (workspace && workspace.organizationId?.toString() === orgId)
|
|
70
|
+
return;
|
|
71
|
+
throw new common_1.ForbiddenException('That workspace does not belong to your organization.');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Los workspaces de la organización que este usuario puede ver.
|
|
75
|
+
*
|
|
76
|
+
* Devuelve `null` cuando NO hay nada que recortar —ningún workspace de la
|
|
77
|
+
* organización tiene lista, o no hay usuario resuelto—, y ese `null` importa: significa «no filtres», así que la consulta
|
|
78
|
+
* y su clave de caché salen exactamente como salían. Sin eso, una lectura
|
|
79
|
+
* filtrada por usuario compartiría entrada de caché con una sin filtrar y
|
|
80
|
+
* acabaría sirviéndole a alguien la vista de otro, que es un agujero peor que
|
|
81
|
+
* el que se está cerrando.
|
|
82
|
+
*/
|
|
83
|
+
async function workspacesVisibles(connection, orgId, userId) {
|
|
84
|
+
if (!userId)
|
|
85
|
+
return null;
|
|
86
|
+
const todos = await connection
|
|
87
|
+
.collection('workspaces')
|
|
88
|
+
.find({ organizationId: new mongoose_1.Types.ObjectId(orgId) }, { projection: { allowedUserIds: 1 } })
|
|
89
|
+
.toArray();
|
|
90
|
+
const restringidos = todos.filter((w) => (w.allowedUserIds ?? []).length > 0);
|
|
91
|
+
if (restringidos.length === 0)
|
|
92
|
+
return null;
|
|
93
|
+
const fuera = restringidos.filter((w) => !(w.allowedUserIds ?? []).some((id) => id.toString() === userId));
|
|
94
|
+
if (fuera.length === 0)
|
|
95
|
+
return null;
|
|
96
|
+
return todos
|
|
97
|
+
.filter((w) => !fuera.some((f) => f._id.toString() === w._id.toString()))
|
|
98
|
+
.map((w) => w._id);
|
|
99
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Model, Connection, Types } from 'mongoose';
|
|
2
|
+
/**
|
|
3
|
+
* Base service for node entities (HTTP Actions, Transform Nodes, Filter Nodes, Sheets Actions, etc.).
|
|
4
|
+
* Provides standard CRUD, saveLastPayload, findActiveByInput, and connection validation.
|
|
5
|
+
*
|
|
6
|
+
* Subclasses must define:
|
|
7
|
+
* - `entityName`: human-readable name for error messages (e.g. 'HTTP action')
|
|
8
|
+
* - `cascadeNodeType`: type key for cascadeCleanupNodeReferences (e.g. 'httpAction')
|
|
9
|
+
*/
|
|
10
|
+
export declare abstract class BaseNodeService<TDoc extends {
|
|
11
|
+
id?: string;
|
|
12
|
+
_id?: any;
|
|
13
|
+
workspaceId?: any;
|
|
14
|
+
}> {
|
|
15
|
+
protected readonly model: Model<TDoc>;
|
|
16
|
+
protected readonly connection: Connection;
|
|
17
|
+
protected abstract readonly entityName: string;
|
|
18
|
+
protected abstract readonly cascadeNodeType: string;
|
|
19
|
+
constructor(model: Model<TDoc>, connection: Connection);
|
|
20
|
+
findAll(orgId: string, workspaceId?: string, visibles?: Types.ObjectId[] | null): Promise<TDoc[]>;
|
|
21
|
+
findOne(id: string, orgId: string): Promise<TDoc>;
|
|
22
|
+
/**
|
|
23
|
+
* Devuelve el `organizationId` del nodo, que ya se busca aquí dentro.
|
|
24
|
+
*
|
|
25
|
+
* Lo necesita quien quiera avisar por socket de que el payload cambió: sin
|
|
26
|
+
* esto tendría que repetir la misma consulta justo después. Los llamantes
|
|
27
|
+
* que no lo usen no se enteran — antes devolvía `void`.
|
|
28
|
+
*/
|
|
29
|
+
saveLastPayload(id: string, payload: any,
|
|
30
|
+
/**
|
|
31
|
+
* De dónde sale este payload.
|
|
32
|
+
*
|
|
33
|
+
* `example: true` sólo cuando el usuario promueve una PLANTILLA desde el
|
|
34
|
+
* Test push: datos inventados, con la forma exacta que el nodo va a
|
|
35
|
+
* despachar, para poder montar el flujo antes del primer evento real.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ Va FUERA del payload a propósito. Meter la marca dentro cambiaría la
|
|
38
|
+
* forma —y el sprint entero fue sobre que la plantilla sea la forma
|
|
39
|
+
* EXACTA—, así que quien copiara `{{payload.x}}` desde ahí acabaría con
|
|
40
|
+
* una ruta que no existe en la entrega de verdad.
|
|
41
|
+
*/
|
|
42
|
+
meta?: {
|
|
43
|
+
example?: boolean;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
organizationId: string;
|
|
46
|
+
} | undefined>;
|
|
47
|
+
remove(id: string, orgId: string): Promise<void>;
|
|
48
|
+
/** Override for custom cascade cleanup (e.g. router rules). Default is no-op. */
|
|
49
|
+
protected onCascadeDelete(_id: string): Promise<void>;
|
|
50
|
+
/** Sugar for the one lookup event-pipeline does often enough to name. */
|
|
51
|
+
findActiveByInputWebhook(webhookId: string): Promise<TDoc[]>;
|
|
52
|
+
/** Find active nodes where the given node is an input source */
|
|
53
|
+
findActiveByInput(nodeType: string, nodeId: string): Promise<TDoc[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Standard create: assertUniqueNodeName → model.create → normalizeAndValidateConnections.
|
|
56
|
+
* Pass node-specific fields via `fields` — userId, orgId, workspaceId, inputNodes,
|
|
57
|
+
* outputNodes, isActive, and name are handled automatically.
|
|
58
|
+
*/
|
|
59
|
+
protected createNode(dto: {
|
|
60
|
+
name?: string;
|
|
61
|
+
isActive?: boolean;
|
|
62
|
+
inputNodes?: any[];
|
|
63
|
+
outputNodes?: any[];
|
|
64
|
+
workspaceId?: string;
|
|
65
|
+
[key: string]: any;
|
|
66
|
+
}, userId: string, orgId: string, extraFields?: Record<string, any>): Promise<TDoc>;
|
|
67
|
+
/**
|
|
68
|
+
* Normalize inputNodes/outputNodes and validate the structural rules.
|
|
69
|
+
*
|
|
70
|
+
* Tres reglas. Dos son sobre la forma del grafo y necesitan ids —no se puede
|
|
71
|
+
* cruzar de workspace, y un nodo no se conecta a sí mismo—, y por eso viven
|
|
72
|
+
* aquí y no en el registro.
|
|
73
|
+
*
|
|
74
|
+
* La tercera sí es de tipos. Aquí ponía que la compatibilidad «es cosa del
|
|
75
|
+
* registro y se comprueba en el lienzo», y eso sólo vale si el lienzo es el
|
|
76
|
+
* único que escribe. No lo es: `/agent-context` le explica a un agente que
|
|
77
|
+
* meta las referencias a mano, y cualquier cliente con la clave puede. Lo
|
|
78
|
+
* que entra por ahí no falla, desaparece — ver `assertConnectableTypes`.
|
|
79
|
+
*/
|
|
80
|
+
protected normalizeAndValidateConnections(dto: {
|
|
81
|
+
inputNodes?: any[];
|
|
82
|
+
outputNodes?: any[];
|
|
83
|
+
}, workspaceId: any, selfId?: any): Promise<void>;
|
|
84
|
+
/** Build update payload with normalized nodes */
|
|
85
|
+
protected buildUpdatePayload(dto: Record<string, any>): Record<string, any>;
|
|
86
|
+
/**
|
|
87
|
+
* Walk the entity's schema and convert any field declared as
|
|
88
|
+
* `Types.ObjectId` (or `[Types.ObjectId]`) from a string back to a
|
|
89
|
+
* proper ObjectId before the value enters a `findOneAndUpdate` $set.
|
|
90
|
+
*
|
|
91
|
+
* **Why this exists:** Mongoose does NOT auto-cast nested $set paths to
|
|
92
|
+
* the schema's declared types — the cast only fires for full-document
|
|
93
|
+
* `save()` and `Document.set()`. So a dashboard PATCH that lands a
|
|
94
|
+
* `credentialId: "69fe70…"` string in the dto would persist a String
|
|
95
|
+
* even though the schema says `Types.ObjectId`. That's invisible to
|
|
96
|
+
* `_id`-keyed lookups (Mongoose casts query filters), but breaks any
|
|
97
|
+
* downstream query using `field: { $in: [ObjectId(…)] }` because
|
|
98
|
+
* MongoDB is strict-typed: String !== ObjectId.
|
|
99
|
+
*
|
|
100
|
+
* The Slack-trigger `processSlackCommand` was the canary: it routes
|
|
101
|
+
* inbound pushes by `team_id → credentials → triggers $in [ObjectId]`
|
|
102
|
+
* and silently dropped every `/joke` invocation as `no-trigger-opted-in`
|
|
103
|
+
* even though the toggle was correctly true on the entity. Fix landed
|
|
104
|
+
* 2026-05-09 — see `feedback_basenode_objectid_cast.md` for the full
|
|
105
|
+
* forensics. Lifting the cast here means every node service inherits
|
|
106
|
+
* the protection without repeating per-subclass logic.
|
|
107
|
+
*
|
|
108
|
+
* Schema-driven so adding a new ObjectId field on any entity
|
|
109
|
+
* (including Array<ObjectId> like `[{type: ObjectId, ref: 'Foo'}]`)
|
|
110
|
+
* gets covered automatically — no maintenance needed.
|
|
111
|
+
*
|
|
112
|
+
* Skips `_id` (Mongoose handles it explicitly in query casting),
|
|
113
|
+
* skips `null`/`undefined` (preserves "clear the relationship"
|
|
114
|
+
* semantics), skips non-hex strings (throwing here would mask
|
|
115
|
+
* real validation errors).
|
|
116
|
+
*/
|
|
117
|
+
private castSchemaObjectIdFields;
|
|
118
|
+
/** Standard update with node normalization and workspace validation */
|
|
119
|
+
update(id: string, dto: Record<string, any>, orgId: string): Promise<TDoc>;
|
|
120
|
+
}
|