@quatronic/qore 0.1.3 → 0.1.5

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 CHANGED
@@ -428,6 +428,26 @@ qore dev --kill-others # the first module to exit stops the rest
428
428
  module that would start is already trusted and has an env file on disk;
429
429
  one exception fails the whole run with the original error.
430
430
 
431
+ **Feedback.** Before anything starts, a module not yet cloned is offered as
432
+ `Clone <module>, <module> now with qore sync? [Y/n]` (a TTY only, never
433
+ with `--json`/`--no-input`; `--yes` does not accept it). A module sync
434
+ cannot clone (a conflicting directory, no repository yet, an unsupported
435
+ repository URL, GitHub not linked, no repository access) gets its own
436
+ warning naming the real next step, and no prompt. While preflight runs, one progress line per
437
+ module per step (package manager, install, env, ports) is printed as it
438
+ completes. Once every module with a port is either listening or has been
439
+ reported as not running, a one-line summary follows: `✔ All modules
440
+ running:` (or `✔ Running:` / `! Not running:` when some were skipped) lists
441
+ each module with its URL or its reason. A module that crashes while others
442
+ keep running gets a restart hint
443
+ (`Restart it on its own: qore dev --only <module>`, in another terminal;
444
+ the others keep running); on shutdown, a final
445
+ line lists what stopped cleanly and what crashed, with how long it ran.
446
+ When the failure is about the developer's machine rather than the app
447
+ (Docker, Node version, a port busy with something other than a previous
448
+ `qore dev`, a package manager), the error is followed by a pointer to
449
+ `qore doctor` for a full check.
450
+
431
451
  **Output.** Every line of a dev server's own stdout/stderr is prefixed per
432
452
  module and column-aligned:
433
453
 
@@ -491,6 +511,38 @@ and never in an environment variable.
491
511
  - `NODE_EXTRA_CA_CERTS` can point at an additional CA bundle, for portals or
492
512
  identity providers behind a corporate proxy with a private root CA.
493
513
 
514
+ ## Privacy
515
+
516
+ `qore` sends crash reports to Quatronic so we can find and fix bugs. It is on by
517
+ default; the first interactive run says so once, on stderr.
518
+
519
+ **What is sent**, to Sentry (EU region, project `qore-cli`):
520
+
521
+ - errors qore did not anticipate — a bug in qore, not an expected error such as
522
+ "not logged in" or "port in use";
523
+ - one health record per run: whether it crashed or not;
524
+ - with each: the qore version, the Node version, the OS, whether it ran in CI, the
525
+ command name (e.g. `env pull`), the stack trace, and the error message;
526
+ - the method, host and status code of the HTTP requests qore made before a crash.
527
+
528
+ **Removed before sending:** tokens, keys and other secrets, environment variable
529
+ values (including the local env qore fetched), argument values, e-mail addresses,
530
+ URL query strings, and your home directory (paths show as `~`). This is pattern-based:
531
+ an error message about a file qore failed to parse can still quote a fragment of it.
532
+
533
+ **Never attached:** your hostname, your user account, your tenant, or an IP address.
534
+ qore sends no field that carries an IP; the address a report arrives from is dropped
535
+ by the Sentry project's "Prevent Storing of IP Addresses" setting, part of its infra setup.
536
+
537
+ **Turning it off** (the first match wins):
538
+
539
+ 1. `DO_NOT_TRACK=1`
540
+ 2. `QORE_TELEMETRY=0` (or `QORE_TELEMETRY=1` to turn it on)
541
+ 3. `qore config telemetry off` (or `on`)
542
+
543
+ `qore config telemetry` and `qore doctor` show the current state and where it
544
+ comes from. A build from source or a fork carries no DSN and sends nothing.
545
+
494
546
  ## Releasing
495
547
 
