@quatronic/qore 0.1.4 → 0.1.6

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
@@ -46,7 +46,26 @@ the portal. Working from a personal fork whose `origin` the portal doesn't
46
46
  know? As long as an `upstream` remote points at the real repository, that one
47
47
  resolves — the context line then says `(upstream)`.
48
48
 
49
- Pass `--module <slug|id>` to pick a module explicitly; the flag always wins
49
+ Outside a repository, with a projects folder set (see `qore init`), the
50
+ order is: the project you are standing in; else `[project]` /
51
+ `--project <ref>` (`app`, `app/module`, `tenant/app` or `tenant/app/module`,
52
+ matched against your local checkouts); else an interactive pick. `qore dev`
53
+ takes the reference as an argument (`qore dev <app>`); `qore dev`, `qore env
54
+ pull`, `qore run` and `qore status` take `--project`. `qore run --project`
55
+ only chooses which module's secrets are used; the command still runs in the
56
+ current directory. `qore env pull --project` writes into the located module,
57
+ and a relative `--target` then resolves against that project directory, not
58
+ the directory you ran it from.
59
+
60
+ Logged out with a projects folder set, the local lookup still runs first: a
61
+ folder that holds no projects gives `QORE_PROJECT_NOT_FOUND`; otherwise the
62
+ project is located exactly as when logged in (a match, a picker, or
63
+ `QORE_CONTEXT_REQUIRED` without a terminal), and the login error comes from
64
+ the portal lookup that follows. Only without a projects folder does a logged-out
65
+ command outside a project go straight to the login error.
66
+
67
+ Pass `--module <slug|id>` to pick a module explicitly (it keeps its meaning and,
68
+ like `--app`, still uses the portal lookup when you are not in a project); the flag always wins
50
69
  over the git remote. Inside a repository, `qore` still looks up the remote's
51
70
  own module (from the cache, or with one extra request) only so the context
52
71
  line can tell you when the flag overrides it. If a module is linked to more
@@ -92,7 +111,9 @@ qore env pull
92
111
 
93
112
  - The target file defaults to the module type's own convention (e.g.
94
113
  `.env.local`), or `.qore.json`'s `envFile`, or `--target <path>` — always
95
- resolved against, and required to stay inside, the repository.
114
+ required to stay inside the repository. `envFile` resolves against the
115
+ repository root; `--target` against the current directory, or with
116
+ `--project` against the located project directory.
96
117
  - If the file doesn't exist yet, it's seeded from `.env.example` when one is
97
118
  present.
98
119
  - Existing values you've hand-edited are kept by default. A key the portal
@@ -301,6 +322,39 @@ If a repository resolves to no module on any portal you're logged in to,
301
322
  the error lists the portals it tried and, when a staging repository looks
302
323
  likely, suggests `qore login --portal staging`.
303
324
 
325
+ ### Projects folder & `qore init`
326
+
327
+ `qore init` is the guided setup. It runs four steps and reports each as
328
+ `done`, `already` or `skipped`, so running it again is safe:
329
+
330
+ 1. **Projects folder**: where your apps live (default `~/qore`; `--root <path>`
331
+ sets it without asking). Nothing existing is moved.
332
+ 2. **Login**: skipped when you are already logged in, and with `--no-input`.
333
+ 3. **Tenant**: `already` when a tenant is active (`done` right after the login
334
+ step logged you in). Otherwise skipped with
335
+ `--no-input` or when you are not logged in (run `qore tenant use <slug>`
336
+ later), skipped with no tenants, selected without a prompt when you have
337
+ one, a picker when you have several.
338
+ 4. **First app**: `--clone <app>` (with `--tenant <slug>` to disambiguate)
339
+ clones it; interactively it asks.
340
+
341
+ With `--json`, stdout is one document: `{"projectsRoot": "<path>|null",
342
+ "steps": {"root"|"login"|"tenant"|"clone": {"status", "detail?"}}}`.
343
+
344
+ Apps clone into `<root>/<tenant>/<app>/<module>`. A checkout outside the
345
+ folder registers itself the first time you use it (`qore clone <app> <dir>`,
346
+ `qore dev`, ...); a registered checkout that was deleted is dropped. With a
347
+ projects folder set, `qore dev`, `qore env pull`, `qore run` and `qore status`
348
+ work from any directory (see Repository context).
349
+
350
+ ```sh
351
+ qore config root # show the folder and how many projects it holds
352
+ qore config root <path> # set it
353
+ qore config root --unset # clear it; known projects stay findable
354
+ ```
355
+
356
+ `qore doctor` checks that the folder exists and is writable.
357
+
304
358
  ### Workspaces
