datagrok-tools 6.5.9 → 6.6.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/CHANGELOG.md +8 -0
- package/CLAUDE.md +3 -1
- package/README.md +5 -0
- package/bin/_deprecated/upload.js +4 -2
- package/bin/commands/help.js +10 -3
- package/bin/commands/publish.js +8 -6
- package/bin/utils/dev-key.js +31 -0
- package/bin/utils/migrate/registry.js +17 -2
- package/bin/utils/migrate/registry.ts +17 -1
- package/bin/utils/node-dapi.js +9 -1
- package/bin/utils/test-utils.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Datagrok-tools changelog
|
|
2
2
|
|
|
3
|
+
## 6.6.0 (WIP)
|
|
4
|
+
|
|
5
|
+
* The developer key is sent in the `Authorization` header (`Dev <key>`) instead of the URL path. `POST /users/login/dev/<key>` and `POST /packages/dev/<key>/<package>` put a long-lived credential into every nginx access log, proxy log and shell history along the way; the key-less routes (`/users/login/dev`, `/packages/dev/<package>`) take it as a header. Servers that predate the header form answer 404/401 and the old URL is used instead, so publishing to an older server still works - but a server from 1.28 on rejects the URL form and asks for datagrok-tools 6.6.0 or later.
|
|
6
|
+
|
|
7
|
+
## 6.5.10 (2026-09-11)
|
|
8
|
+
|
|
9
|
+
* `grok s pull` — a bundle file name is capped at 200 bytes plus a digest of the full name, so it stays unique and stable across pulls. A nested view under a long space spells a longer name than a path component may hold (255 bytes on ext4 and NTFS): one TWIG snapshot reached 268 and the write failed with `ENAMETOOLONG`, taking down the whole part rather than the entity — a `--by-namespace` part that only touched the space as a dependency died with it.
|
|
10
|
+
|
|
3
11
|
## 6.5.9 (2026-09-11)
|
|
4
12
|
|
|
5
13
|
* `grok s push/migrate` — placement no longer claims an entity the walk refused. Containment is exclusive, so asserting a project's relation to a platform entity takes it away from whatever holds it on the target: migrating a dashboard built on demo data moved `System:DemoFiles` into the migrated space, and every reference to it by name broke stand-wide until its `System` row was restored by hand. The guard covered a space's own `Files` connection but not `System:` connections, personal `Home` shares, package projects or built-in groups; it now defers to the same `untransferableReason` the walk uses.
|
package/CLAUDE.md
CHANGED
|
@@ -94,7 +94,9 @@ The `publish` command executes these steps:
|
|
|
94
94
|
3. Process environment variables in `/connections/*.json` files (replace `${VAR}`)
|
|
95
95
|
4. **Process Docker images** (see below)
|
|
96
96
|
5. Create ZIP archive with archiver-promise (includes `image.json` metadata per container)
|
|
97
|
-
6. Upload to server: `POST ${host}/packages/dev/${
|
|
97
|
+
6. Upload to server: `POST ${host}/packages/dev/${packageName}` with `Authorization: Dev ${devKey}`
|
|
98
|
+
(older servers only know `POST ${host}/packages/dev/${devKey}/${packageName}`; `devKeyFetch` in
|
|
99
|
+
`bin/utils/dev-key.ts` falls back to it on 404/401)
|
|
98
100
|
|
|
99
101
|
**Key flags:**
|
|
100
102
|
- `--debug` (default) - Package visible only to developer
|
package/README.md
CHANGED
|
@@ -100,6 +100,11 @@ For more information on configuring connections, refer to the [Connections](http
|
|
|
100
100
|
- `--suffix`: a string containing package version hash
|
|
101
101
|
|
|
102
102
|
Running `grok publish` is the same as running `grok publish defaultHost --build --debug`.
|
|
103
|
+
- `report` works with user error reports on a Datagrok instance (the instance is a server alias from
|
|
104
|
+
`config.yaml`): `grok report fetch <instance> <number>` downloads the report zip, `grok report read
|
|
105
|
+
<zip|json|instance number>` prints one normalized JSON object (flags `--extract-screenshot`,
|
|
106
|
+
`--extract-d42`, `--extract-client-log`), `grok report resolve <instance> <number>` marks it resolved.
|
|
107
|
+
`ticket`, `comment`, `label`, `attach` talk to JIRA with `JIRA_TOKEN`. See `grok report --help`.
|
|
103
108
|
- `check` checks package content (function signatures, import statements of external modules,
|
|
104
109
|
etc.). The check is also run during package publication.
|
|
105
110
|
- `init` modifies a package template by adding config files for linters, IDE, and so on
|
|
@@ -72,7 +72,8 @@ async function processPackage() {
|
|
|
72
72
|
let timestamps = {};
|
|
73
73
|
if (debug) {
|
|
74
74
|
try {
|
|
75
|
-
timestamps = await (await fetch(`${host}/packages/dev/${
|
|
75
|
+
timestamps = await (await fetch(`${host}/packages/dev/${packageName}/timestamps`,
|
|
76
|
+
{headers: {Authorization: `Dev ${devKey}`}})).json();
|
|
76
77
|
if (timestamps['#type'] === 'ApiError') {
|
|
77
78
|
console.log(timestamps.message);
|
|
78
79
|
return 1;
|
|
@@ -134,8 +135,9 @@ async function processPackage() {
|
|
|
134
135
|
|
|
135
136
|
//upload
|
|
136
137
|
const uploadPromise = new Promise((resolve, reject) => {
|
|
137
|
-
fetch(`${host}/packages/dev/${
|
|
138
|
+
fetch(`${host}/packages/dev/${packageName}?debug=${debug.toString()}&rebuild=${rebuild.toString()}`, {
|
|
138
139
|
method: 'POST',
|
|
140
|
+
headers: {Authorization: `Dev ${devKey}`},
|
|
139
141
|
body: zip,
|
|
140
142
|
}).then((body) => body.json()).then((j) => resolve(j)).catch((err) => {
|
|
141
143
|
reject(err);
|
package/bin/commands/help.js
CHANGED
|
@@ -371,10 +371,17 @@ Usage: grok report <subcommand> [args]
|
|
|
371
371
|
Manage Datagrok user error reports
|
|
372
372
|
|
|
373
373
|
Subcommands:
|
|
374
|
-
fetch Download a report zip from a managed instance
|
|
374
|
+
fetch Download a report zip from a managed instance (writes <stem>_meta.json next to it)
|
|
375
375
|
read Normalize a report (zip or json) into one JSON object on stdout
|
|
376
|
-
resolve Mark a report as resolved
|
|
377
|
-
ticket Create a JIRA ticket for a report
|
|
376
|
+
resolve Mark a report as resolved (needs the _meta.json written by fetch)
|
|
377
|
+
ticket Create a JIRA ticket for a report directly in JIRA (no dedup; the key is NOT
|
|
378
|
+
written back to the report — prefer POST /reports/{id}/jira for that)
|
|
379
|
+
comment Add a comment to a JIRA ticket (--body <text> | --body-file <path>)
|
|
380
|
+
label Add labels to a JIRA ticket
|
|
381
|
+
attach Attach a file to a JIRA ticket
|
|
382
|
+
|
|
383
|
+
JIRA subcommands need JIRA_TOKEN (plus JIRA_USER for a user API token); ticket also needs
|
|
384
|
+
--project or $JIRA_PROJECT.
|
|
378
385
|
|
|
379
386
|
Read flags:
|
|
380
387
|
--extract-screenshot <path> Write the screenshot binary to <path>
|
package/bin/commands/publish.js
CHANGED
|
@@ -20,6 +20,7 @@ var color = _interopRequireWildcard(require("../utils/color-utils"));
|
|
|
20
20
|
var _check = require("./check");
|
|
21
21
|
var _pythonCeleryGen = require("../utils/python-celery-gen");
|
|
22
22
|
var _queueWorkerGen = require("../utils/queue-worker-gen");
|
|
23
|
+
var _devKey = require("../utils/dev-key");
|
|
23
24
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
24
25
|
// @ts-ignore
|
|
25
26
|
|
|
@@ -243,7 +244,7 @@ function listRecursive(basePath, rel) {
|
|
|
243
244
|
async function getUserLogin(host, devKey) {
|
|
244
245
|
let loginResp;
|
|
245
246
|
try {
|
|
246
|
-
loginResp = await (0,
|
|
247
|
+
loginResp = await (0, _devKey.devKeyFetch)(`${host}/users/login/dev`, `${host}/users/login/dev/${devKey}`, devKey, {
|
|
247
248
|
method: 'POST'
|
|
248
249
|
});
|
|
249
250
|
} catch (e) {
|
|
@@ -485,9 +486,10 @@ async function fallbackImage(img, host, devKey, registry, version, contentHash)
|
|
|
485
486
|
async function processPackage(debug, rebuild, host, devKey, packageName, dropDb, suffix, hostAlias, registry, rebuildDocker, skipDockerRebuild) {
|
|
486
487
|
// Validate server connectivity and dev key
|
|
487
488
|
let timestamps = {};
|
|
488
|
-
|
|
489
|
+
const url = `${host}/packages/dev/${packageName}`;
|
|
490
|
+
const legacyUrl = `${host}/packages/dev/${devKey}/${packageName}`;
|
|
489
491
|
try {
|
|
490
|
-
const checkResp = await (0,
|
|
492
|
+
const checkResp = await (0, _devKey.devKeyFetch)(`${url}/timestamps`, `${legacyUrl}/timestamps`, devKey);
|
|
491
493
|
const checkData = await checkResp.json();
|
|
492
494
|
if (checkData['#type'] === 'ApiError') {
|
|
493
495
|
color.error(checkData.message);
|
|
@@ -614,12 +616,12 @@ async function processPackage(debug, rebuild, host, devKey, packageName, dropDb,
|
|
|
614
616
|
});
|
|
615
617
|
|
|
616
618
|
// Upload
|
|
617
|
-
|
|
618
|
-
if (suffix)
|
|
619
|
+
let query = `?debug=${debug.toString()}&rebuild=${rebuild.toString()}&dropDb=${(dropDb ?? false).toString()}`;
|
|
620
|
+
if (suffix) query += `&suffix=${suffix.toString()}`;
|
|
619
621
|
await zip.finalize();
|
|
620
622
|
const zipBuffer = Buffer.concat(chunks);
|
|
621
623
|
try {
|
|
622
|
-
const body = await (0,
|
|
624
|
+
const body = await (0, _devKey.devKeyFetch)(url + query, legacyUrl + query, devKey, {
|
|
623
625
|
method: 'POST',
|
|
624
626
|
body: zipBuffer
|
|
625
627
|
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.devKeyFetch = devKeyFetch;
|
|
7
|
+
exports.devKeyHeaders = devKeyHeaders;
|
|
8
|
+
const fetch = require('node-fetch');
|
|
9
|
+
|
|
10
|
+
/** `Authorization` header carrying the developer key. */
|
|
11
|
+
function devKeyHeaders(key, headers = {}) {
|
|
12
|
+
return {
|
|
13
|
+
...headers,
|
|
14
|
+
'Authorization': `Dev ${key}`
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Calls [url] with the developer key in the `Authorization` header, falling back to
|
|
20
|
+
* [legacyUrl] - which carries the key as a path segment - for servers that predate the
|
|
21
|
+
* header form. Those answer 404 (no such route) or 401 (the route-less path is not on
|
|
22
|
+
* their anonymous allow-list); a server that knows the header form answers neither.
|
|
23
|
+
*/
|
|
24
|
+
async function devKeyFetch(url, legacyUrl, key, init = {}) {
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
...init,
|
|
27
|
+
headers: devKeyHeaders(key, init.headers)
|
|
28
|
+
});
|
|
29
|
+
if (response.status !== 404 && response.status !== 401) return response;
|
|
30
|
+
return await fetch(legacyUrl, init);
|
|
31
|
+
}
|
|
@@ -16,6 +16,7 @@ exports.resolveTypes = resolveTypes;
|
|
|
16
16
|
exports.stripCredentials = stripCredentials;
|
|
17
17
|
exports.typeOptionsOf = typeOptionsOf;
|
|
18
18
|
exports.untransferableReason = untransferableReason;
|
|
19
|
+
var _crypto = require("crypto");
|
|
19
20
|
/// Docs: [Entity export / import](/docs/features/grok-tool/export-import/DESIGN.md)
|
|
20
21
|
|
|
21
22
|
/** Extra listing rules a `--type` alias adds on top of its entity type. */
|
|
@@ -344,6 +345,20 @@ function fileNameFor(json) {
|
|
|
344
345
|
const cleaned = String(nqName).replace(/[:/\\?*"<>|]+/g, '.').replace(/^\.+|\.+$/g, '');
|
|
345
346
|
// Windows resolves `NUL.json` (and `COM1.csv.json`) to a device, whatever the extension.
|
|
346
347
|
const head = cleaned.split('.')[0];
|
|
347
|
-
return RESERVED_RE.test(head) ? `${head}-${json.id ?? 'entity'}${cleaned.slice(head.length)}` : cleaned;
|
|
348
|
+
return shorten(RESERVED_RE.test(head) ? `${head}-${json.id ?? 'entity'}${cleaned.slice(head.length)}` : cleaned);
|
|
348
349
|
}
|
|
349
|
-
const RESERVED_RE = /^(NUL|CON|PRN|AUX|COM[1-9]|LPT[1-9])$/i;
|
|
350
|
+
const RESERVED_RE = /^(NUL|CON|PRN|AUX|COM[1-9]|LPT[1-9])$/i;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* A path component caps at 255 bytes on ext4 and NTFS, and a nested view under a long space
|
|
354
|
+
* reaches that: one TWIG snapshot spelled 268. The write is what fails, so the whole part dies
|
|
355
|
+
* rather than the entity. The digest keeps a truncated name unique and stable across pulls.
|
|
356
|
+
*/
|
|
357
|
+
const MAX_FILE_NAME = 200;
|
|
358
|
+
function shorten(name) {
|
|
359
|
+
if (Buffer.byteLength(name) <= MAX_FILE_NAME) return name;
|
|
360
|
+
const digest = (0, _crypto.createHash)('sha1').update(name).digest('hex').slice(0, 8);
|
|
361
|
+
let head = name;
|
|
362
|
+
while (Buffer.byteLength(head) > MAX_FILE_NAME - 9) head = head.slice(0, -1);
|
|
363
|
+
return `${head}-${digest}`;
|
|
364
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// Docs: [Entity export / import](/docs/features/grok-tool/export-import/DESIGN.md)
|
|
2
|
+
import {createHash} from 'crypto';
|
|
2
3
|
|
|
3
4
|
export type BytesKind = 'tables' | 'files';
|
|
4
5
|
|
|
@@ -249,7 +250,22 @@ export function fileNameFor(json: any): string {
|
|
|
249
250
|
const cleaned = String(nqName).replace(/[:/\\?*"<>|]+/g, '.').replace(/^\.+|\.+$/g, '');
|
|
250
251
|
// Windows resolves `NUL.json` (and `COM1.csv.json`) to a device, whatever the extension.
|
|
251
252
|
const head = cleaned.split('.')[0];
|
|
252
|
-
return RESERVED_RE.test(head) ? `${head}-${json.id ?? 'entity'}${cleaned.slice(head.length)}` : cleaned;
|
|
253
|
+
return shorten(RESERVED_RE.test(head) ? `${head}-${json.id ?? 'entity'}${cleaned.slice(head.length)}` : cleaned);
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
const RESERVED_RE = /^(NUL|CON|PRN|AUX|COM[1-9]|LPT[1-9])$/i;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* A path component caps at 255 bytes on ext4 and NTFS, and a nested view under a long space
|
|
260
|
+
* reaches that: one TWIG snapshot spelled 268. The write is what fails, so the whole part dies
|
|
261
|
+
* rather than the entity. The digest keeps a truncated name unique and stable across pulls.
|
|
262
|
+
*/
|
|
263
|
+
const MAX_FILE_NAME = 200;
|
|
264
|
+
|
|
265
|
+
function shorten(name: string): string {
|
|
266
|
+
if (Buffer.byteLength(name) <= MAX_FILE_NAME) return name;
|
|
267
|
+
const digest = createHash('sha1').update(name).digest('hex').slice(0, 8);
|
|
268
|
+
let head = name;
|
|
269
|
+
while (Buffer.byteLength(head) > MAX_FILE_NAME - 9) head = head.slice(0, -1);
|
|
270
|
+
return `${head}-${digest}`;
|
|
271
|
+
}
|
package/bin/utils/node-dapi.js
CHANGED
|
@@ -68,7 +68,15 @@ class NodeApiClient {
|
|
|
68
68
|
this.devKey = devKey;
|
|
69
69
|
}
|
|
70
70
|
static async login(baseUrl, devKey) {
|
|
71
|
-
|
|
71
|
+
// Servers before 1.28 only knew the key-in-URL form, where it leaked into every
|
|
72
|
+
// access log on the way; they answer 404 or 401 to the key-less route.
|
|
73
|
+
let res = await fetch(`${baseUrl}/users/login/dev`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'Authorization': `Dev ${devKey}`
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
if (res.status === 404 || res.status === 401) res = await fetch(`${baseUrl}/users/login/dev/${devKey}`, {
|
|
72
80
|
method: 'POST'
|
|
73
81
|
});
|
|
74
82
|
const json = (res.headers.get('content-type') ?? '').includes('application/json') ? await res.json() : null;
|
package/bin/utils/test-utils.js
CHANGED
|
@@ -35,6 +35,7 @@ var utils = _utils;
|
|
|
35
35
|
var _puppeteer = _interopRequireDefault(require("puppeteer"));
|
|
36
36
|
var color = _interopRequireWildcard(require("../utils/color-utils"));
|
|
37
37
|
var _papaparse = _interopRequireDefault(require("papaparse"));
|
|
38
|
+
var _devKey = require("./dev-key");
|
|
38
39
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
39
40
|
const fetch = require('node-fetch');
|
|
40
41
|
const grokDir = _path.default.join(_os.default.homedir(), '.grok');
|
|
@@ -57,7 +58,7 @@ async function getToken(url, key) {
|
|
|
57
58
|
let lastError;
|
|
58
59
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
59
60
|
try {
|
|
60
|
-
const response = await
|
|
61
|
+
const response = await (0, _devKey.devKeyFetch)(`${url}/users/login/dev`, `${url}/users/login/dev/${key}`, key, {
|
|
61
62
|
method: 'POST'
|
|
62
63
|
});
|
|
63
64
|
const text = await response.text();
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "git",
|
|
5
5
|
"url": "https://github.com/datagrok-ai/public.git"
|
|
6
6
|
},
|
|
7
|
-
"version": "6.
|
|
7
|
+
"version": "6.6.0",
|
|
8
8
|
"description": "Utility to upload and publish packages to Datagrok",
|
|
9
9
|
"homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
|
|
10
10
|
"dependencies": {
|