@levr-one/auth 1.1.2 → 1.1.3

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.
Files changed (37) hide show
  1. package/dist/connection-error.d.ts +24 -0
  2. package/dist/connection-error.d.ts.map +1 -0
  3. package/dist/connection-error.js +56 -0
  4. package/dist/connection-error.js.map +1 -0
  5. package/dist/connection-error.test.d.ts +2 -0
  6. package/dist/connection-error.test.d.ts.map +1 -0
  7. package/dist/connection-error.test.js +63 -0
  8. package/dist/connection-error.test.js.map +1 -0
  9. package/dist/logout-args.d.ts +36 -0
  10. package/dist/logout-args.d.ts.map +1 -0
  11. package/dist/logout-args.js +100 -0
  12. package/dist/logout-args.js.map +1 -0
  13. package/dist/logout-args.test.d.ts +2 -0
  14. package/dist/logout-args.test.d.ts.map +1 -0
  15. package/dist/logout-args.test.js +72 -0
  16. package/dist/logout-args.test.js.map +1 -0
  17. package/dist/status-report.d.ts +91 -0
  18. package/dist/status-report.d.ts.map +1 -0
  19. package/dist/status-report.js +145 -0
  20. package/dist/status-report.js.map +1 -0
  21. package/dist/status-report.test.d.ts +2 -0
  22. package/dist/status-report.test.d.ts.map +1 -0
  23. package/dist/status-report.test.js +159 -0
  24. package/dist/status-report.test.js.map +1 -0
  25. package/dist/token-store.d.ts +43 -2
  26. package/dist/token-store.d.ts.map +1 -1
  27. package/dist/token-store.js +89 -25
  28. package/dist/token-store.js.map +1 -1
  29. package/dist/token-store.test.js +116 -0
  30. package/dist/token-store.test.js.map +1 -1
  31. package/dist/workspace-store.d.ts +13 -1
  32. package/dist/workspace-store.d.ts.map +1 -1
  33. package/dist/workspace-store.js +25 -2
  34. package/dist/workspace-store.js.map +1 -1
  35. package/dist/workspace-store.test.js +46 -0
  36. package/dist/workspace-store.test.js.map +1 -1
  37. package/package.json +1 -1
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Turning a failed `fetch` into something an operator can act on (internal D5).
3
+ *
4
+ * Node's fetch reports every transport failure as the same opaque
5
+ * `TypeError: fetch failed`, with the real reason buried in `.cause`. Printed
6
+ * raw by the CLI's error handlers it named no URL and gave no reason:
7
+ *
8
+ * $ TQ_BACKEND_URL=http://localhost:9999 yarn tq:login
9
+ * Error: TypeError: fetch failed
10
+ *
11
+ * In a worktree that is nearly always "the backend is not running" — but it
12
+ * reads as a broken CLI, so the operator debugs the CLI. The exit code was
13
+ * already correct; only the message was useless.
14
+ */
15
+ /**
16
+ * A one-line explanation when `err` is a transport failure, otherwise
17
+ * undefined so the caller falls back to printing the error itself.
18
+ *
19
+ * Deliberately narrow: only codes that genuinely mean "nothing answered at
20
+ * this address" are claimed. Reporting an unrelated failure as "backend not
21
+ * running" would send the operator to start a server that is already up.
22
+ */
23
+ export declare function describeConnectionError(err: unknown, apiUrl: string): string | undefined;
24
+ //# sourceMappingURL=connection-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-error.d.ts","sourceRoot":"","sources":["../src/connection-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAuBH;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,GACb,MAAM,GAAG,SAAS,CAqBpB"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Turning a failed `fetch` into something an operator can act on (internal D5).
3
+ *
4
+ * Node's fetch reports every transport failure as the same opaque
5
+ * `TypeError: fetch failed`, with the real reason buried in `.cause`. Printed
6
+ * raw by the CLI's error handlers it named no URL and gave no reason:
7
+ *
8
+ * $ TQ_BACKEND_URL=http://localhost:9999 yarn tq:login
9
+ * Error: TypeError: fetch failed
10
+ *
11
+ * In a worktree that is nearly always "the backend is not running" — but it
12
+ * reads as a broken CLI, so the operator debugs the CLI. The exit code was
13
+ * already correct; only the message was useless.
14
+ */
15
+ /**
16
+ * Walk the `cause` chain for a syscall-level error code.
17
+ *
18
+ * Depth-bounded: a cause chain is nominally short, but it is attacker- and
19
+ * bug-reachable data, and a cyclic `cause` would hang the process inside an
20
+ * error handler — the one place with nothing left to catch it.
21
+ */
22
+ function errorCodeOf(err, depth = 0) {
23
+ if (depth > 5 || err === null || typeof err !== 'object')
24
+ return undefined;
25
+ const { code, cause } = err;
26
+ if (typeof code === 'string' && code)
27
+ return code;
28
+ return errorCodeOf(cause, depth + 1);
29
+ }
30
+ /**
31
+ * A one-line explanation when `err` is a transport failure, otherwise
32
+ * undefined so the caller falls back to printing the error itself.
33
+ *
34
+ * Deliberately narrow: only codes that genuinely mean "nothing answered at
35
+ * this address" are claimed. Reporting an unrelated failure as "backend not
36
+ * running" would send the operator to start a server that is already up.
37
+ */
38
+ export function describeConnectionError(err, apiUrl) {
39
+ const code = errorCodeOf(err);
40
+ switch (code) {
41
+ case 'ECONNREFUSED':
42
+ case 'ERR_SOCKET_CONNECTION_TIMEOUT':
43
+ case 'ETIMEDOUT':
44
+ case 'EHOSTUNREACH':
45
+ case 'ENETUNREACH':
46
+ return (`No backend is listening at ${apiUrl} — it does not exist or is not running.\n` +
47
+ 'Start it, or point at another environment with `yarn tq:env <local|staging|production>`.');
48
+ case 'ENOTFOUND':
49
+ case 'EAI_AGAIN':
50
+ return (`Cannot resolve the host for ${apiUrl} — the address does not exist.\n` +
51
+ 'Check `yarn tq:env` for the configured backend.');
52
+ default:
53
+ return undefined;
54
+ }
55
+ }
56
+ //# sourceMappingURL=connection-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-error.js","sourceRoot":"","sources":["../src/connection-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,GAAY,EAAE,KAAK,GAAG,CAAC;IAC1C,IAAI,KAAK,GAAG,CAAC,IAAI,GAAG,KAAK,IAAI,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC3E,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,GAAgB,CAAC;IACzC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAClD,OAAO,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CACrC,GAAY,EACZ,MAAc;IAEd,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC9B,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,cAAc,CAAC;QACpB,KAAK,+BAA+B,CAAC;QACrC,KAAK,WAAW,CAAC;QACjB,KAAK,cAAc,CAAC;QACpB,KAAK,aAAa;YAChB,OAAO,CACL,8BAA8B,MAAM,2CAA2C;gBAC/E,0FAA0F,CAC3F,CAAC;QACJ,KAAK,WAAW,CAAC;QACjB,KAAK,WAAW;YACd,OAAO,CACL,+BAA+B,MAAM,kCAAkC;gBACvE,iDAAiD,CAClD,CAAC;QACJ;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=connection-error.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-error.test.d.ts","sourceRoot":"","sources":["../src/connection-error.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { describeConnectionError } from './connection-error.js';
3
+ const URL_ = 'http://localhost:9999';
4
+ /** The shape Node's fetch actually produces: opaque outer, real code in cause. */
5
+ function fetchFailure(code) {
6
+ const outer = new TypeError('fetch failed');
7
+ outer.cause = Object.assign(new Error('connect ' + code), { code });
8
+ return outer;
9
+ }
10
+ describe('describeConnectionError', () => {
11
+ it('explains a refused connection, naming the backend', () => {
12
+ const msg = describeConnectionError(fetchFailure('ECONNREFUSED'), URL_);
13
+ expect(msg).toContain(URL_);
14
+ expect(msg).toContain('not running');
15
+ });
16
+ it.each([
17
+ 'ETIMEDOUT',
18
+ 'EHOSTUNREACH',
19
+ 'ENETUNREACH',
20
+ 'ERR_SOCKET_CONNECTION_TIMEOUT',
21
+ ])('explains %s as nothing listening', (code) => {
22
+ expect(describeConnectionError(fetchFailure(code), URL_)).toContain('No backend is listening');
23
+ });
24
+ it.each(['ENOTFOUND', 'EAI_AGAIN'])('explains %s as an unresolvable host', (code) => {
25
+ expect(describeConnectionError(fetchFailure(code), URL_)).toContain('Cannot resolve the host');
26
+ });
27
+ it('finds a code nested several causes deep', () => {
28
+ const inner = Object.assign(new Error('refused'), {
29
+ code: 'ECONNREFUSED',
30
+ });
31
+ const mid = Object.assign(new Error('mid'), { cause: inner });
32
+ const outer = Object.assign(new TypeError('fetch failed'), { cause: mid });
33
+ expect(describeConnectionError(outer, URL_)).toContain('not running');
34
+ });
35
+ it('reads a code on the error itself, with no cause chain', () => {
36
+ expect(describeConnectionError(Object.assign(new Error('x'), {
37
+ code: 'ECONNREFUSED',
38
+ }), URL_)).toContain('not running');
39
+ });
40
+ // The caller falls back to printing the error, so claiming an unrelated
41
+ // failure is "backend not running" would send the operator to start a
42
+ // server that is already up.
43
+ it('declines an HTTP-level failure it cannot attribute', () => {
44
+ expect(describeConnectionError(new Error('401 Unauthorized'), URL_)).toBeUndefined();
45
+ });
46
+ it.each([
47
+ ['a plain string', 'boom'],
48
+ ['null', null],
49
+ ['undefined', undefined],
50
+ ['a code that is not a string', Object.assign(new Error('x'), { code: 7 })],
51
+ ])('declines %s', (_label, input) => {
52
+ expect(describeConnectionError(input, URL_)).toBeUndefined();
53
+ });
54
+ it('terminates on a cyclic cause chain', () => {
55
+ // An error handler is the one place with nothing left to catch a hang.
56
+ const a = new Error('a');
57
+ const b = new Error('b');
58
+ a.cause = b;
59
+ b.cause = a;
60
+ expect(describeConnectionError(a, URL_)).toBeUndefined();
61
+ });
62
+ });
63
+ //# sourceMappingURL=connection-error.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-error.test.js","sourceRoot":"","sources":["../src/connection-error.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Argument parsing for `tq:logout` (internal D1), kept pure so it can be
3
+ * tested — `cli.ts` ends in a self-executing `switch`, so importing it from a
4
+ * test would run a CLI command against the developer's real `~/.tq`.
5
+ *
6
+ * ## The defect this exists to prevent
7
+ *
8
+ * The first cut matched flags with `args.indexOf('--backend')`, which sees
9
+ * only the space-separated form. `--backend=<url>` therefore matched nothing,
10
+ * fell past every branch, and returned the CONFIGURED backend — clearing a
11
+ * backend the operator had not named, printing success, and exiting 0. An
12
+ * unrecognised flag did the same, because nothing rejected leftovers.
13
+ *
14
+ * That is the whole-file-wipe failure mode reached through argument parsing
15
+ * rather than through a fallback, so this fails CLOSED: anything not
16
+ * understood is an error, never a default.
17
+ */
18
+ /** What the operator asked `logout` to clear. */
19
+ export type LogoutRequest = {
20
+ kind: 'all';
21
+ } | {
22
+ kind: 'backend';
23
+ url: string;
24
+ } | {
25
+ kind: 'env';
26
+ env: 'local' | 'staging' | 'production';
27
+ }
28
+ /** No target named — the configured backend, as before. */
29
+ | {
30
+ kind: 'configured';
31
+ } | {
32
+ kind: 'error';
33
+ message: string;
34
+ };
35
+ export declare function parseLogoutArgs(args: string[]): LogoutRequest;
36
+ //# sourceMappingURL=logout-args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout-args.d.ts","sourceRoot":"","sources":["../src/logout-args.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,iDAAiD;AACjD,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GACf;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,YAAY,CAAA;CAAE;AAC1D,2DAA2D;GACzD;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAyCvC,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAsD7D"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Argument parsing for `tq:logout` (internal D1), kept pure so it can be
3
+ * tested — `cli.ts` ends in a self-executing `switch`, so importing it from a
4
+ * test would run a CLI command against the developer's real `~/.tq`.
5
+ *
6
+ * ## The defect this exists to prevent
7
+ *
8
+ * The first cut matched flags with `args.indexOf('--backend')`, which sees
9
+ * only the space-separated form. `--backend=<url>` therefore matched nothing,
10
+ * fell past every branch, and returned the CONFIGURED backend — clearing a
11
+ * backend the operator had not named, printing success, and exiting 0. An
12
+ * unrecognised flag did the same, because nothing rejected leftovers.
13
+ *
14
+ * That is the whole-file-wipe failure mode reached through argument parsing
15
+ * rather than through a fallback, so this fails CLOSED: anything not
16
+ * understood is an error, never a default.
17
+ */
18
+ const ENV_NAMES = ['local', 'staging', 'production'];
19
+ const USAGE = 'Usage: tq-oauth logout [--backend <url> | --env <local|staging|production> | --all]';
20
+ /** Both `--flag value` and `--flag=value`. */
21
+ function readFlag(args, flag) {
22
+ const inline = args.find((a) => a.startsWith(`${flag}=`));
23
+ if (inline !== undefined) {
24
+ return { present: true, value: inline.slice(flag.length + 1) };
25
+ }
26
+ const i = args.indexOf(flag);
27
+ if (i === -1)
28
+ return { present: false, value: '' };
29
+ const next = args[i + 1];
30
+ return {
31
+ present: true,
32
+ value: next && !next.startsWith('--') ? next : '',
33
+ };
34
+ }
35
+ /** Every token the known flags account for, so leftovers can be rejected. */
36
+ function consumedTokens(args) {
37
+ const consumed = new Set();
38
+ for (const flag of ['--backend', '--env', '--all']) {
39
+ const inline = args.find((a) => a.startsWith(`${flag}=`));
40
+ if (inline)
41
+ consumed.add(inline);
42
+ const i = args.indexOf(flag);
43
+ if (i !== -1) {
44
+ consumed.add(args[i]);
45
+ const next = args[i + 1];
46
+ if (flag !== '--all' && next && !next.startsWith('--'))
47
+ consumed.add(next);
48
+ }
49
+ }
50
+ return consumed;
51
+ }
52
+ export function parseLogoutArgs(args) {
53
+ const consumed = consumedTokens(args);
54
+ const unknown = args.filter((a) => !consumed.has(a));
55
+ if (unknown.length > 0) {
56
+ return {
57
+ kind: 'error',
58
+ message: `Unknown argument${unknown.length > 1 ? 's' : ''}: ${unknown.join(', ')}\n` +
59
+ USAGE,
60
+ };
61
+ }
62
+ const backend = readFlag(args, '--backend');
63
+ const env = readFlag(args, '--env');
64
+ const all = args.includes('--all');
65
+ // Two targets is an operator mistake, and picking one silently is exactly
66
+ // the class of bug this module exists to prevent.
67
+ if ([backend.present, env.present, all].filter(Boolean).length > 1) {
68
+ return {
69
+ kind: 'error',
70
+ message: `Pass only one of --backend, --env, --all\n${USAGE}`,
71
+ };
72
+ }
73
+ if (all)
74
+ return { kind: 'all' };
75
+ if (backend.present) {
76
+ if (!backend.value) {
77
+ return { kind: 'error', message: `--backend requires a url\n${USAGE}` };
78
+ }
79
+ if (!/^https?:\/\//i.test(backend.value.trim())) {
80
+ return {
81
+ kind: 'error',
82
+ message: `Not a backend url: "${backend.value}" — expected an http(s) origin, ` +
83
+ 'e.g. http://localhost:8180',
84
+ };
85
+ }
86
+ return { kind: 'backend', url: backend.value.trim().replace(/\/+$/, '') };
87
+ }
88
+ if (env.present) {
89
+ const match = ENV_NAMES.find((n) => n === env.value);
90
+ if (!match) {
91
+ return {
92
+ kind: 'error',
93
+ message: `Unknown environment: "${env.value}" — expected ${ENV_NAMES.join(', ')}`,
94
+ };
95
+ }
96
+ return { kind: 'env', env: match };
97
+ }
98
+ return { kind: 'configured' };
99
+ }
100
+ //# sourceMappingURL=logout-args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout-args.js","sourceRoot":"","sources":["../src/logout-args.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=logout-args.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout-args.test.d.ts","sourceRoot":"","sources":["../src/logout-args.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseLogoutArgs } from './logout-args.js';
3
+ describe('parseLogoutArgs', () => {
4
+ it('defaults to the configured backend when nothing is named', () => {
5
+ expect(parseLogoutArgs([])).toEqual({ kind: 'configured' });
6
+ });
7
+ // review F-002. `args.indexOf('--backend')` saw only the space-separated
8
+ // form, so `--backend=<url>` fell through to the CONFIGURED backend —
9
+ // clearing one the operator had not named, and exiting 0.
10
+ it.each([
11
+ ['space-separated', ['--backend', 'http://localhost:9480']],
12
+ ['inline', ['--backend=http://localhost:9480']],
13
+ ])('accepts the %s --backend form', (_label, args) => {
14
+ expect(parseLogoutArgs(args)).toEqual({
15
+ kind: 'backend',
16
+ url: 'http://localhost:9480',
17
+ });
18
+ });
19
+ it.each([
20
+ ['space-separated', ['--env', 'staging']],
21
+ ['inline', ['--env=staging']],
22
+ ])('accepts the %s --env form', (_label, args) => {
23
+ expect(parseLogoutArgs(args)).toEqual({ kind: 'env', env: 'staging' });
24
+ });
25
+ it('normalizes a trailing slash on --backend', () => {
26
+ expect(parseLogoutArgs(['--backend=https://api.levr.now/'])).toEqual({
27
+ kind: 'backend',
28
+ url: 'https://api.levr.now',
29
+ });
30
+ });
31
+ it('recognises --all', () => {
32
+ expect(parseLogoutArgs(['--all'])).toEqual({ kind: 'all' });
33
+ });
34
+ // Fails CLOSED: an unrecognised argument must never fall back to a default
35
+ // target, which is the whole-file-wipe failure mode via argument parsing.
36
+ it.each([
37
+ [['--target', 'http://localhost:9480']],
38
+ [['--backened=http://localhost:9480']],
39
+ [['http://localhost:9480']],
40
+ [['--backend', 'http://x', '--extra']],
41
+ ])('rejects unknown arguments %j', (args) => {
42
+ const r = parseLogoutArgs(args);
43
+ expect(r.kind).toBe('error');
44
+ expect(r.kind === 'error' && r.message).toMatch(/Unknown argument/);
45
+ });
46
+ it('rejects two targets rather than silently picking one', () => {
47
+ const r = parseLogoutArgs(['--backend=http://localhost:9480', '--all']);
48
+ expect(r.kind).toBe('error');
49
+ expect(r.kind === 'error' && r.message).toMatch(/only one of/);
50
+ });
51
+ it.each([
52
+ ['a bare flag', ['--backend']],
53
+ ['an empty inline value', ['--backend=']],
54
+ ['a following flag', ['--backend', '--all']],
55
+ ])('rejects --backend with no url (%s)', (_label, args) => {
56
+ expect(parseLogoutArgs(args).kind).toBe('error');
57
+ });
58
+ it.each(['localhost:8580', 'ftp://x', '/tmp/nope', 'api.levr.now'])('rejects %s as a backend url', (value) => {
59
+ const r = parseLogoutArgs([`--backend=${value}`]);
60
+ expect(r.kind).toBe('error');
61
+ expect(r.kind === 'error' && r.message).toMatch(/Not a backend url/);
62
+ });
63
+ it('rejects an unknown environment name', () => {
64
+ const r = parseLogoutArgs(['--env=staging-typo']);
65
+ expect(r.kind).toBe('error');
66
+ expect(r.kind === 'error' && r.message).toMatch(/Unknown environment/);
67
+ });
68
+ it.each(['local', 'staging', 'production'])('accepts --env %s', (env) => {
69
+ expect(parseLogoutArgs([`--env=${env}`])).toEqual({ kind: 'env', env });
70
+ });
71
+ });
72
+ //# sourceMappingURL=logout-args.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout-args.test.js","sourceRoot":"","sources":["../src/logout-args.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Pure decision logic behind `tq:status` (internal).
3
+ *
4
+ * Lives outside `cli.ts` for the same reason `scripts/setup-mcp-workspace.lib.ts`
5
+ * lives outside `setup-mcp.ts`: that module ends in a self-executing `switch`,
6
+ * so importing it from a test would run a CLI command against the developer's
7
+ * real `~/.tq`. Everything here is pure — the caller does the fs reading and
8
+ * passes the parsed values in.
9
+ *
10
+ * ## The defect this exists to prevent
11
+ *
12
+ * `tq:status` used to render its per-environment block from the three
13
+ * hardcoded `ENV_URLS` presets, while `tq:login` stores a token under the
14
+ * backend's real URL. On any local that is not literally `http://localhost:8080`
15
+ * — every worktree, every DEV_TLS checkout — the block therefore reported a
16
+ * backend the user had not logged into and could not refresh: a stale
17
+ * `local: expired 157366m ago (active)` that survived every login, sitting
18
+ * directly beneath a `Status: valid` line describing a different token.
19
+ *
20
+ * The load-bearing assertion in the tests is that a backend absent from
21
+ * `ENV_URLS` is still listed, and that "active" is decided by URL rather than
22
+ * by environment name.
23
+ */
24
+ /** One rendered row of the stored-token table. */
25
+ export interface TokenRow {
26
+ /** True for the backend `~/.tq/config.json` currently points at. */
27
+ active: boolean;
28
+ url: string;
29
+ /** `local` / `staging` / `production` / `custom`. */
30
+ label: string;
31
+ /** e.g. `valid 738m`, `expired 12m ago`. */
32
+ expiry: string;
33
+ }
34
+ export declare function formatExpiry(expiresAt: number, now: number): string;
35
+ /**
36
+ * Human label for a stored backend URL.
37
+ *
38
+ * `resolveEnvFromUrl` only recognises the three `ENV_URLS` presets, so every
39
+ * worktree-offset local and every DEV_TLS local comes back unnamed — which is
40
+ * most of a working machine's token map. Loopback and the `.levr.test` dev
41
+ * domain are local by construction, so name them rather than render a dozen
42
+ * indistinguishable `custom` rows.
43
+ */
44
+ export declare function labelForBackend(url: string): string;
45
+ /**
46
+ * Build the stored-token table from the backends the token map ACTUALLY holds.
47
+ *
48
+ * `activeUrl` is matched by URL, never by environment name: the name `local`
49
+ * maps to the hardcoded `:8080` preset, which on a worktree is a different
50
+ * backend from the configured one.
51
+ *
52
+ * Active row first, then alphabetical — deterministic, so the table does not
53
+ * reshuffle between runs.
54
+ */
55
+ export declare function buildTokenRows(entries: ReadonlyArray<{
56
+ url: string;
57
+ tokens: {
58
+ expiresAt: number;
59
+ };
60
+ }>, activeUrl: string | undefined, now: number): TokenRow[];
61
+ /**
62
+ * Map a `levr` MCP URL to the backend URL that `~/.tq/oauth-tokens.json` and
63
+ * `~/.tq/workspace.json` are keyed by, so the MCP entry can be compared
64
+ * against the configured backend.
65
+ *
66
+ * The MCP host is not the API host for the hosted environments — both are
67
+ * reached through the client origin's `/api` proxy — so the levr URL cannot be
68
+ * compared directly. The aliases are DERIVED from `PRESETS` rather than
69
+ * written out, so they cannot drift from the environment definitions.
70
+ *
71
+ * Twin of `identityKeyFromLevrUrl` in `scripts/setup-mcp-workspace.lib.ts`,
72
+ * which resolves the same key on the WRITE side. `scripts/` imports no
73
+ * workspace package, so the two cannot share code today; deriving from
74
+ * `PRESETS` is what keeps this copy honest.
75
+ */
76
+ export declare function backendUrlForLevrMcp(levrUrl: string): string;
77
+ /** The `levr` server entry, extracted from a parsed `.mcp.json`. */
78
+ export interface LevrMcpEntry {
79
+ url: string;
80
+ workspaceId?: string;
81
+ }
82
+ /**
83
+ * Pull the `levr` entry out of a parsed `.mcp.json`.
84
+ *
85
+ * Returns undefined for every shape that does not carry a usable URL —
86
+ * missing file, wrong type, no `levr` server, no `url`. Reporting nothing is
87
+ * correct here: the section is diagnostic, and a guess about which control
88
+ * plane an agent is talking to is worse than silence.
89
+ */
90
+ export declare function levrEntryFrom(parsed: unknown): LevrMcpEntry | undefined;
91
+ //# sourceMappingURL=status-report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status-report.d.ts","sourceRoot":"","sources":["../src/status-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,kDAAkD;AAClD,MAAM,WAAW,QAAQ;IACvB,oEAAoE;IACpE,MAAM,EAAE,OAAO,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGnE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAkBnD;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,aAAa,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,EACtE,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,GAAG,EAAE,MAAM,GACV,QAAQ,EAAE,CAYZ;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAiB5D;AAED,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAsBvE"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Pure decision logic behind `tq:status` (internal).
3
+ *
4
+ * Lives outside `cli.ts` for the same reason `scripts/setup-mcp-workspace.lib.ts`
5
+ * lives outside `setup-mcp.ts`: that module ends in a self-executing `switch`,
6
+ * so importing it from a test would run a CLI command against the developer's
7
+ * real `~/.tq`. Everything here is pure — the caller does the fs reading and
8
+ * passes the parsed values in.
9
+ *
10
+ * ## The defect this exists to prevent
11
+ *
12
+ * `tq:status` used to render its per-environment block from the three
13
+ * hardcoded `ENV_URLS` presets, while `tq:login` stores a token under the
14
+ * backend's real URL. On any local that is not literally `http://localhost:8080`
15
+ * — every worktree, every DEV_TLS checkout — the block therefore reported a
16
+ * backend the user had not logged into and could not refresh: a stale
17
+ * `local: expired 157366m ago (active)` that survived every login, sitting
18
+ * directly beneath a `Status: valid` line describing a different token.
19
+ *
20
+ * The load-bearing assertion in the tests is that a backend absent from
21
+ * `ENV_URLS` is still listed, and that "active" is decided by URL rather than
22
+ * by environment name.
23
+ */
24
+ import { resolveEnvFromUrl } from './token-store.js';
25
+ import { PRESETS } from './config.js';
26
+ export function formatExpiry(expiresAt, now) {
27
+ const mins = Math.round((expiresAt - now) / 1000 / 60);
28
+ return now >= expiresAt ? `expired ${-mins}m ago` : `valid ${mins}m`;
29
+ }
30
+ /**
31
+ * Human label for a stored backend URL.
32
+ *
33
+ * `resolveEnvFromUrl` only recognises the three `ENV_URLS` presets, so every
34
+ * worktree-offset local and every DEV_TLS local comes back unnamed — which is
35
+ * most of a working machine's token map. Loopback and the `.levr.test` dev
36
+ * domain are local by construction, so name them rather than render a dozen
37
+ * indistinguishable `custom` rows.
38
+ */
39
+ export function labelForBackend(url) {
40
+ const preset = resolveEnvFromUrl(url);
41
+ if (preset)
42
+ return preset;
43
+ try {
44
+ const { hostname } = new URL(url);
45
+ if (hostname === 'localhost' ||
46
+ hostname === '127.0.0.1' ||
47
+ hostname === '[::1]' ||
48
+ hostname === 'levr.test' ||
49
+ hostname.endsWith('.levr.test')) {
50
+ return 'local';
51
+ }
52
+ }
53
+ catch {
54
+ // Not a parseable URL — fall through to `custom`.
55
+ }
56
+ return 'custom';
57
+ }
58
+ /**
59
+ * Build the stored-token table from the backends the token map ACTUALLY holds.
60
+ *
61
+ * `activeUrl` is matched by URL, never by environment name: the name `local`
62
+ * maps to the hardcoded `:8080` preset, which on a worktree is a different
63
+ * backend from the configured one.
64
+ *
65
+ * Active row first, then alphabetical — deterministic, so the table does not
66
+ * reshuffle between runs.
67
+ */
68
+ export function buildTokenRows(entries, activeUrl, now) {
69
+ return entries
70
+ .map((e) => ({
71
+ active: !!activeUrl && e.url === activeUrl,
72
+ url: e.url,
73
+ label: labelForBackend(e.url),
74
+ expiry: formatExpiry(e.tokens.expiresAt, now),
75
+ }))
76
+ .sort((a, b) => {
77
+ if (a.active !== b.active)
78
+ return a.active ? -1 : 1;
79
+ return a.url.localeCompare(b.url);
80
+ });
81
+ }
82
+ /**
83
+ * Map a `levr` MCP URL to the backend URL that `~/.tq/oauth-tokens.json` and
84
+ * `~/.tq/workspace.json` are keyed by, so the MCP entry can be compared
85
+ * against the configured backend.
86
+ *
87
+ * The MCP host is not the API host for the hosted environments — both are
88
+ * reached through the client origin's `/api` proxy — so the levr URL cannot be
89
+ * compared directly. The aliases are DERIVED from `PRESETS` rather than
90
+ * written out, so they cannot drift from the environment definitions.
91
+ *
92
+ * Twin of `identityKeyFromLevrUrl` in `scripts/setup-mcp-workspace.lib.ts`,
93
+ * which resolves the same key on the WRITE side. `scripts/` imports no
94
+ * workspace package, so the two cannot share code today; deriving from
95
+ * `PRESETS` is what keeps this copy honest.
96
+ */
97
+ export function backendUrlForLevrMcp(levrUrl) {
98
+ const base = levrUrl.replace(/\/+$/, '').replace(/\/v1\/mcp$/, '');
99
+ // ONLY the two hosted presets. `PRESETS.local` must never join this list:
100
+ // under DEV_TLS it is `LOCAL_HTTPS`, whose clientUrl+`/api` is
101
+ // `https://ai.levr.test:3020/api` — a real key in the token map that has to
102
+ // resolve VERBATIM. Adding it would rewrite that key to
103
+ // `https://api.levr.test:8080` and make the entry unreachable, and the
104
+ // "keys a DEV_TLS local verbatim" test would still pass, because
105
+ // `isTestEnv()` makes PRESETS.local the plain-HTTP variant. The test below
106
+ // that asserts this exclusion holds under TLS is the one that would catch
107
+ // it. (review F-006)
108
+ for (const preset of [PRESETS.staging, PRESETS.production]) {
109
+ if (base === `${preset.clientUrl}/api`)
110
+ return preset.apiUrl;
111
+ }
112
+ // Everything else keys verbatim: that base IS the URL the backend was
113
+ // reached at when its token was stored (worktree locals, DEV_TLS).
114
+ return base;
115
+ }
116
+ /**
117
+ * Pull the `levr` entry out of a parsed `.mcp.json`.
118
+ *
119
+ * Returns undefined for every shape that does not carry a usable URL —
120
+ * missing file, wrong type, no `levr` server, no `url`. Reporting nothing is
121
+ * correct here: the section is diagnostic, and a guess about which control
122
+ * plane an agent is talking to is worse than silence.
123
+ */
124
+ export function levrEntryFrom(parsed) {
125
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
126
+ return undefined;
127
+ }
128
+ const servers = parsed.mcpServers;
129
+ if (servers === null || typeof servers !== 'object')
130
+ return undefined;
131
+ const levr = servers.levr;
132
+ if (levr === null || typeof levr !== 'object')
133
+ return undefined;
134
+ const { url, headers } = levr;
135
+ if (typeof url !== 'string' || !url)
136
+ return undefined;
137
+ const wsId = headers !== null && typeof headers === 'object'
138
+ ? headers['Workspace-Id']
139
+ : undefined;
140
+ return {
141
+ url,
142
+ workspaceId: typeof wsId === 'string' && wsId ? wsId : undefined,
143
+ };
144
+ }
145
+ //# sourceMappingURL=status-report.js.map