305
359
 
306
360
  `qore clone` sets up a local directory holding one or more of an app's
@@ -319,6 +373,9 @@ qore sync # clone any module that's new, fetch the rest
319
373
  qore sync --pull # ...and fast-forward every clean repository with an upstream
320
374
  ```
321
375
 
376
+ - **Where it clones.** Without `[dir]` and with a projects folder set, `qore
377
+ clone` uses `<root>/<tenant>/<app>`. Without a projects folder it behaves as
378
+ described above and prints a tip to run `qore init`.
322
379
  - **`--protocol https|ssh`** picks the clone URL (default `https`). `ssh`
323
380
  rewrites `https://github.com/o/r` to `git@github.com:o/r.git`; your own
324
381
  git `insteadOf` config still applies on top of either.
@@ -485,8 +542,8 @@ qore whoami --json
485
542
  | Code | Meaning |
486
543
  |------|---------|
487
544
  | 0 | Success |
488
- | 1 | Internal error, `git` is not installed or not on `PATH`, the OS keychain is unavailable/access was denied, or qore itself needs a newer Node (`QORE_NODE_VERSION`, checked by `qore doctor`'s own `node` check against qore's own `process.version`); `qore dev`: its package manager isn't installed (`QORE_PACKAGE_MANAGER`), its install failed (`QORE_INSTALL_FAILED`), Docker is missing (`QORE_DOCKER_MISSING`), not running (`QORE_DOCKER_NOT_RUNNING`) or its compose v2 plugin is missing (`QORE_DOCKER_COMPOSE_MISSING`), a `db:up`/service failed, never became healthy (`QORE_SERVICE_UNHEALTHY`) or its data is incompatible (`QORE_SERVICE_DATA_INCOMPATIBLE`), or the repository's own `engines.node` doesn't match the `node` on `PATH` (`QORE_NODE_VERSION`, a different check from qore's own — `qore doctor`'s `node:<module>` check, run only in a trusted repository) |
489
- | 2 | Missing or invalid context (e.g. a bad `--portal` value, or no module selected), or the module is part of several apps and no `--app` was given; `qore dev`: a port is already in use (`QORE_PORT_IN_USE`) or the repository's dev setup isn't supported (`QORE_DEV_UNSUPPORTED`) |
545
+ | 1 | Internal error, `git` is not installed or not on `PATH`, the OS keychain is unavailable/access was denied, or qore itself needs a newer Node (`QORE_NODE_VERSION`, checked by `qore doctor`'s own `node` check against qore's own `process.version`); `qore dev`: its package manager isn't installed (`QORE_PACKAGE_MANAGER`), its install failed (`QORE_INSTALL_FAILED`), Docker is missing (`QORE_DOCKER_MISSING`), not running (`QORE_DOCKER_NOT_RUNNING`) or its compose v2 plugin is missing (`QORE_DOCKER_COMPOSE_MISSING`), a `db:up`/service failed, never became healthy (`QORE_SERVICE_UNHEALTHY`) or its data is incompatible (`QORE_SERVICE_DATA_INCOMPATIBLE`), or the repository's own `engines.node` doesn't match the `node` on `PATH` (`QORE_NODE_VERSION`, a different check from qore's own — `qore doctor`'s `node:<module>` check, run only in a trusted repository); or the projects folder isn't writable (`QORE_ROOT_NOT_WRITABLE`, fix its permissions or choose another folder) |
546
+ | 2 | Missing or invalid context (e.g. a bad `--portal` value, or no module selected), or the module is part of several apps and no `--app` was given; `qore dev`: a port is already in use (`QORE_PORT_IN_USE`) or the repository's dev setup isn't supported (`QORE_DEV_UNSUPPORTED`); projects folder: not set and not inside a project, for a logged-in user (`QORE_ROOT_NOT_SET`, run `qore init`; logged out with no folder you get the login error, exit 4, and logged out with a folder the local lookup runs first, so an empty folder still gives `QORE_PROJECT_NOT_FOUND`), not usable as a folder (`QORE_ROOT_INVALID`, pick another with `qore init --root <path>`), or no local project matches the reference or the folder holds none (`QORE_PROJECT_NOT_FOUND`, run `qore clone <app>`) |
490
547
  | 4 | Not logged in, session/login expired, login denied, or clock skew |
