@dudousxd/nestjs-catalog 0.9.0 → 0.11.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/catalog.controller.js +39 -11
- package/dist/catalog.events.d.ts +79 -8
- package/dist/catalog.events.js +40 -1
- package/dist/catalog.overlay-store.d.ts +29 -0
- package/dist/catalog.overlay-store.js +45 -2
- package/dist/catalog.pipeline.d.ts +82 -2
- package/dist/catalog.pipeline.js +60 -2
- package/dist/catalog.registry.base.d.ts +15 -4
- package/dist/catalog.registry.d.ts +9 -3
- package/dist/catalog.registry.js +22 -4
- package/dist/catalog.secrets.d.ts +283 -0
- package/dist/catalog.secrets.js +209 -0
- package/dist/catalog.service.d.ts +20 -5
- package/dist/catalog.service.js +23 -8
- package/dist/client.d.ts +2 -1
- package/dist/client.js +9 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +14 -2
- package/dist/transform-runner.d.ts +22 -5
- package/dist/transform-runner.js +126 -9
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* types are.
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.isWorkflowNodeKind = exports.isWorkflowExecutionMode = exports.workflowRunOrder = exports.workflowGraphHash = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.validateWorkflow = exports.TRANSFORM_LANGUAGES = exports.isWorkflowNode = exports.isWorkflowEdge = exports.isTransformLanguage = exports.isConnectorKind = exports.CONNECTOR_KINDS = exports.catalogRoutes = void 0;
|
|
14
|
+
exports.isWorkflowStatus = exports.isWorkflowNodeKind = exports.isWorkflowExecutionMode = exports.workflowRunOrder = exports.workflowGraphHash = exports.WORKFLOW_STATUSES = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.validateWorkflow = exports.TRANSFORM_LANGUAGES = exports.isWorkflowNode = exports.isWorkflowEdge = exports.isTransformLanguage = exports.isConnectorKind = exports.CONNECTOR_KINDS = exports.catalogRoutes = void 0;
|
|
15
15
|
/**
|
|
16
16
|
* Builds the paths the catalog controller serves, relative to wherever it was
|
|
17
17
|
* mounted. Kept as string builders rather than a fetch wrapper so the host
|
|
@@ -87,7 +87,15 @@ Object.defineProperty(exports, "WORKFLOW_EXECUTION_MODES", { enumerable: true, g
|
|
|
87
87
|
Object.defineProperty(exports, "WORKFLOW_ISSUE_CODES", { enumerable: true, get: function () { return catalog_pipeline_2.WORKFLOW_ISSUE_CODES; } });
|
|
88
88
|
Object.defineProperty(exports, "WORKFLOW_NODE_ID_PATTERN", { enumerable: true, get: function () { return catalog_pipeline_2.WORKFLOW_NODE_ID_PATTERN; } });
|
|
89
89
|
Object.defineProperty(exports, "WORKFLOW_NODE_KINDS", { enumerable: true, get: function () { return catalog_pipeline_2.WORKFLOW_NODE_KINDS; } });
|
|
90
|
+
// The draft/ready pair, for the same reason as the list above: a canvas that
|
|
91
|
+
// cannot see it restates it, and the copy is what drifts. Without this the
|
|
92
|
+
// editor could not tell a graph it is allowed to store from one the server
|
|
93
|
+
// would refuse — so it told everybody the second, which is wrong for every
|
|
94
|
+
// draft and is exactly the kind of confident-and-false sentence this codebase
|
|
95
|
+
// keeps removing.
|
|
96
|
+
Object.defineProperty(exports, "WORKFLOW_STATUSES", { enumerable: true, get: function () { return catalog_pipeline_2.WORKFLOW_STATUSES; } });
|
|
90
97
|
Object.defineProperty(exports, "workflowGraphHash", { enumerable: true, get: function () { return catalog_pipeline_2.workflowGraphHash; } });
|
|
91
98
|
Object.defineProperty(exports, "workflowRunOrder", { enumerable: true, get: function () { return catalog_pipeline_2.workflowRunOrder; } });
|
|
92
99
|
Object.defineProperty(exports, "isWorkflowExecutionMode", { enumerable: true, get: function () { return catalog_pipeline_2.isWorkflowExecutionMode; } });
|
|
93
100
|
Object.defineProperty(exports, "isWorkflowNodeKind", { enumerable: true, get: function () { return catalog_pipeline_2.isWorkflowNodeKind; } });
|
|
101
|
+
Object.defineProperty(exports, "isWorkflowStatus", { enumerable: true, get: function () { return catalog_pipeline_2.isWorkflowStatus; } });
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
export { CatalogProperty, type CatalogPropertyOptions, CatalogType, type CatalogTypeOptions, } from './catalog.decorators';
|
|
2
|
-
export { CATALOG_EVENT_PHASE, CATALOG_EVENT_PHASE_FALLBACK, CATALOG_EVENTS, CATALOG_LIB, type CatalogEvent, type CatalogEventPayloads, catalogEventPhase, channelNameFor, emitCatalog, } from './catalog.events';
|
|
2
|
+
export { CATALOG_EVENT_PHASE, CATALOG_EVENT_PHASE_FALLBACK, CATALOG_EVENTS, CATALOG_LIB, type CatalogEvent, type CatalogEventPayloads, UNATTRIBUTED_PRINCIPAL_ID, catalogEventPhase, channelNameFor, curationActor, emitCatalog, } from './catalog.events';
|
|
3
3
|
export { CatalogModule } from './catalog.module';
|
|
4
4
|
export { assertReadOnlyShape, type CatalogQueryRelation, type CatalogQueryRequest, type CatalogQueryResult, type CatalogQueryStore, isQueryStore, } from './catalog.query';
|
|
5
5
|
export { CATALOG_OPTIONS, type CatalogModuleOptions } from './catalog.options';
|
|
6
|
+
export * from './catalog.secrets';
|
|
6
7
|
export { type CatalogOverlayStore, FileCatalogOverlayStore, InMemoryCatalogOverlayStore, } from './catalog.overlay-store';
|
|
7
8
|
export { CATALOG_OVERLAY_STORE } from './catalog.overlay-store.token';
|
|
8
9
|
export { MikroOrmCatalogRegistry } from './catalog.registry';
|
|
9
10
|
export { CatalogRegistry } from './catalog.registry.base';
|
|
10
|
-
export { CATALOG_PIPELINE_STORE, type CatalogConnection, type CatalogConnector, type ConnectionCheck, CONNECTOR_KINDS, type CatalogPipelineStore, type CatalogStageStore, type CatalogTransform, type CatalogWorkflow, type CatalogWorkflowCapabilities, type CatalogWorkflowStore, type ConnectorKind, type ConnectorRun, isConnectorKind, isPipelineStore, isTransformLanguage, isWorkflowEdge, isWorkflowExecutionMode, isWorkflowNode, isWorkflowNodeKind, supportsWorkflows, supportsWorkflowStages, TRANSFORM_RUNNER, TRANSFORM_LANGUAGES, type TransformLanguage, type TransformResult, type TransformRunner, validateWorkflow, WORKFLOW_EXECUTION_MODES, WORKFLOW_ISSUE_CODES, WORKFLOW_NODE_ID_PATTERN, WORKFLOW_NODE_KINDS, type WorkflowEdge, type WorkflowExecutionMode, type WorkflowGraph, workflowGraphHash, type WorkflowIssueCode, type WorkflowNode, type WorkflowNodeKind, type WorkflowNodeOutcome, type WorkflowNodeStepInput, type WorkflowNodeStepOutput, workflowRunOrder, type WorkflowSinkNode, type WorkflowSourceNode, type WorkflowStageRef, type WorkflowTransformNode, type WorkflowValidationIssue, } from './catalog.pipeline';
|
|
11
|
+
export { CATALOG_PIPELINE_STORE, type CatalogConnection, type CatalogConnector, type ConnectionCheck, CONNECTOR_KINDS, type CatalogPipelineStore, type CatalogStageStore, type CatalogTransform, type CatalogWorkflow, type CatalogWorkflowCapabilities, type CatalogWorkflowStore, type ConnectorKind, type ConnectorRun, isConnectorKind, isPipelineStore, isTransformLanguage, isWorkflowEdge, isWorkflowExecutionMode, isWorkflowNode, isWorkflowNodeKind, isWorkflowStatus, supportsWorkflows, supportsWorkflowStages, TRANSFORM_RUNNER, TRANSFORM_LANGUAGES, type TransformLanguage, type TransformResult, type TransformRunner, validateWorkflow, WORKFLOW_EXECUTION_MODES, WORKFLOW_ISSUE_CODES, WORKFLOW_NODE_ID_PATTERN, WORKFLOW_NODE_KINDS, WORKFLOW_STATUSES, type WorkflowEdge, type WorkflowExecutionMode, type WorkflowGraph, workflowGraphHash, type WorkflowIssueCode, type WorkflowNode, type WorkflowNodeKind, type WorkflowNodeOutcome, type WorkflowNodeStepInput, type WorkflowNodeStepOutput, workflowRunOrder, type WorkflowSinkNode, type WorkflowSourceNode, type WorkflowStageRef, type WorkflowStatus, type WorkflowTransformNode, type WorkflowValidationIssue, } from './catalog.pipeline';
|
|
11
12
|
export * from './catalog.environment';
|
|
12
13
|
export { QueryCache, toCsv } from './catalog.query-cache';
|
|
13
14
|
export { SubprocessTransformRunner, type TransformRunnerOptions, } from './transform-runner';
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.
|
|
18
|
-
exports.RequireScopes = exports.RequireHuman = exports.REQUIRES_HUMAN = exports.REQUIRED_SCOPES = exports.MikroOrmReadStore = exports.supportsCarryForward = exports.isWriteStore = exports.isReservedColumn = exports.isCatalogStoreCapabilities = exports.findColumnCollisions = exports.CatalogColumnCollisionError = exports.CATALOG_STORE = exports.CATALOG_SNAPSHOT_MODES = exports.CATALOG_RESERVED_COLUMNS = exports.assertNoColumnCollisions = exports.StaticKeyPrincipalResolver = exports.readableObjectPage = exports.mayWrite = exports.mayRead = exports.maySeeClassification = exports.PRINCIPAL_ACTOR_SEPARATOR = exports.parsePrincipalId = exports.hasScope = exports.expandScopes = exports.delegatePrincipal = exports.composePrincipalId = exports.CATALOG_PRINCIPAL_RESOLVER = exports.traceOutcomeFilter = exports.isWorkspaceStore = exports.isTraceStore = exports.isCatalogTraceOutcome = exports.embeddedVisualization = exports.CATALOG_WORKSPACE_STORE = exports.CATALOG_TRACE_STORE = void 0;
|
|
17
|
+
exports.emptySearch = exports.bestMatch = exports.MAX_SEARCH_LIMIT = exports.DEFAULT_SEARCH_LIMIT = exports.CatalogService = exports.SubprocessTransformRunner = exports.toCsv = exports.QueryCache = exports.workflowRunOrder = exports.workflowGraphHash = exports.WORKFLOW_STATUSES = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.validateWorkflow = exports.TRANSFORM_LANGUAGES = exports.TRANSFORM_RUNNER = exports.supportsWorkflowStages = exports.supportsWorkflows = exports.isWorkflowStatus = exports.isWorkflowNodeKind = exports.isWorkflowNode = exports.isWorkflowExecutionMode = exports.isWorkflowEdge = exports.isTransformLanguage = exports.isPipelineStore = exports.isConnectorKind = exports.CONNECTOR_KINDS = exports.CATALOG_PIPELINE_STORE = exports.CatalogRegistry = exports.MikroOrmCatalogRegistry = exports.CATALOG_OVERLAY_STORE = exports.InMemoryCatalogOverlayStore = exports.FileCatalogOverlayStore = exports.CATALOG_OPTIONS = exports.isQueryStore = exports.assertReadOnlyShape = exports.CatalogModule = exports.emitCatalog = exports.curationActor = exports.channelNameFor = exports.catalogEventPhase = exports.UNATTRIBUTED_PRINCIPAL_ID = exports.CATALOG_LIB = exports.CATALOG_EVENTS = exports.CATALOG_EVENT_PHASE_FALLBACK = exports.CATALOG_EVENT_PHASE = exports.CatalogType = exports.CatalogProperty = void 0;
|
|
18
|
+
exports.RequireScopes = exports.RequireHuman = exports.REQUIRES_HUMAN = exports.REQUIRED_SCOPES = exports.MikroOrmReadStore = exports.supportsCarryForward = exports.isWriteStore = exports.isReservedColumn = exports.isCatalogStoreCapabilities = exports.findColumnCollisions = exports.CatalogColumnCollisionError = exports.CATALOG_STORE = exports.CATALOG_SNAPSHOT_MODES = exports.CATALOG_RESERVED_COLUMNS = exports.assertNoColumnCollisions = exports.StaticKeyPrincipalResolver = exports.readableObjectPage = exports.mayWrite = exports.mayRead = exports.maySeeClassification = exports.PRINCIPAL_ACTOR_SEPARATOR = exports.parsePrincipalId = exports.hasScope = exports.expandScopes = exports.delegatePrincipal = exports.composePrincipalId = exports.CATALOG_PRINCIPAL_RESOLVER = exports.traceOutcomeFilter = exports.isWorkspaceStore = exports.isTraceStore = exports.isCatalogTraceOutcome = exports.embeddedVisualization = exports.CATALOG_WORKSPACE_STORE = exports.CATALOG_TRACE_STORE = exports.CATALOG_TRACE_OUTCOMES = exports.visibleToPrincipal = exports.searchCatalog = exports.maySearch = void 0;
|
|
19
19
|
var catalog_decorators_1 = require("./catalog.decorators");
|
|
20
20
|
Object.defineProperty(exports, "CatalogProperty", { enumerable: true, get: function () { return catalog_decorators_1.CatalogProperty; } });
|
|
21
21
|
Object.defineProperty(exports, "CatalogType", { enumerable: true, get: function () { return catalog_decorators_1.CatalogType; } });
|
|
@@ -24,8 +24,10 @@ Object.defineProperty(exports, "CATALOG_EVENT_PHASE", { enumerable: true, get: f
|
|
|
24
24
|
Object.defineProperty(exports, "CATALOG_EVENT_PHASE_FALLBACK", { enumerable: true, get: function () { return catalog_events_1.CATALOG_EVENT_PHASE_FALLBACK; } });
|
|
25
25
|
Object.defineProperty(exports, "CATALOG_EVENTS", { enumerable: true, get: function () { return catalog_events_1.CATALOG_EVENTS; } });
|
|
26
26
|
Object.defineProperty(exports, "CATALOG_LIB", { enumerable: true, get: function () { return catalog_events_1.CATALOG_LIB; } });
|
|
27
|
+
Object.defineProperty(exports, "UNATTRIBUTED_PRINCIPAL_ID", { enumerable: true, get: function () { return catalog_events_1.UNATTRIBUTED_PRINCIPAL_ID; } });
|
|
27
28
|
Object.defineProperty(exports, "catalogEventPhase", { enumerable: true, get: function () { return catalog_events_1.catalogEventPhase; } });
|
|
28
29
|
Object.defineProperty(exports, "channelNameFor", { enumerable: true, get: function () { return catalog_events_1.channelNameFor; } });
|
|
30
|
+
Object.defineProperty(exports, "curationActor", { enumerable: true, get: function () { return catalog_events_1.curationActor; } });
|
|
29
31
|
Object.defineProperty(exports, "emitCatalog", { enumerable: true, get: function () { return catalog_events_1.emitCatalog; } });
|
|
30
32
|
var catalog_module_1 = require("./catalog.module");
|
|
31
33
|
Object.defineProperty(exports, "CatalogModule", { enumerable: true, get: function () { return catalog_module_1.CatalogModule; } });
|
|
@@ -34,6 +36,14 @@ Object.defineProperty(exports, "assertReadOnlyShape", { enumerable: true, get: f
|
|
|
34
36
|
Object.defineProperty(exports, "isQueryStore", { enumerable: true, get: function () { return catalog_query_1.isQueryStore; } });
|
|
35
37
|
var catalog_options_1 = require("./catalog.options");
|
|
36
38
|
Object.defineProperty(exports, "CATALOG_OPTIONS", { enumerable: true, get: function () { return catalog_options_1.CATALOG_OPTIONS; } });
|
|
39
|
+
// Everything, deliberately, and here more than anywhere: this is a seam two
|
|
40
|
+
// separate provider packages are being written against. A barrel that shipped
|
|
41
|
+
// `CATALOG_SECRET_VAULT` and `CatalogSecretVault` but not `SealedSecret` or
|
|
42
|
+
// `SecretContext` — the return type and the argument of the two methods a
|
|
43
|
+
// provider implements — would be the exact gap `index.barrel.spec.ts` was
|
|
44
|
+
// written after, reproduced on the one surface where a third party compiles
|
|
45
|
+
// against it.
|
|
46
|
+
__exportStar(require("./catalog.secrets"), exports);
|
|
37
47
|
var catalog_overlay_store_1 = require("./catalog.overlay-store");
|
|
38
48
|
Object.defineProperty(exports, "FileCatalogOverlayStore", { enumerable: true, get: function () { return catalog_overlay_store_1.FileCatalogOverlayStore; } });
|
|
39
49
|
Object.defineProperty(exports, "InMemoryCatalogOverlayStore", { enumerable: true, get: function () { return catalog_overlay_store_1.InMemoryCatalogOverlayStore; } });
|
|
@@ -53,6 +63,7 @@ Object.defineProperty(exports, "isWorkflowEdge", { enumerable: true, get: functi
|
|
|
53
63
|
Object.defineProperty(exports, "isWorkflowExecutionMode", { enumerable: true, get: function () { return catalog_pipeline_1.isWorkflowExecutionMode; } });
|
|
54
64
|
Object.defineProperty(exports, "isWorkflowNode", { enumerable: true, get: function () { return catalog_pipeline_1.isWorkflowNode; } });
|
|
55
65
|
Object.defineProperty(exports, "isWorkflowNodeKind", { enumerable: true, get: function () { return catalog_pipeline_1.isWorkflowNodeKind; } });
|
|
66
|
+
Object.defineProperty(exports, "isWorkflowStatus", { enumerable: true, get: function () { return catalog_pipeline_1.isWorkflowStatus; } });
|
|
56
67
|
Object.defineProperty(exports, "supportsWorkflows", { enumerable: true, get: function () { return catalog_pipeline_1.supportsWorkflows; } });
|
|
57
68
|
Object.defineProperty(exports, "supportsWorkflowStages", { enumerable: true, get: function () { return catalog_pipeline_1.supportsWorkflowStages; } });
|
|
58
69
|
Object.defineProperty(exports, "TRANSFORM_RUNNER", { enumerable: true, get: function () { return catalog_pipeline_1.TRANSFORM_RUNNER; } });
|
|
@@ -62,6 +73,7 @@ Object.defineProperty(exports, "WORKFLOW_EXECUTION_MODES", { enumerable: true, g
|
|
|
62
73
|
Object.defineProperty(exports, "WORKFLOW_ISSUE_CODES", { enumerable: true, get: function () { return catalog_pipeline_1.WORKFLOW_ISSUE_CODES; } });
|
|
63
74
|
Object.defineProperty(exports, "WORKFLOW_NODE_ID_PATTERN", { enumerable: true, get: function () { return catalog_pipeline_1.WORKFLOW_NODE_ID_PATTERN; } });
|
|
64
75
|
Object.defineProperty(exports, "WORKFLOW_NODE_KINDS", { enumerable: true, get: function () { return catalog_pipeline_1.WORKFLOW_NODE_KINDS; } });
|
|
76
|
+
Object.defineProperty(exports, "WORKFLOW_STATUSES", { enumerable: true, get: function () { return catalog_pipeline_1.WORKFLOW_STATUSES; } });
|
|
65
77
|
Object.defineProperty(exports, "workflowGraphHash", { enumerable: true, get: function () { return catalog_pipeline_1.workflowGraphHash; } });
|
|
66
78
|
Object.defineProperty(exports, "workflowRunOrder", { enumerable: true, get: function () { return catalog_pipeline_1.workflowRunOrder; } });
|
|
67
79
|
// The environment surface: which catalog database a call is served from, and
|
|
@@ -15,11 +15,28 @@ export interface TransformRunnerOptions {
|
|
|
15
15
|
/**
|
|
16
16
|
* Runs a transform in a child process, with a clock on it.
|
|
17
17
|
*
|
|
18
|
-
* **This is not a security boundary
|
|
19
|
-
* runaway allocation, a stray
|
|
20
|
-
* the child gets a timeout and
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* **This is not a security boundary, and the trimmed environment is not one
|
|
19
|
+
* either.** It stops accidents — an infinite loop, a runaway allocation, a stray
|
|
20
|
+
* read of `process.env.DATABASE_PASSWORD` — because the child gets a timeout and
|
|
21
|
+
* an environment of `{PATH, NODE_ENV}`. It does not stop code written to escape
|
|
22
|
+
* it, and it is worth being exact about how thin the allowlist is rather than
|
|
23
|
+
* leaving a reader to assume it holds:
|
|
24
|
+
*
|
|
25
|
+
* - the child inherits nothing of the parent's environment **through `env`**,
|
|
26
|
+
* and reads all of it anyway from `/proc/<ppid>/environ`, which is readable
|
|
27
|
+
* because parent and child run as the same uid;
|
|
28
|
+
* - it runs in a working directory of this runner's choosing but on the host's
|
|
29
|
+
* filesystem, so a service account token under
|
|
30
|
+
* `/var/run/secrets/kubernetes.io/serviceaccount/` is an absolute path away;
|
|
31
|
+
* - it can open sockets, as whatever user the service runs as.
|
|
32
|
+
*
|
|
33
|
+
* So the allowlist is a guard rail against the accident, and the reachability of
|
|
34
|
+
* everything it names is a property of the process boundary, not a leak to be
|
|
35
|
+
* patched. **Running a transform is running code in this pod.** Who is allowed
|
|
36
|
+
* to is therefore an authorisation question and not a sandboxing one, and it is
|
|
37
|
+
* answered at the HTTP surface — see the "Running a transform is running code"
|
|
38
|
+
* section of `@dudousxd/nestjs-catalog-pipeline`'s README, which is where a host
|
|
39
|
+
* can actually read it, and `pipeline.controller.ts` for the checks themselves.
|
|
23
40
|
*
|
|
24
41
|
* That is a deliberate trade for the case this is built for, where transforms
|
|
25
42
|
* are written by the same people who already have database access. A catalog
|
package/dist/transform-runner.js
CHANGED
|
@@ -13,10 +13,50 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
13
13
|
exports.SubprocessTransformRunner = void 0;
|
|
14
14
|
const node_child_process_1 = require("node:child_process");
|
|
15
15
|
const node_fs_1 = require("node:fs");
|
|
16
|
+
const node_os_1 = require("node:os");
|
|
16
17
|
const node_path_1 = require("node:path");
|
|
17
18
|
const common_1 = require("@nestjs/common");
|
|
18
19
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
19
20
|
const MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
21
|
+
/**
|
|
22
|
+
* How much of the child's stderr is held, and why it is a different number from
|
|
23
|
+
* {@link MAX_OUTPUT_BYTES} with a different consequence.
|
|
24
|
+
*
|
|
25
|
+
* It had no bound at all, which is the one shape a capture must never have when
|
|
26
|
+
* the thing filling it is user code: `stderr += chunk` ran for the whole timeout
|
|
27
|
+
* window, so a transform whose only line is a loop writing to fd 2 grew the
|
|
28
|
+
* **parent's** heap — not the child's — at whatever rate the pipe would carry,
|
|
29
|
+
* and took the pod out with it. The timeout is no answer to that: thirty seconds
|
|
30
|
+
* of an unthrottled writer is gigabytes, and the process that dies is the one
|
|
31
|
+
* serving every other request.
|
|
32
|
+
*
|
|
33
|
+
* Bounded rather than killed, which is the opposite of what stdout overflow
|
|
34
|
+
* does, and the asymmetry is the point. Stdout *is* the result channel — past
|
|
35
|
+
* {@link MAX_OUTPUT_BYTES} there is no readable JSON line at the end of it and
|
|
36
|
+
* the run has already failed, so killing costs nothing. Stderr is only ever the
|
|
37
|
+
* diagnostic: a transform that writes a great deal to it and then returns a
|
|
38
|
+
* perfectly good array of rows is a working transform, and killing it would turn
|
|
39
|
+
* a noisy dependency's warnings into a failed load.
|
|
40
|
+
*
|
|
41
|
+
* The **head** is kept, because the head is what is read. Both places that
|
|
42
|
+
* consume this take `stderr.slice(0, 500)` — the first line of a traceback, the
|
|
43
|
+
* import error, the thing that says what went wrong — so dropping the tail
|
|
44
|
+
* discards exactly the part nobody was going to see. 64 KiB is far more than any
|
|
45
|
+
* of those and small enough that the ceiling is not itself a memory decision.
|
|
46
|
+
*/
|
|
47
|
+
const MAX_CAPTURED_STDERR_BYTES = 64 * 1024;
|
|
48
|
+
/**
|
|
49
|
+
* Whether the child is put in its own process group, so that stopping it stops
|
|
50
|
+
* what it started.
|
|
51
|
+
*
|
|
52
|
+
* POSIX only, because the mechanism is POSIX: `detached` there makes the child a
|
|
53
|
+
* process-group leader and `process.kill(-pid)` signals the whole group, which
|
|
54
|
+
* is the only way to reach a grandchild. On Windows `detached` means something
|
|
55
|
+
* else entirely (a new console) and a negative pid is not a group, so the
|
|
56
|
+
* platform gets the single-process kill it always had rather than a call that
|
|
57
|
+
* would throw on every timeout.
|
|
58
|
+
*/
|
|
59
|
+
const KILL_PROCESS_GROUP = process.platform !== 'win32';
|
|
20
60
|
/**
|
|
21
61
|
* How much of what a transform logged is carried back, on both axes.
|
|
22
62
|
*
|
|
@@ -73,11 +113,28 @@ const REPORTED_PACKAGES = ['pandas', 'numpy', 'pyarrow', 'requests'];
|
|
|
73
113
|
/**
|
|
74
114
|
* Runs a transform in a child process, with a clock on it.
|
|
75
115
|
*
|
|
76
|
-
* **This is not a security boundary
|
|
77
|
-
* runaway allocation, a stray
|
|
78
|
-
* the child gets a timeout and
|
|
79
|
-
*
|
|
80
|
-
*
|
|
116
|
+
* **This is not a security boundary, and the trimmed environment is not one
|
|
117
|
+
* either.** It stops accidents — an infinite loop, a runaway allocation, a stray
|
|
118
|
+
* read of `process.env.DATABASE_PASSWORD` — because the child gets a timeout and
|
|
119
|
+
* an environment of `{PATH, NODE_ENV}`. It does not stop code written to escape
|
|
120
|
+
* it, and it is worth being exact about how thin the allowlist is rather than
|
|
121
|
+
* leaving a reader to assume it holds:
|
|
122
|
+
*
|
|
123
|
+
* - the child inherits nothing of the parent's environment **through `env`**,
|
|
124
|
+
* and reads all of it anyway from `/proc/<ppid>/environ`, which is readable
|
|
125
|
+
* because parent and child run as the same uid;
|
|
126
|
+
* - it runs in a working directory of this runner's choosing but on the host's
|
|
127
|
+
* filesystem, so a service account token under
|
|
128
|
+
* `/var/run/secrets/kubernetes.io/serviceaccount/` is an absolute path away;
|
|
129
|
+
* - it can open sockets, as whatever user the service runs as.
|
|
130
|
+
*
|
|
131
|
+
* So the allowlist is a guard rail against the accident, and the reachability of
|
|
132
|
+
* everything it names is a property of the process boundary, not a leak to be
|
|
133
|
+
* patched. **Running a transform is running code in this pod.** Who is allowed
|
|
134
|
+
* to is therefore an authorisation question and not a sandboxing one, and it is
|
|
135
|
+
* answered at the HTTP surface — see the "Running a transform is running code"
|
|
136
|
+
* section of `@dudousxd/nestjs-catalog-pipeline`'s README, which is where a host
|
|
137
|
+
* can actually read it, and `pipeline.controller.ts` for the checks themselves.
|
|
81
138
|
*
|
|
82
139
|
* That is a deliberate trade for the case this is built for, where transforms
|
|
83
140
|
* are written by the same people who already have database access. A catalog
|
|
@@ -179,7 +236,21 @@ let SubprocessTransformRunner = SubprocessTransformRunner_1 = class SubprocessTr
|
|
|
179
236
|
const child = (0, node_child_process_1.spawn)(command, args, {
|
|
180
237
|
// An empty environment, not the parent's. A transform has no business
|
|
181
238
|
// reading the database password, and inheriting env is how it would.
|
|
239
|
+
// Read the class docblock before treating this as containment: the same
|
|
240
|
+
// values are a `/proc/<ppid>/environ` read away, and this is a guard
|
|
241
|
+
// rail against the accidental read rather than a boundary.
|
|
182
242
|
env: { PATH: process.env.PATH ?? '', NODE_ENV: 'production' },
|
|
243
|
+
// Not the parent's, which is a running service's directory and holds
|
|
244
|
+
// the `.env` the allowlist above exists to withhold — a transform whose
|
|
245
|
+
// first line is `readFileSync(".env")` was reading the host application's
|
|
246
|
+
// configuration by relative path. A temporary directory keeps the file
|
|
247
|
+
// writes a transform may legitimately want working while making the one
|
|
248
|
+
// path it can name without knowing anything about the deployment
|
|
249
|
+
// uninteresting. Absolute paths are unaffected, and cannot be.
|
|
250
|
+
cwd: (0, node_os_1.tmpdir)(),
|
|
251
|
+
// Its own process group, so the timeout below can reach a grandchild.
|
|
252
|
+
// See {@link KILL_PROCESS_GROUP}.
|
|
253
|
+
detached: KILL_PROCESS_GROUP,
|
|
183
254
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
184
255
|
});
|
|
185
256
|
let stdout = '';
|
|
@@ -189,16 +260,22 @@ let SubprocessTransformRunner = SubprocessTransformRunner_1 = class SubprocessTr
|
|
|
189
260
|
if (settled)
|
|
190
261
|
return;
|
|
191
262
|
settled = true;
|
|
192
|
-
child
|
|
263
|
+
stop(child);
|
|
193
264
|
reject(new Error(`The transform ran for longer than ${timeoutMs}ms and was stopped.`));
|
|
194
265
|
}, timeoutMs);
|
|
195
266
|
child.stdout.on('data', (chunk) => {
|
|
196
267
|
stdout += chunk.toString();
|
|
197
268
|
if (stdout.length > MAX_OUTPUT_BYTES)
|
|
198
|
-
child
|
|
269
|
+
stop(child);
|
|
199
270
|
});
|
|
200
271
|
child.stderr.on('data', (chunk) => {
|
|
201
|
-
|
|
272
|
+
// Appended only while there is room, rather than appended and trimmed:
|
|
273
|
+
// trimming after the fact still materialises the whole chunk into the
|
|
274
|
+
// parent's heap, which is the thing being bounded. See
|
|
275
|
+
// {@link MAX_CAPTURED_STDERR_BYTES} for why this bounds rather than kills.
|
|
276
|
+
if (stderr.length >= MAX_CAPTURED_STDERR_BYTES)
|
|
277
|
+
return;
|
|
278
|
+
stderr += chunk.toString().slice(0, MAX_CAPTURED_STDERR_BYTES - stderr.length);
|
|
202
279
|
});
|
|
203
280
|
child.on('error', (error) => {
|
|
204
281
|
if (settled)
|
|
@@ -228,7 +305,12 @@ let SubprocessTransformRunner = SubprocessTransformRunner_1 = class SubprocessTr
|
|
|
228
305
|
return this.pythonPath;
|
|
229
306
|
const venv = this.options.pythonVenv ?? process.env.CATALOG_PYTHON_VENV;
|
|
230
307
|
if (venv) {
|
|
231
|
-
|
|
308
|
+
// Absolute, and it has to be: a child now runs in a temporary directory
|
|
309
|
+
// rather than the parent's, so a relative `CATALOG_PYTHON_VENV` — which
|
|
310
|
+
// `existsSync` here resolves against the *parent's* cwd — would pass this
|
|
311
|
+
// check and then fail to spawn. Resolved once, at the point the two cwds
|
|
312
|
+
// are still the same.
|
|
313
|
+
const candidate = (0, node_path_1.resolve)((0, node_path_1.join)(venv, 'bin', 'python'));
|
|
232
314
|
if ((0, node_fs_1.existsSync)(candidate)) {
|
|
233
315
|
this.pythonPath = candidate;
|
|
234
316
|
this.logger.log(`Python transforms run in the venv at ${venv}`);
|
|
@@ -256,6 +338,41 @@ exports.SubprocessTransformRunner = SubprocessTransformRunner = SubprocessTransf
|
|
|
256
338
|
(0, common_1.Injectable)(),
|
|
257
339
|
__metadata("design:paramtypes", [Object])
|
|
258
340
|
], SubprocessTransformRunner);
|
|
341
|
+
/**
|
|
342
|
+
* Stop the transform, and everything the transform started.
|
|
343
|
+
*
|
|
344
|
+
* `child.kill()` signals one pid. A transform that double-forks — two lines of
|
|
345
|
+
* `child_process.spawn` with `detached` and an `unref` — leaves a grandchild
|
|
346
|
+
* that the direct child's death says nothing about, so the timeout expired, the
|
|
347
|
+
* caller was told the run had been stopped, and the work carried on
|
|
348
|
+
* indefinitely. That is not a hypothetical: it is the standard way a timeout on
|
|
349
|
+
* a process is escaped, and a bound that a caller can opt out of is not a bound.
|
|
350
|
+
*
|
|
351
|
+
* So the child leads its own process group and the negative pid signals the
|
|
352
|
+
* group, which is every descendant that has not deliberately left it. Leaving
|
|
353
|
+
* one is possible (`setsid` again) and there is no answer to that short of a
|
|
354
|
+
* cgroup or a container — the same place the class docblock's honesty about the
|
|
355
|
+
* boundary ends up, for the same reason.
|
|
356
|
+
*
|
|
357
|
+
* Falls through to the single-process kill whenever the group kill cannot be the
|
|
358
|
+
* one that happens: on Windows, where a negative pid is not a group, and on an
|
|
359
|
+
* `ESRCH` where the group is already gone and the direct kill is a harmless
|
|
360
|
+
* no-op. A throw here would replace a timeout error — which says something true
|
|
361
|
+
* and useful — with an unhandled one that says nothing.
|
|
362
|
+
*/
|
|
363
|
+
function stop(child) {
|
|
364
|
+
const pid = child.pid;
|
|
365
|
+
if (KILL_PROCESS_GROUP && pid !== undefined) {
|
|
366
|
+
try {
|
|
367
|
+
process.kill(-pid, 'SIGKILL');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// Already gone, or never grouped. The direct kill below covers both.
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
child.kill('SIGKILL');
|
|
375
|
+
}
|
|
259
376
|
/**
|
|
260
377
|
* The traceback, plus the tail of what the code printed on its way to it.
|
|
261
378
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-catalog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "A metadata registry for NestJS: object types, properties and relations, derived from your ORM and enriched with decorators.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Davide Carvalho",
|