@xeplr/actions 2.0.0 → 2.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.
- package/README.md +1 -1
- package/lib/dataDir.js +6 -135
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -302,7 +302,7 @@ Which inputs are checked: `file-list` `folder`; `file-read` / `file-delete` `pat
|
|
|
302
302
|
|
|
303
303
|
**`email-send` attachments** are `{ filename, path }` (inside the folder) or `{ filename, content }`. `href` and `raw` are refused, and so is a body that is not text: the mail library reads `path`, `href` and `html: { path }` from disk or from the network, so an unchecked one would mail the server's own settings or fetch an internal address.
|
|
304
304
|
|
|
305
|
-
`dataDir` is exported (`resolve(name)`, `tenantFolder()`, `scratch()`, `nameOf(path)`) for a host's own code that keeps files per tenant.
|
|
305
|
+
`dataDir` is exported (`resolve(name)`, `tenantFolder()`, `scratch()`, `nameOf(path)`) for a host's own code that keeps files per tenant. It is `@xeplr/db`'s (peer ^1.1.0), the one copy every package uses.
|
|
306
306
|
|
|
307
307
|
**From 1.x:** `XEPLR_FILE_ROOTS` and `XEPLR_ACTIONS_TMP_DIR` are gone. Set `XEPLR_DATA_DIR`, call `registerApplication()`, and run actions inside a tenant context (jobs and the workflow engine already do). A saved path outside the tenant's folder is refused until its files are moved in.
|
|
308
308
|
|
package/lib/dataDir.js
CHANGED
|
@@ -1,135 +1,6 @@
|
|
|
1
|
-
// WHERE A STEP'S FILES ARE —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
// <XEPLR_DATA_DIR>/<applicationId>/<mtId1>/<mtId2>/…
|
|
9
|
-
//
|
|
10
|
-
// The application comes from @xeplr/db's registerApplication (a company id is
|
|
11
|
-
// minted per app, so company 42 of BI and company 42 of ERP are two tenants);
|
|
12
|
-
// the tenant from the context every query already runs in — a request after
|
|
13
|
-
// the membership gate, a job or a workflow run (runWithMt). Never from a
|
|
14
|
-
// step's input: a step names `bank/inbox/sept.csv`, and cannot name anything
|
|
15
|
-
// outside its own folder.
|
|
16
|
-
//
|
|
17
|
-
// a name bank/inbox/sept.csv → inside the folder
|
|
18
|
-
// a full path accepted only when it is inside the folder already (what
|
|
19
|
-
// an earlier step returned: file-list's `path`)
|
|
20
|
-
// '*' at a level `_all` — a job for every company has a folder of its own,
|
|
21
|
-
// and no company's step can reach it
|
|
22
|
-
// no tenant refused, as a query with no tenant finds nothing; never
|
|
23
|
-
// the root
|
|
24
|
-
//
|
|
25
|
-
// `..` and symlinks are resolved before the check, so neither steps out.
|
|
26
|
-
// NOT DEFAULTED: "Where data lives is never defaulted." A missing or relative
|
|
27
|
-
// XEPLR_DATA_DIR is refused by name, and so is one that does not exist — a
|
|
28
|
-
// wrong root that boots reads as empty data, not as an error.
|
|
29
|
-
|
|
30
|
-
var fs = require('fs');
|
|
31
|
-
var path = require('path');
|
|
32
|
-
|
|
33
|
-
var VAR = 'XEPLR_DATA_DIR';
|
|
34
|
-
var requiredEnv = [VAR];
|
|
35
|
-
var ALL = '_all';
|
|
36
|
-
var SCRATCH = 'tmp';
|
|
37
|
-
|
|
38
|
-
function refused(message, code) {
|
|
39
|
-
var err = new Error(message);
|
|
40
|
-
err.code = code || 'PATH_NOT_ALLOWED';
|
|
41
|
-
err.status = 400;
|
|
42
|
-
return err;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function db() { return require('@xeplr/db'); }
|
|
46
|
-
|
|
47
|
-
/** The data root, absolute and real. */
|
|
48
|
-
function root() {
|
|
49
|
-
var dir = process.env[VAR];
|
|
50
|
-
if (!dir || !String(dir).trim()) {
|
|
51
|
-
throw refused(VAR + ' is not set: it is the folder all data lives under (absolute), e.g. /srv/xeplr-data.', 'DATA_DIR_UNSET');
|
|
52
|
-
}
|
|
53
|
-
if (!path.isAbsolute(dir)) throw refused(VAR + ' must be an absolute folder, and "' + dir + '" is not one.', 'DATA_DIR_UNSET');
|
|
54
|
-
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
55
|
-
throw refused(VAR + ' is "' + dir + '", which is not a folder that exists. Create it, or point ' + VAR + ' at the one that holds the data.', 'DATA_DIR_UNSET');
|
|
56
|
-
}
|
|
57
|
-
return fs.realpathSync(dir);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// An id becomes a folder name as it is. One that could not be one — or could
|
|
61
|
-
// climb out (`..`) — is refused rather than cleaned up into someone else's.
|
|
62
|
-
function segment(value, what) {
|
|
63
|
-
if (value === '*') return ALL;
|
|
64
|
-
var s = value === null || value === undefined ? '' : String(value);
|
|
65
|
-
if (!/^[A-Za-z0-9_.-]{1,100}$/.test(s) || s === '.' || s === '..') {
|
|
66
|
-
throw refused('The ' + what + ' "' + s + '" cannot name a folder.', 'NO_TENANT');
|
|
67
|
-
}
|
|
68
|
-
return s;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* The folder the current work may use: <root>/<app>/<mtId1>/…, made if it is
|
|
73
|
-
* not there yet. An app with no tenancy registered is one tenant: <root>/<app>.
|
|
74
|
-
*/
|
|
75
|
-
function tenantFolder() {
|
|
76
|
-
var x = db();
|
|
77
|
-
var app = x.getApplicationId();
|
|
78
|
-
if (!app) throw refused('No application is registered (registerApplication), so there is no folder for its files.', 'NO_TENANT');
|
|
79
|
-
var parts = [root(), segment(app, 'application')];
|
|
80
|
-
var levels = x.getMtConfig().levels || 0;
|
|
81
|
-
var ctx = x.getMtContext() || {};
|
|
82
|
-
for (var i = 1; i <= levels; i++) {
|
|
83
|
-
var v = ctx['mtId' + i];
|
|
84
|
-
if (v === undefined || v === null || v === '') {
|
|
85
|
-
throw refused('This work runs as no tenant, so it has no folder: files are only ever inside the folder of the company it runs for.', 'NO_TENANT');
|
|
86
|
-
}
|
|
87
|
-
parts.push(segment(v, 'tenant id'));
|
|
88
|
-
}
|
|
89
|
-
var dir = path.join.apply(path, parts);
|
|
90
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
91
|
-
return fs.realpathSync(dir);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** The tenant's own temporary folder: spools, downloads, exports nobody named. */
|
|
95
|
-
function scratch() {
|
|
96
|
-
var dir = path.join(tenantFolder(), SCRATCH);
|
|
97
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
98
|
-
return dir;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function inside(real, folder) {
|
|
102
|
-
return real === folder || real.indexOf(folder + path.sep) === 0;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* A name (or a full path already inside the folder) → the real absolute path,
|
|
107
|
-
* inside the tenant's folder; refused otherwise. A path that does not exist
|
|
108
|
-
* yet (a move's target, an export) is checked by its nearest existing parent,
|
|
109
|
-
* so `../` and symlinks cannot step out.
|
|
110
|
-
*/
|
|
111
|
-
function resolve(p, what) {
|
|
112
|
-
if (!p || typeof p !== 'string' || !p.trim()) throw refused((what || 'A path') + ' is required.');
|
|
113
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(p)) throw refused((what || 'The path') + ' "' + p + '" is an address, not a file in this company\'s folder.');
|
|
114
|
-
var folder = tenantFolder();
|
|
115
|
-
var resolved = path.isAbsolute(p) ? path.resolve(p) : path.resolve(folder, p);
|
|
116
|
-
var probe = resolved;
|
|
117
|
-
var rest = '';
|
|
118
|
-
while (!fs.existsSync(probe) && path.dirname(probe) !== probe) {
|
|
119
|
-
rest = path.join(path.basename(probe), rest);
|
|
120
|
-
probe = path.dirname(probe);
|
|
121
|
-
}
|
|
122
|
-
var real = path.join(fs.realpathSync(probe), rest);
|
|
123
|
-
if (!inside(real, folder)) {
|
|
124
|
-
throw refused((what || 'The path') + ' "' + p + '" is outside this company\'s folder. Name it inside, e.g. bank/inbox/statement.csv.');
|
|
125
|
-
}
|
|
126
|
-
return real;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** The name a path has inside the tenant's folder — what a screen shows. */
|
|
130
|
-
function nameOf(real) {
|
|
131
|
-
var folder = tenantFolder();
|
|
132
|
-
return inside(real, folder) ? path.relative(folder, real) : null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
module.exports = { VAR: VAR, requiredEnv: requiredEnv, ALL: ALL, root: root, tenantFolder: tenantFolder, scratch: scratch, resolve: resolve, nameOf: nameOf };
|
|
1
|
+
// WHERE A STEP'S FILES ARE — @xeplr/db's dataDir: one root (XEPLR_DATA_DIR),
|
|
2
|
+
// a folder per tenant (<root>/<app>/<mtId1>/…), the tenant from the context
|
|
3
|
+
// the work runs in, never from input. One copy, in @xeplr/db beside the
|
|
4
|
+
// tenant context, so uploads, imports and factory use the same one. See its
|
|
5
|
+
// README ("Files: a folder per tenant").
|
|
6
|
+
module.exports = require('@xeplr/db').dataDir;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeplr/actions",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Named-action registry with typed inputs and safe invocation — plus built-in actions for HTTP, DB, files, email, spawn",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@duckdb/node-api": "1.5.5-r.3",
|
|
43
|
-
"@xeplr/db": "^1.0
|
|
43
|
+
"@xeplr/db": "^1.1.0",
|
|
44
44
|
"@xeplr/email": "^1.0.0",
|
|
45
45
|
"@xeplr/utils": "^1.0.0",
|
|
46
46
|
"csv-parse": "^7.0.2",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
},
|
|
122
122
|
"devDependencies": {
|
|
123
123
|
"@duckdb/node-api": "^1.5.5-r.3",
|
|
124
|
-
"@xeplr/db": "^1.0
|
|
124
|
+
"@xeplr/db": "^1.1.0",
|
|
125
125
|
"@xeplr/utils": "^1.0.0",
|
|
126
126
|
"csv-parse": "^7.0.2",
|
|
127
127
|
"dejavu-fonts-ttf": "^2.37.3",
|