491
548
  | 5 | Access denied — lost tenant access, resource not found, no developer access |
492
549
  | 6 | Auth or API temporarily unreachable/unavailable |
@@ -511,6 +568,38 @@ and never in an environment variable.
511
568
  - `NODE_EXTRA_CA_CERTS` can point at an additional CA bundle, for portals or
512
569
  identity providers behind a corporate proxy with a private root CA.
513
570
 
571
+ ## Privacy
572
+
573
+ `qore` sends crash reports to Quatronic so we can find and fix bugs. It is on by
574
+ default; the first interactive run says so once, on stderr.
575
+
576
+ **What is sent**, to Sentry (EU region, project `qore-cli`):
577
+
578
+ - errors qore did not anticipate — a bug in qore, not an expected error such as
579
+ "not logged in" or "port in use";
580
+ - one health record per run: whether it crashed or not;
581
+ - with each: the qore version, the Node version, the OS, whether it ran in CI, the
582
+ command name (e.g. `env pull`), the stack trace, and the error message;
583
+ - the method, host and status code of the HTTP requests qore made before a crash.
584
+
585
+ **Removed before sending:** tokens, keys and other secrets, environment variable
586
+ values (including the local env qore fetched), argument values, e-mail addresses,
587
+ URL query strings, and your home directory (paths show as `~`). This is pattern-based:
588
+ an error message about a file qore failed to parse can still quote a fragment of it.
589
+
590
+ **Never attached:** your hostname, your user account, your tenant, or an IP address.
591
+ qore sends no field that carries an IP; the address a report arrives from is dropped
592
+ by the Sentry project's "Prevent Storing of IP Addresses" setting, part of its infra setup.
593
+
594
+ **Turning it off** (the first match wins):
595
+
596
+ 1. `DO_NOT_TRACK=1`
597
+ 2. `QORE_TELEMETRY=0` (or `QORE_TELEMETRY=1` to turn it on)
598
+ 3. `qore config telemetry off` (or `on`)
599
+
600
+ `qore config telemetry` and `qore doctor` show the current state and where it
601
+ comes from. A build from source or a fork carries no DSN and sends nothing.
602
+
514
603
  ## Releasing
515
604
 
516
605
  See [RELEASING.md](./RELEASING.md).