496
548
  See [RELEASING.md](./RELEASING.md).
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a8210538-dc3e-507d-ab33-bdf6a49d6d16")}catch(e){}}();
4
+ import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
5
+
6
+ // src/core/telemetry/scrub.ts
7
+ var SECRET_ENV_KEY = /TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE|API_?KEY|ACCESS_?KEY|DSN|AUTH|CREDENTIAL|COOKIE|SESSION/i;
8
+ var KEEP_CONTEXTS = /* @__PURE__ */ new Set(["runtime", "os"]);
9
+ var PATTERNS = [
10
+ // an unterminated block runs to the end, so a truncated key is still dropped
11
+ [/-----BEGIN [A-Z0-9 ]{1,40}-----[\s\S]*?(?:-----END [A-Z0-9 ]{1,40}-----|$)/g, "[pem]"],
12
+ // query strings and fragments carry OAuth codes, states and signed-URL tokens
13
+ [/\b([a-z][a-z0-9+.-]{0,30}:\/\/[^\s?#"'<>]*)[?#][^\s"'<>]*/gi, "$1"],
14
+ [/\b(authorization["']?\s*[=:]\s*)(\[redacted\]|"[^"]*"|'[^']*'|[A-Za-z][A-Za-z0-9_-]{0,30}\s+[^\s,;&]+|[^\s,;&]+)/gi, "$1[redacted]"],
15
+ [/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]"],
16
+ [/\bBasic\s+[A-Za-z0-9+/=]{8,}/gi, "Basic [redacted]"],
17
+ [/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]*/g, "[jwt]"],
18
+ [/\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|sntr[ysu]_[A-Za-z0-9+/=_-]{20,}|npm_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[abpr]-[A-Za-z0-9-]{10,}|[sr]k_(?:live|test)_[A-Za-z0-9]{10,}|(?:AKIA|ASIA)[0-9A-Z]{16})/g, "[token]"],
19
+ // URL credentials, which is also what a DSN's public key looks like
20
+ [/\b([a-z][a-z0-9+.-]{0,30}:\/\/)[^\s/:@]+(?::[^\s/@]*)?@/gi, "$1[redacted]@"],
21
+ [/[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9-]{1,63}(?:\.[A-Za-z0-9-]{1,63}){0,8}\.[A-Za-z]{2,24}\b/g, "[email]"],
22
+ // affixes are bounded so a long run of key-like characters cannot backtrack quadratically
23
+ [/\b([A-Za-z0-9_.-]{0,40}(?:(?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key|private[_-]?key|auth|credential|dsn|session|cookie)[A-Za-z0-9_.-]{0,40}|key))(["']?\s*[=:]\s*)(\[redacted\]|"[^"]*"|'[^']*'|[^\s&,;]+)/gi, "$1$2[redacted]"]
24
+ ];
25
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26
+ var longestFirst = (xs, min) => [...new Set(xs.filter((x) => x.length >= min))].sort((a, b) => b.length - a.length);
27
+ function buildScrubContext(o) {
28
+ const words = new Set(o.commandWords);
29
+ const argvValues = o.argv.flatMap((a) => {
30
+ if (a.startsWith("-")) {
31
+ const i = a.indexOf("=");
32
+ return i > 0 ? [a.slice(i + 1)] : [];
33
+ }
34
+ return words.has(a) ? [] : [a];
35
+ });
36
+ const secretEnvValues = Object.entries(o.env).flatMap(([k, v]) => v && SECRET_ENV_KEY.test(k) ? [v] : []);
37
+ return { home: o.home, platform: o.platform, argvValues: longestFirst(argvValues, 3), secretEnvValues: longestFirst(secretEnvValues, 8) };
38
+ }
39
+ function addSecretValues(ctx, values) {
40
+ ctx.secretEnvValues = longestFirst([...ctx.secretEnvValues, ...values], 8);
41
+ }
42
+ function replaceLiteral(s, needle, repl, flags) {
43
+ return s.replace(new RegExp(escapeRe(needle), flags), () => repl);
44
+ }
45
+ function replaceHome(s, ctx) {
46
+ const win = ctx.platform === "win32";
47
+ const plain = [ctx.home, ...win ? [ctx.home.replace(/\\/g, "/")] : []];
48
+ const variants = /* @__PURE__ */ new Set([...plain, ...plain.map((h) => encodeURI(h).replace(/[?#]/g, encodeURIComponent))]);
49
+ let out = s;
50
+ for (const h of [...variants].filter((v) => v.length > 1).sort((a, b) => b.length - a.length)) {
51
+ out = out.replace(new RegExp(`${escapeRe(h)}(?![A-Za-z0-9._-])`, win ? "gi" : "g"), () => "~");
52
+ }
53
+ return out;
54
+ }
55
+ function scrubText(s, ctx) {
56
+ let out = s;
57
+ for (const v of ctx.secretEnvValues) out = replaceLiteral(out, v, "[env]", "g");
58
+ for (const v of ctx.argvValues) out = replaceLiteral(out, v, "[arg]", "g");
59
+ out = replaceHome(out, ctx);
60
+ for (const [re, repl] of PATTERNS) out = out.replace(re, repl);
61
+ return out;
62
+ }
63
+ function hostOf(url) {
64
+ if (typeof url !== "string") return void 0;
65
+ try {
66
+ return new URL(url).host;
67
+ } catch {
68
+ return void 0;
69
+ }
70
+ }
71
+ function scrubBreadcrumb(b, ctx) {
72
+ if (b.category === "console") return null;
73
+ if (b.type === "http" || b.category === "http" || b.category === "fetch") {
74
+ const d = b.data ?? {};
75
+ return {
76
+ type: b.type,
77
+ category: b.category,
78
+ level: b.level,
79
+ timestamp: b.timestamp,
80
+ data: { method: d.method ?? d["http.method"], host: hostOf(d.url) ?? d.host, status_code: d.status_code }
81
+ };
82
+ }
83
+ return { ...b, message: b.message === void 0 ? void 0 : scrubText(b.message, ctx), data: void 0 };
84
+ }
85
+ function scrubEvent(e, ctx) {
86
+ delete e.server_name;
87
+ delete e.user;
88
+ delete e.request;
89
+ delete e.extra;
90
+ delete e.modules;
91
+ for (const k of Object.keys(e.contexts ?? {})) if (!KEEP_CONTEXTS.has(k)) delete e.contexts[k];
92
+ if (e.message !== void 0) e.message = scrubText(e.message, ctx);
93
+ if (e.logentry?.message !== void 0) {
94
+ e.logentry.message = scrubText(e.logentry.message, ctx);
95
+ delete e.logentry.params;
96
+ }
97
+ for (const v of e.exception?.values ?? []) {
98
+ if (v.value !== void 0) v.value = scrubText(v.value, ctx);
99
+ for (const f of v.stacktrace?.frames ?? []) {
100
+ if (f.filename) f.filename = replaceHome(f.filename, ctx);
101
+ if (f.abs_path) f.abs_path = replaceHome(f.abs_path, ctx);
102
+ if (f.module) f.module = replaceHome(f.module, ctx);
103
+ delete f.vars;
104
+ delete f.pre_context;
105
+ delete f.context_line;
106
+ delete f.post_context;
107
+ }
108
+ }
109
+ for (const img of e.debug_meta?.images ?? []) if (img.code_file) img.code_file = replaceHome(img.code_file, ctx);
110
+ if (e.breadcrumbs) e.breadcrumbs = e.breadcrumbs.map((b) => scrubBreadcrumb(b, ctx)).filter((b) => b !== null);
111
+ return e;
112
+ }
113
+
114
+ export {
115
+ buildScrubContext,
116
+ addSecretValues,
117
+ scrubBreadcrumb,
118
+ scrubEvent
119
+ };
120
+ //# sourceMappingURL=chunk-QYBDINUC.js.map
121
+ //# debugId=a8210538-dc3e-507d-ab33-bdf6a49d6d16