@levr-one/auth 1.1.2 → 1.1.4

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 (43) hide show
  1. package/dist/config.d.ts +66 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +127 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/config.test.js +123 -1
  6. package/dist/config.test.js.map +1 -1
  7. package/dist/connection-error.d.ts +24 -0
  8. package/dist/connection-error.d.ts.map +1 -0
  9. package/dist/connection-error.js +56 -0
  10. package/dist/connection-error.js.map +1 -0
  11. package/dist/connection-error.test.d.ts +2 -0
  12. package/dist/connection-error.test.d.ts.map +1 -0
  13. package/dist/connection-error.test.js +63 -0
  14. package/dist/connection-error.test.js.map +1 -0
  15. package/dist/logout-args.d.ts +36 -0
  16. package/dist/logout-args.d.ts.map +1 -0
  17. package/dist/logout-args.js +100 -0
  18. package/dist/logout-args.js.map +1 -0
  19. package/dist/logout-args.test.d.ts +2 -0
  20. package/dist/logout-args.test.d.ts.map +1 -0
  21. package/dist/logout-args.test.js +72 -0
  22. package/dist/logout-args.test.js.map +1 -0
  23. package/dist/status-report.d.ts +91 -0
  24. package/dist/status-report.d.ts.map +1 -0
  25. package/dist/status-report.js +145 -0
  26. package/dist/status-report.js.map +1 -0
  27. package/dist/status-report.test.d.ts +2 -0
  28. package/dist/status-report.test.d.ts.map +1 -0
  29. package/dist/status-report.test.js +159 -0
  30. package/dist/status-report.test.js.map +1 -0
  31. package/dist/token-store.d.ts +43 -2
  32. package/dist/token-store.d.ts.map +1 -1
  33. package/dist/token-store.js +89 -25
  34. package/dist/token-store.js.map +1 -1
  35. package/dist/token-store.test.js +116 -0
  36. package/dist/token-store.test.js.map +1 -1
  37. package/dist/workspace-store.d.ts +13 -1
  38. package/dist/workspace-store.d.ts.map +1 -1
  39. package/dist/workspace-store.js +25 -2
  40. package/dist/workspace-store.js.map +1 -1
  41. package/dist/workspace-store.test.js +46 -0
  42. package/dist/workspace-store.test.js.map +1 -1
  43. package/package.json +1 -1
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status-report.js","sourceRoot":"","sources":["../src/status-report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=status-report.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status-report.test.d.ts","sourceRoot":"","sources":["../src/status-report.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { PRESETS } from './config.js';
3
+ import { backendUrlForLevrMcp, buildTokenRows, formatExpiry, labelForBackend, levrEntryFrom, } from './status-report.js';
4
+ const NOW = 1_700_000_000_000;
5
+ const at = (minutes) => ({ expiresAt: NOW + minutes * 60_000 });
6
+ describe('buildTokenRows', () => {
7
+ // The regression this whole module exists for: the old block rendered the
8
+ // three hardcoded ENV_URLS presets, so a worktree-offset local was invisible
9
+ // and the `:8080` row it printed instead could never be refreshed.
10
+ it('lists a backend that is not one of the three presets', () => {
11
+ const rows = buildTokenRows([{ url: 'http://localhost:9480', tokens: at(60) }], 'http://localhost:9480', NOW);
12
+ expect(rows.map((r) => r.url)).toEqual(['http://localhost:9480']);
13
+ });
14
+ it('decides "active" by url, not by environment name', () => {
15
+ const rows = buildTokenRows([
16
+ { url: 'http://localhost:8080', tokens: at(-99999) },
17
+ { url: 'http://localhost:9480', tokens: at(60) },
18
+ ], 'http://localhost:9480', NOW);
19
+ // Both are "local" by name. Only the configured URL is active — marking
20
+ // the :8080 preset active is exactly the old bug.
21
+ expect(rows.filter((r) => r.active).map((r) => r.url)).toEqual([
22
+ 'http://localhost:9480',
23
+ ]);
24
+ expect(rows.every((r) => r.label === 'local')).toBe(true);
25
+ });
26
+ it('puts the active row first, then sorts the rest alphabetically', () => {
27
+ const rows = buildTokenRows([
28
+ { url: 'https://api.levr.now', tokens: at(60) },
29
+ { url: 'http://localhost:8080', tokens: at(60) },
30
+ { url: 'http://localhost:9480', tokens: at(60) },
31
+ ], 'http://localhost:9480', NOW);
32
+ expect(rows.map((r) => r.url)).toEqual([
33
+ 'http://localhost:9480',
34
+ 'http://localhost:8080',
35
+ 'https://api.levr.now',
36
+ ]);
37
+ });
38
+ it('marks nothing active when the configured backend has no token', () => {
39
+ const rows = buildTokenRows([{ url: 'https://api.levr.now', tokens: at(60) }], 'http://localhost:9480', NOW);
40
+ expect(rows.some((r) => r.active)).toBe(false);
41
+ });
42
+ it('marks nothing active when there is no configured backend', () => {
43
+ const rows = buildTokenRows([{ url: 'https://api.levr.now', tokens: at(60) }], undefined, NOW);
44
+ expect(rows.some((r) => r.active)).toBe(false);
45
+ });
46
+ });
47
+ describe('formatExpiry', () => {
48
+ it('reports remaining minutes while valid', () => {
49
+ expect(formatExpiry(NOW + 90 * 60_000, NOW)).toBe('valid 90m');
50
+ });
51
+ it('reports elapsed minutes once expired', () => {
52
+ expect(formatExpiry(NOW - 12 * 60_000, NOW)).toBe('expired 12m ago');
53
+ });
54
+ it('treats the exact expiry instant as expired', () => {
55
+ expect(formatExpiry(NOW, NOW)).toBe('expired 0m ago');
56
+ });
57
+ });
58
+ describe('labelForBackend', () => {
59
+ it('names the presets', () => {
60
+ expect(labelForBackend('https://api.levr.now')).toBe('staging');
61
+ expect(labelForBackend('https://api.levr.one')).toBe('production');
62
+ expect(labelForBackend('http://localhost:8080')).toBe('local');
63
+ });
64
+ it('names a worktree-offset loopback backend local', () => {
65
+ expect(labelForBackend('http://localhost:9480')).toBe('local');
66
+ expect(labelForBackend('http://127.0.0.1:9480')).toBe('local');
67
+ });
68
+ it('names the DEV_TLS dev domain local', () => {
69
+ expect(labelForBackend('https://ai.levr.test:3020/api')).toBe('local');
70
+ expect(labelForBackend('https://api.levr.test:8080')).toBe('local');
71
+ });
72
+ it('names the levr.test apex local, not just its subdomains', () => {
73
+ expect(labelForBackend('https://levr.test:3020')).toBe('local');
74
+ });
75
+ it('falls back to custom for a remote backend', () => {
76
+ expect(labelForBackend('https://backend.testquality.net')).toBe('custom');
77
+ });
78
+ it('falls back to custom for an unparseable url', () => {
79
+ expect(labelForBackend('not a url')).toBe('custom');
80
+ });
81
+ it('does not treat a lookalike host as local', () => {
82
+ // `.endsWith('.levr.test')` must not match an attacker-ish suffix host.
83
+ expect(labelForBackend('https://evil-levr.test.example.com')).toBe('custom');
84
+ });
85
+ });
86
+ describe('backendUrlForLevrMcp', () => {
87
+ it('maps the hosted staging MCP url to the api host it is keyed by', () => {
88
+ expect(backendUrlForLevrMcp('https://ai.levr.now/api/v1/mcp')).toBe('https://api.levr.now');
89
+ });
90
+ it('maps the hosted production MCP url to the api host it is keyed by', () => {
91
+ expect(backendUrlForLevrMcp('https://ai.levr.one/api/v1/mcp')).toBe('https://api.levr.one');
92
+ });
93
+ it('keys a worktree local verbatim', () => {
94
+ expect(backendUrlForLevrMcp('http://localhost:9780/v1/mcp')).toBe('http://localhost:9780');
95
+ });
96
+ it('keys a DEV_TLS local verbatim, keeping the /api prefix', () => {
97
+ // This exact string is a real key in the token map — rewriting it would
98
+ // make the entry unreachable.
99
+ expect(backendUrlForLevrMcp('https://ai.levr.test:3020/api/v1/mcp')).toBe('https://ai.levr.test:3020/api');
100
+ });
101
+ it('keeps two different worktree ports distinct', () => {
102
+ expect(backendUrlForLevrMcp('http://localhost:8180/v1/mcp')).not.toBe(backendUrlForLevrMcp('http://localhost:8580/v1/mcp'));
103
+ });
104
+ // review F-006. `PRESETS.local` must never join the alias loop: under
105
+ // DEV_TLS its clientUrl+/api is `https://ai.levr.test:3020/api`, a real key
106
+ // in the token map that has to resolve verbatim. Asserting against
107
+ // PRESETS.local itself pins the exclusion in BOTH modes — a hardcoded
108
+ // DEV_TLS string cannot, because isTestEnv() forces the plain-HTTP preset
109
+ // and the assertion would pass over a broken function.
110
+ it('never aliases the local preset, whichever variant is active', () => {
111
+ const localMcp = `${PRESETS.local.clientUrl}/api/v1/mcp`;
112
+ expect(backendUrlForLevrMcp(localMcp)).toBe(`${PRESETS.local.clientUrl}/api`);
113
+ expect(backendUrlForLevrMcp(localMcp)).not.toBe(PRESETS.local.apiUrl);
114
+ });
115
+ it('tolerates a trailing slash', () => {
116
+ expect(backendUrlForLevrMcp('https://ai.levr.now/api/v1/mcp/')).toBe('https://api.levr.now');
117
+ });
118
+ });
119
+ describe('levrEntryFrom', () => {
120
+ const config = {
121
+ mcpServers: {
122
+ levr: {
123
+ type: 'http',
124
+ url: 'https://ai.levr.now/api/v1/mcp',
125
+ headers: { 'Workspace-Id': 'ws-1' },
126
+ },
127
+ 'qinetic-mcp': { type: 'stdio' },
128
+ },
129
+ };
130
+ it('extracts the url and the Workspace-Id header', () => {
131
+ expect(levrEntryFrom(config)).toEqual({
132
+ url: 'https://ai.levr.now/api/v1/mcp',
133
+ workspaceId: 'ws-1',
134
+ });
135
+ });
136
+ it('returns the url alone when no Workspace-Id is pinned', () => {
137
+ expect(levrEntryFrom({ mcpServers: { levr: { url: 'http://x/v1/mcp' } } })).toEqual({ url: 'http://x/v1/mcp', workspaceId: undefined });
138
+ });
139
+ it.each([
140
+ ['null', null],
141
+ ['an array', []],
142
+ ['a string', 'nope'],
143
+ ['no mcpServers', {}],
144
+ ['no levr server', { mcpServers: { 'qinetic-mcp': { type: 'stdio' } } }],
145
+ ['a levr server with no url', { mcpServers: { levr: { type: 'http' } } }],
146
+ ['a non-string url', { mcpServers: { levr: { url: 42 } } }],
147
+ ['an empty url', { mcpServers: { levr: { url: '' } } }],
148
+ ])('returns undefined for %s', (_label, input) => {
149
+ expect(levrEntryFrom(input)).toBeUndefined();
150
+ });
151
+ it('ignores a non-string Workspace-Id rather than rendering it', () => {
152
+ expect(levrEntryFrom({
153
+ mcpServers: {
154
+ levr: { url: 'http://x/v1/mcp', headers: { 'Workspace-Id': 7 } },
155
+ },
156
+ })).toEqual({ url: 'http://x/v1/mcp', workspaceId: undefined });
157
+ });
158
+ });
159
+ //# sourceMappingURL=status-report.test.js.map