@@ -0,0 +1,140 @@
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]="885f6de1-0da7-54a5-86df-beaf550bdd8a")}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
+ var PKG = "node_modules/@quatronic/qore/";
56
+ var NM = "node_modules/";
57
+ function rewritePath(p, ctx) {
58
+ if (p.startsWith("app://") || !/^(?:file:\/\/|\/|[A-Za-z]:[\\/])/.test(p)) return { path: p };
59
+ let s = p.replace(/^file:\/\//, "");
60
+ if (ctx.platform === "win32") s = s.replace(/\\/g, "/").replace(/^\/(?=[A-Za-z]:)/, "");
61
+ const pkg = s.lastIndexOf(PKG);
62
+ const nm = s.lastIndexOf(NM);
63
+ if (pkg >= 0 && pkg === nm) return { path: `app:///${s.slice(pkg + PKG.length)}`, inApp: true };
64
+ if (nm >= 0) return { path: `app:///${s.slice(nm)}`, inApp: false };
65
+ return { path: `app:///${replaceHome(s, ctx).replace(/^\/+/, "")}` };
66
+ }
67
+ function framePath(p, ctx) {
68
+ return rewritePath(p, ctx).path;
69
+ }
70
+ function scrubText(s, ctx) {
71
+ let out = s;
72
+ for (const v of ctx.secretEnvValues) out = replaceLiteral(out, v, "[env]", "g");
73
+ for (const v of ctx.argvValues) out = replaceLiteral(out, v, "[arg]", "g");
74
+ out = replaceHome(out, ctx);
75
+ for (const [re, repl] of PATTERNS) out = out.replace(re, repl);
76
+ return out;
77
+ }
78
+ function hostOf(url) {
79
+ if (typeof url !== "string") return void 0;
80
+ try {
81
+ return new URL(url).host;
82
+ } catch {
83
+ return void 0;
84
+ }
85
+ }
86
+ function scrubBreadcrumb(b, ctx) {
87
+ if (b.category === "console") return null;
88
+ if (b.type === "http" || b.category === "http" || b.category === "fetch") {
89
+ const d = b.data ?? {};
90
+ return {
91
+ type: b.type,
92
+ category: b.category,
93
+ level: b.level,
94
+ timestamp: b.timestamp,
95
+ data: { method: d.method ?? d["http.method"], host: hostOf(d.url) ?? d.host, status_code: d.status_code }
96
+ };
97
+ }
98
+ return { ...b, message: b.message === void 0 ? void 0 : scrubText(b.message, ctx), data: void 0 };
99
+ }
100
+ function scrubEvent(e, ctx) {
101
+ e.user = { ip_address: null };
102
+ delete e.server_name;
103
+ delete e.request;
104
+ delete e.extra;
105
+ delete e.modules;
106
+ for (const k of Object.keys(e.contexts ?? {})) if (!KEEP_CONTEXTS.has(k)) delete e.contexts[k];
107
+ if (e.message !== void 0) e.message = scrubText(e.message, ctx);
108
+ if (e.logentry?.message !== void 0) {
109
+ e.logentry.message = scrubText(e.logentry.message, ctx);
110
+ delete e.logentry.params;
111
+ }
112
+ for (const v of e.exception?.values ?? []) {
113
+ if (v.value !== void 0) v.value = scrubText(v.value, ctx);
114
+ for (const f of v.stacktrace?.frames ?? []) {
115
+ for (const k of ["filename", "abs_path"]) {
116
+ if (!f[k]) continue;
117
+ const r = rewritePath(f[k], ctx);
118
+ f[k] = r.path;
119
+ if (r.inApp !== void 0) f.in_app = r.inApp;
120
+ }
121
+ if (f.module) f.module = replaceHome(f.module, ctx);
122
+ delete f.vars;
123
+ delete f.pre_context;
124
+ delete f.context_line;
125
+ delete f.post_context;
126
+ }
127
+ }
128
+ for (const img of e.debug_meta?.images ?? []) if (img.code_file) img.code_file = framePath(img.code_file, ctx);
129
+ if (e.breadcrumbs) e.breadcrumbs = e.breadcrumbs.map((b) => scrubBreadcrumb(b, ctx)).filter((b) => b !== null);
130
+ return e;
131
+ }
132
+
133
+ export {
134
+ buildScrubContext,
135
+ addSecretValues,
136
+ scrubBreadcrumb,
137
+ scrubEvent
138
+ };
139
+ //# sourceMappingURL=chunk-OAU6HJHZ.js.map
140
+ //# debugId=885f6de1-0da7-54a5-86df-beaf550bdd8a