@lanes-sh/link 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +21 -8
  2. package/instructions/skills/lanes-link/SKILL.md +59 -14
  3. package/package.json +1 -1
  4. package/src/auth/index.ts +3 -1
  5. package/src/auth/oauth/metadata.ts +83 -9
  6. package/src/auth/oauth/redirects.ts +70 -0
  7. package/src/auth/oauth/server.ts +49 -69
  8. package/src/auth/oauth/store.ts +19 -5
  9. package/src/cli/argv.ts +50 -0
  10. package/src/cli/brand.ts +178 -0
  11. package/src/cli/callback-page.ts +108 -128
  12. package/src/cli/commands/connect/accounts.ts +5 -0
  13. package/src/cli/commands/connect/assertion.ts +187 -0
  14. package/src/cli/commands/connect/authorise.ts +61 -17
  15. package/src/cli/commands/connect/client.ts +37 -9
  16. package/src/cli/commands/connect/discover.ts +94 -0
  17. package/src/cli/commands/connect/family.ts +72 -0
  18. package/src/cli/commands/connect/index.ts +113 -115
  19. package/src/cli/commands/connect/method.ts +237 -0
  20. package/src/cli/commands/connect/outcome.ts +42 -1
  21. package/src/cli/commands/connect/pasted-token.ts +66 -0
  22. package/src/cli/commands/connect/requirements.ts +60 -8
  23. package/src/cli/commands/connect/setup.ts +16 -5
  24. package/src/cli/commands/connect/target-note.ts +34 -0
  25. package/src/cli/commands/identity.ts +258 -0
  26. package/src/cli/commands/knowledge/index.ts +390 -0
  27. package/src/cli/commands/knowledge/migrate.ts +180 -0
  28. package/src/cli/commands/knowledge/setup.ts +144 -0
  29. package/src/cli/commands/knowledge.ts +10 -0
  30. package/src/cli/commands/mcp/harnesses.ts +16 -2
  31. package/src/cli/commands/mcp/register.ts +9 -1
  32. package/src/cli/commands/mcp/stdio.ts +21 -0
  33. package/src/cli/commands/operate/dashboard.ts +107 -0
  34. package/src/cli/commands/operate/findings.ts +151 -0
  35. package/src/cli/commands/operate/inspect.ts +56 -158
  36. package/src/cli/commands/operate/outputs.ts +38 -11
  37. package/src/cli/commands/operate/serve.ts +3 -0
  38. package/src/cli/commands/operate/token.ts +1 -1
  39. package/src/cli/commands/operate.ts +2 -0
  40. package/src/cli/commands/profile/declare.ts +154 -0
  41. package/src/cli/commands/profile/removal.ts +17 -0
  42. package/src/cli/commands/profile.ts +83 -35
  43. package/src/cli/commands/setup.ts +22 -6
  44. package/src/cli/commands/target.ts +65 -83
  45. package/src/cli/config-edit.ts +48 -144
  46. package/src/cli/config-repair.ts +186 -0
  47. package/src/cli/dashboard-page.ts +284 -0
  48. package/src/cli/dashboard-shell.ts +125 -0
  49. package/src/cli/identity.ts +12 -1
  50. package/src/cli/main.ts +68 -4
  51. package/src/cli/oauth-callback.ts +187 -0
  52. package/src/cli/oauth-exchange.ts +57 -15
  53. package/src/cli/oauth.ts +67 -177
  54. package/src/cli/output.ts +21 -5
  55. package/src/cli/provider-marks.ts +45 -0
  56. package/src/cli/runtime/open.ts +67 -49
  57. package/src/cli/runtime/registry.ts +60 -2
  58. package/src/cli/runtime/select.ts +26 -13
  59. package/src/cli/runtime/vault.ts +61 -0
  60. package/src/cli/runtime.ts +2 -1
  61. package/src/cli/selection.ts +357 -0
  62. package/src/cli/usage.ts +32 -9
  63. package/src/connectivity/auth/README.md +7 -1
  64. package/src/connectivity/auth/basic/index.ts +1 -1
  65. package/src/connectivity/auth/index.ts +14 -0
  66. package/src/connectivity/auth/oauth-authcode/broker.ts +26 -0
  67. package/src/connectivity/auth/oauth-authcode/index.ts +16 -2
  68. package/src/connectivity/auth/oauth-authcode/provider.ts +1 -1
  69. package/src/connectivity/auth/oauth-authcode/refresh.ts +3 -3
  70. package/src/connectivity/auth/oauth-jwt/README.md +33 -0
  71. package/src/connectivity/auth/oauth-jwt/index.ts +237 -0
  72. package/src/connectivity/auth/oauth-jwt/key.ts +148 -0
  73. package/src/connectivity/auth/resolve.ts +1 -1
  74. package/src/connectivity/auth/token.ts +11 -0
  75. package/src/connectivity/index.ts +2 -0
  76. package/src/connectivity/manifest/auth.ts +99 -2
  77. package/src/connectivity/manifest/identity.ts +12 -0
  78. package/src/connectivity/manifest/index.ts +3 -1
  79. package/src/connectivity/manifest/provider.ts +37 -8
  80. package/src/connectivity/manifest/requirements.ts +109 -6
  81. package/src/deployments/adapters/filesystem.ts +10 -1
  82. package/src/deployments/adapters/github-api.ts +106 -0
  83. package/src/deployments/adapters/github-commit.ts +103 -0
  84. package/src/deployments/adapters/github-repo.ts +356 -0
  85. package/src/deployments/adapters/github-testing.ts +258 -0
  86. package/src/deployments/adapters/github.ts +125 -0
  87. package/src/deployments/deploy.ts +48 -19
  88. package/src/deployments/driver.ts +8 -1
  89. package/src/deployments/gcp/driver.ts +9 -1
  90. package/src/deployments/gcp/survey.ts +3 -0
  91. package/src/deployments/knowledge.ts +119 -0
  92. package/src/deployments/prepare.ts +2 -2
  93. package/src/deployments/servable.ts +81 -0
  94. package/src/deployments/target.ts +3 -2
  95. package/src/deployments/upload.ts +2 -1
  96. package/src/dispatch/dispatch.ts +1 -1
  97. package/src/profile/authorization.ts +13 -4
  98. package/src/profile/identity.ts +60 -0
  99. package/src/profile/index.ts +15 -5
  100. package/src/profile/knowledge.ts +124 -0
  101. package/src/profile/load.ts +17 -5
  102. package/src/profile/primitives.ts +24 -1
  103. package/src/profile/schema.ts +72 -4
  104. package/src/profile/targets.ts +74 -114
  105. package/src/profile/workspace.ts +71 -84
  106. package/src/providers/google/calendar/index.ts +2 -0
  107. package/src/providers/google/contacts/index.ts +2 -0
  108. package/src/providers/google/docs/index.ts +2 -0
  109. package/src/providers/google/drive/index.ts +2 -0
  110. package/src/providers/google/gmail/index.ts +2 -0
  111. package/src/providers/google/gmail-imap/index.ts +125 -0
  112. package/src/providers/google/index.ts +2 -1
  113. package/src/providers/google/shared/oauth.ts +18 -6
  114. package/src/providers/google/shared/service-account.ts +110 -0
  115. package/src/providers/google/shared/setup.ts +5 -2
  116. package/src/providers/google/sheets/index.ts +2 -0
  117. package/src/providers/google/tasks/index.ts +2 -0
  118. package/src/providers/identity/provider.ts +166 -0
  119. package/src/providers/index.ts +3 -0
  120. package/src/providers/owner.ts +10 -2
  121. package/src/providers/scopes.ts +2 -0
  122. package/src/providers/setup/plan.ts +31 -9
  123. package/src/providers/setup/provider.ts +23 -0
  124. package/src/providers/slack/index.ts +81 -33
  125. package/src/providers/slack/oauth.ts +103 -0
  126. package/src/providers/slack/scopes.ts +37 -0
  127. package/src/server/container.ts +18 -1
  128. package/src/server/cors.ts +252 -0
  129. package/src/server/dashboard.ts +208 -0
  130. package/src/server/endpoint.ts +57 -4
  131. package/src/server/generation.ts +1 -0
  132. package/src/server/generations.ts +13 -2
  133. package/src/server/harness.ts +20 -3
  134. package/src/server/index.ts +76 -12
  135. package/src/server/mcp/build.ts +1 -1
  136. package/src/server/mcp/index.ts +1 -0
  137. package/src/server/mcp/instructions.ts +62 -5
  138. package/src/server/mcp/visibility.ts +42 -0
  139. package/src/stores/blobs/route.ts +123 -0
@@ -0,0 +1,357 @@
1
+ import {
2
+ ConfigError,
3
+ listProfiles,
4
+ loadProfileConfig,
5
+ noProfileNamed,
6
+ noTargetNamed,
7
+ resolveWorkspaceRoot,
8
+ } from '#profile';
9
+ import type { Flags } from './argv.ts';
10
+
11
+ /**
12
+ * Which commands must name a profile and a target, and which flags each accepts.
13
+ *
14
+ * Two rules, in one file because they fail for the same reason and the fix for
15
+ * one makes the other legible.
16
+ *
17
+ * **A flag that is silently ignored is the defect.** `lanes link profile add
18
+ * work --target cloud` printed `ok` and dropped the flag: `main.ts` built a
19
+ * literal for that command and never spread the global flags into it. Nothing
20
+ * refused, because nothing had a list of what the command accepts. That is what
21
+ * `assertKnownFlags` is — and it matters more than the requirement, because
22
+ * required flags make a typo *worse* on their own. `--porfile work` used to
23
+ * fall through to a workspace default and mostly work; with a requirement and
24
+ * no allowlist it produces "--profile is required", naming a flag the operator
25
+ * believes they just passed.
26
+ *
27
+ * **A selection is named or the command does not run** (ADR-037). The table
28
+ * below is the whole rule, and `selection.test.ts` reads `main.ts` to check that
29
+ * every dispatched command appears in it — so a new command cannot quietly
30
+ * default to requiring nothing.
31
+ */
32
+
33
+ /** What a command must be told before it can act. */
34
+ export type Requires = 'none' | 'profile' | 'profile+target';
35
+
36
+ /**
37
+ * The rule, per command path.
38
+ *
39
+ * `--profile` for anything that reads or writes a profile's config or stores.
40
+ * `--target` for anything that opens a target's adapters or acts against a
41
+ * target's endpoint. A command that names a target positionally or through
42
+ * `--from`/`--to` supplies it that way and is not asked twice.
43
+ *
44
+ * Three entries are worth defending, because uniformity would be wrong:
45
+ *
46
+ * `check`, `config show` and `policy list` take no `--target`. All three are
47
+ * target-independent — a YAML file, the whole of it, and a policy block that is
48
+ * declared once and applies everywhere. Demanding a target would be the
49
+ * ceremony that teaches people to type `--target local` without reading it,
50
+ * which is how a required flag stops being a guard.
51
+ *
52
+ * `target list` takes no required `--target` either, and that is not an
53
+ * oversight: it is the command you run to find out what to pass. Requiring the
54
+ * answer as input is circular, and it has to keep working in the state every
55
+ * other command fails in.
56
+ *
57
+ * `profile add` **rejects** both. The name is positional, and there is no
58
+ * profile to select before it exists.
59
+ */
60
+ export const SELECTION: Record<string, Requires> = {
61
+ help: 'none',
62
+ version: 'none',
63
+ update: 'none',
64
+ skill: 'none',
65
+ 'mcp skill': 'none',
66
+ 'mcp list': 'none',
67
+ // The bare forms, which each dispatch to a `case undefined` in `main.ts`.
68
+ // `lanes link profile` is `profile list`, and needs the same as it.
69
+ profile: 'none',
70
+ mcp: 'none',
71
+ 'profile list': 'none',
72
+ 'profile add': 'none',
73
+ 'profile default': 'none',
74
+ 'target use': 'none',
75
+ 'vault key': 'none',
76
+
77
+ check: 'profile',
78
+ config: 'profile',
79
+ policy: 'profile',
80
+ target: 'profile',
81
+ 'config show': 'profile',
82
+ 'policy list': 'profile',
83
+ 'target list': 'profile',
84
+ 'target show': 'profile',
85
+ 'secrets push': 'profile',
86
+ 'profile remove': 'profile',
87
+ // Target-independent for the same reason `policy list` is: the block is
88
+ // declared once in the YAML and applies to every target the profile has.
89
+ identity: 'profile',
90
+ 'identity list': 'profile',
91
+
92
+ connect: 'profile+target',
93
+ setup: 'profile+target',
94
+ token: 'profile+target',
95
+ audit: 'profile+target',
96
+ secrets: 'profile+target',
97
+ plan: 'profile+target',
98
+ doctor: 'profile+target',
99
+ status: 'profile+target',
100
+ outputs: 'profile+target',
101
+ tools: 'profile+target',
102
+ // It reads which target it is rendering for before it decides anything: a
103
+ // deployed one has no page to open, and the refusal has to name it.
104
+ dashboard: 'profile+target',
105
+ attach: 'profile+target',
106
+ start: 'profile+target',
107
+ deploy: 'profile+target',
108
+ 'policy allow': 'profile+target',
109
+ 'policy deny': 'profile+target',
110
+ // Both, unlike `identity list`, and for the same reason the policy edits are:
111
+ // each publishes the edit, which opens the target's credential store and
112
+ // reaches that target's endpoint.
113
+ 'identity add': 'profile+target',
114
+ 'identity remove': 'profile+target',
115
+ 'token show': 'profile+target',
116
+ 'token rotate': 'profile+target',
117
+ 'audit tail': 'profile+target',
118
+ 'audit verify': 'profile+target',
119
+ 'secrets set': 'profile+target',
120
+ 'secrets list': 'profile+target',
121
+ 'mcp add': 'profile+target',
122
+ 'mcp stdio': 'profile+target',
123
+ memory: 'profile+target',
124
+ skills: 'profile+target',
125
+ vault: 'profile+target',
126
+ // Both halves open the target's adapters — `show` counts what is in the
127
+ // stores, and `use` migrates between them — and both edit the profile's
128
+ // config. Neither can be answered without being told which.
129
+ knowledge: 'profile+target',
130
+ };
131
+
132
+ /**
133
+ * The second words each command accepts.
134
+ *
135
+ * Only the commands that have subcommands appear. This exists for one reason:
136
+ * the checks below run before the switch, so without it `lanes link vault bogus`
137
+ * is refused for a missing `--profile` rather than for the subcommand that does
138
+ * not exist — a usage error reported as the wrong usage error, which is its own
139
+ * small version of the bug being fixed. `selection.test.ts` reads `main.ts` and
140
+ * asserts this stays true.
141
+ */
142
+ const SUBCOMMANDS: Record<string, readonly string[]> = {
143
+ profile: ['add', 'list', 'default', 'remove'],
144
+ target: ['list', 'use', 'show'],
145
+ policy: ['list', 'allow', 'deny'],
146
+ identity: ['add', 'list', 'remove'],
147
+ token: ['show', 'rotate'],
148
+ audit: ['tail', 'verify'],
149
+ config: ['show'],
150
+ setup: ['plan'],
151
+ memory: ['list', 'get', 'write', 'forget'],
152
+ skills: ['list', 'show', 'add', 'remove'],
153
+ vault: ['list', 'get', 'set', 'remove', 'key'],
154
+ mcp: ['skill', 'add', 'stdio', 'list'],
155
+ secrets: ['push', 'set', 'list'],
156
+ knowledge: ['show', 'use'],
157
+ };
158
+
159
+ /**
160
+ * Whether the switch is going to refuse this command path anyway.
161
+ *
162
+ * When it is, these checks stay quiet and let it: "Unknown: lanes link vault
163
+ * bogus" is the useful sentence, and a complaint about `--profile` on a command
164
+ * that does not exist sends someone off to fix the wrong thing.
165
+ */
166
+ function dispatchWillRefuse(first: string, second: string | undefined): boolean {
167
+ const known = SUBCOMMANDS[first];
168
+ if (!known || second === undefined) return false;
169
+ return !known.includes(second);
170
+ }
171
+
172
+ /**
173
+ * The key for a command, longest match first.
174
+ *
175
+ * `token show` before `token`, so a two-word command can differ from its
176
+ * siblings without every sibling having to be listed.
177
+ */
178
+ export function selectionKey(first: string, second: string | undefined): string {
179
+ const pair = second ? `${first} ${second}` : first;
180
+ if (pair in SELECTION) return pair;
181
+ return first;
182
+ }
183
+
184
+ /** Whether this command needs a profile, a target, both, or neither. */
185
+ export function requirementFor(first: string, second: string | undefined): Requires {
186
+ return SELECTION[selectionKey(first, second)] ?? 'profile+target';
187
+ }
188
+
189
+ /**
190
+ * Refuse before the command runs, naming what it wants and what there is.
191
+ *
192
+ * Async, and it reads the workspace — but only on the way to throwing. The
193
+ * useful half of "which profile did you mean" is the list of them, and the same
194
+ * for targets; a refusal that only restates the flag name leaves someone to go
195
+ * and look it up. Both messages come from `#profile` so this file and the
196
+ * resolver cannot describe the same refusal differently, and both name an
197
+ * exported variable that no longer counts — the shell still configured for the
198
+ * old world is the state hardest to diagnose from the inside.
199
+ */
200
+ export async function requireSelection(
201
+ first: string,
202
+ second: string | undefined,
203
+ flags: Flags,
204
+ env?: Record<string, string | undefined>,
205
+ ): Promise<void> {
206
+ if (dispatchWillRefuse(first, second)) return;
207
+
208
+ const needs = requirementFor(first, second);
209
+ if (needs === 'none') return;
210
+
211
+ const profile = flags['profile'];
212
+ if (typeof profile !== 'string') {
213
+ const root = resolveWorkspaceRoot(env ? { env } : {});
214
+ throw noProfileNamed(root, await listProfiles(root), env);
215
+ }
216
+
217
+ if (needs !== 'profile+target' || typeof flags['target'] === 'string') return;
218
+
219
+ // The profile is known by here, so the target list is the one belonging to it
220
+ // rather than a guess. A profile that does not exist is a different refusal,
221
+ // and `resolveSelection` gives it a better one a moment later.
222
+ const root = resolveWorkspaceRoot(env ? { env } : {});
223
+ try {
224
+ const { config } = await loadProfileConfig(root, profile);
225
+ throw noTargetNamed(config, profile, env);
226
+ } catch (error) {
227
+ if (error instanceof ConfigError) throw error;
228
+ throw new ConfigError(`--target is required for "${[first, second].filter(Boolean).join(' ')}".`);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Flags every command accepts, whatever it does.
234
+ *
235
+ * `--help` short-circuits before dispatch, and `--json` is offered widely enough
236
+ * that listing it per command would be noise. `--quiet` is read by `announce`
237
+ * rather than by any one command.
238
+ */
239
+ const UNIVERSAL = ['help', 'json', 'quiet'];
240
+
241
+ /**
242
+ * What each command accepts beyond the universal set and its own selection.
243
+ *
244
+ * Only commands with flags of their own appear. Anything absent accepts the
245
+ * universal set plus whatever `SELECTION` says it must be told.
246
+ */
247
+ const ACCEPTS: Record<string, readonly string[]> = {
248
+ // `own-client` is the older spelling of one of the routes `auth` names, kept
249
+ // because it is in scripts and a year of documentation (ADR-038).
250
+ connect: [
251
+ 'id',
252
+ 'display-name',
253
+ 'replace',
254
+ 'non-interactive',
255
+ 'accept-broad-scopes',
256
+ 'own-client',
257
+ 'auth',
258
+ ],
259
+ setup: ['id'],
260
+ 'profile add': ['target', 'non-interactive'],
261
+ 'profile remove': ['dry-run', 'yes'],
262
+ 'target list': ['urls', 'target'],
263
+ 'target show': ['target'],
264
+ 'token show': ['show', 'raw'],
265
+ 'token rotate': ['show', 'raw', 'yes'],
266
+ 'audit tail': ['limit', 'denied-only', 'format'],
267
+ 'audit verify': ['limit', 'format'],
268
+ attach: ['connection'],
269
+ outputs: ['show'],
270
+ start: ['port', 'only'],
271
+ 'mcp stdio': ['only'],
272
+ 'mcp add': ['name', 'scope', 'token-env', 'dry-run', 'force', 'no-skill'],
273
+ 'mcp skill': ['print', 'force'],
274
+ dashboard: ['print'],
275
+ skill: ['print', 'force'],
276
+ deploy: ['dry-run', 'iam', 'access', 'service-account', 'tag', 'yes', 'non-interactive'],
277
+ 'secrets push': ['from', 'to', 'overwrite', 'dry-run'],
278
+ update: ['check'],
279
+ 'identity add': ['note'],
280
+ memory: ['connection', 'title', 'description', 'file'],
281
+ skills: ['connection', 'title', 'description', 'file'],
282
+ vault: ['connection'],
283
+ // `no-migrate` is listed beside `migrate` because they are three states
284
+ // rather than two: neither one asks, and a run with no terminal has to be
285
+ // able to say which it meant (ADR-041).
286
+ knowledge: ['repo', 'branch', 'path', 'migrate', 'no-migrate', 'keep', 'allow-public', 'replace', 'yes'],
287
+ };
288
+
289
+ /**
290
+ * Refuse a flag this command does not read, and guess what was meant.
291
+ *
292
+ * This is the fix for the reported bug rather than a nicety. `profile add
293
+ * --target cloud` was accepted and dropped, and nothing could refuse it because
294
+ * `parseArgv` returns every `--anything` it sees and no command ever inspected
295
+ * the leftovers. A typo was swallowed the same way on every command in the CLI.
296
+ */
297
+ export function assertKnownFlags(first: string, second: string | undefined, flags: Flags): void {
298
+ if (dispatchWillRefuse(first, second)) return;
299
+
300
+ const key = selectionKey(first, second);
301
+ const needs = SELECTION[key] ?? 'profile+target';
302
+
303
+ const allowed = new Set<string>([
304
+ ...UNIVERSAL,
305
+ ...(ACCEPTS[key] ?? []),
306
+ ...(needs === 'profile' || needs === 'profile+target' ? ['profile'] : []),
307
+ ...(needs === 'profile+target' ? ['target'] : []),
308
+ ]);
309
+
310
+ const named = [first, second].filter(Boolean).join(' ');
311
+
312
+ for (const given of Object.keys(flags)) {
313
+ if (allowed.has(given)) continue;
314
+
315
+ throw new ConfigError(
316
+ `Unknown flag "--${given}" for "lanes link ${named}".` +
317
+ (nearest(given, allowed) ? `\n Did you mean --${nearest(given, allowed)}?` : '') +
318
+ `\n Accepts: ${[...allowed].sort().map((name) => `--${name}`).join(' ')}`,
319
+ );
320
+ }
321
+ }
322
+
323
+ /**
324
+ * The closest accepted flag, when there is an obviously close one.
325
+ *
326
+ * One edit away, or one transposition — enough for `--porfile` and `--taget`,
327
+ * and short of guessing at something the operator did not mean. A wrong guess
328
+ * here costs more than no guess: it sends them to a flag that is not the answer.
329
+ */
330
+ function nearest(given: string, allowed: ReadonlySet<string>): string | undefined {
331
+ for (const candidate of allowed) {
332
+ if (Math.abs(candidate.length - given.length) > 1) continue;
333
+ if (distance(given, candidate) <= 1) return candidate;
334
+ if (sorted(given) === sorted(candidate)) return candidate;
335
+ }
336
+ return undefined;
337
+ }
338
+
339
+ const sorted = (text: string): string => [...text].sort().join('');
340
+
341
+ function distance(a: string, b: string): number {
342
+ let row = Array.from({ length: b.length + 1 }, (_, index) => index);
343
+
344
+ for (let i = 1; i <= a.length; i++) {
345
+ const next = [i];
346
+ for (let j = 1; j <= b.length; j++) {
347
+ next[j] = Math.min(
348
+ row[j]! + 1,
349
+ next[j - 1]! + 1,
350
+ row[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1),
351
+ );
352
+ }
353
+ row = next;
354
+ }
355
+
356
+ return row[b.length]!;
357
+ }
package/src/cli/usage.ts CHANGED
@@ -24,11 +24,13 @@ ${style.bold('Everyday')}
24
24
  ${PROGRAM} connect <provider> add an account (run once per account)
25
25
  ${PROGRAM} connect <provider>.<id> re-authorise one existing account
26
26
  ${PROGRAM} connect <...> --replace ask for the stored password or key again
27
+ ${PROGRAM} connect <...> --auth <method> pick how, where there is a choice
27
28
  ${PROGRAM} connect <...> --non-interactive [--json]
28
29
  answer nothing from a terminal: take every value
29
30
  from the credential store, or say what is missing
30
31
  ${PROGRAM} start [--only] reconcile and serve every profile on one endpoint
31
32
  ${PROGRAM} outputs [--show] [--json] the endpoint an agent needs
33
+ ${PROGRAM} dashboard [--print] open the local endpoint's page in a browser
32
34
  ${PROGRAM} mcp add [claude|codex] register this endpoint, and install the agent skill
33
35
  ${PROGRAM} mcp add --no-skill register only, leaving the agent's own files alone
34
36
  ${PROGRAM} mcp list where it is registered, and whether the skill is current
@@ -37,16 +39,22 @@ ${style.bold('Everyday')}
37
39
  ${PROGRAM} status [--json] connections, reachable capabilities, endpoint
38
40
 
39
41
  ${style.bold('Profiles')}
40
- ${PROGRAM} profile add <name> [--default] [--json]
42
+ ${PROGRAM} profile add <name> --target <name> [--target <name>] [--json]
43
+ a target per place it runs; local is derived, the
44
+ rest are copied from a sibling profile
41
45
  ${PROGRAM} profile list [--json]
42
- ${PROGRAM} profile default <name>
43
46
  ${PROGRAM} profile remove <name> [--target t] [--dry-run] [--yes] [--json]
44
47
  the profile, its credentials, and its data
45
48
 
46
49
  ${style.bold('Targets')}
47
- ${PROGRAM} target list [--urls] where this profile can run, and which one is in play
48
- ${PROGRAM} target show [<name>] one target's adapters, and the address it answers on
49
- ${PROGRAM} target use <name> make one the profile's default_target
50
+ ${PROGRAM} target list [--urls] where this profile can run
51
+ ${PROGRAM} target show <name> one target's adapters, and the address it answers on
52
+
53
+ ${style.bold('Who you are')}
54
+ ${PROGRAM} identity add <kind> <value> [--note text] [--json]
55
+ e.g. name, email, github — any kind you like
56
+ ${PROGRAM} identity list [--json]
57
+ ${PROGRAM} identity remove <kind> <value> [--json]
50
58
 
51
59
  ${style.bold('Permissions')}
52
60
  ${PROGRAM} policy list
@@ -66,6 +74,14 @@ ${style.bold('Your own context')}
66
74
  ${PROGRAM} skills add <name> [--file f] document on stdin
67
75
  ${PROGRAM} skills remove <name>
68
76
 
77
+ ${PROGRAM} knowledge show where memory and skills are kept, and how many
78
+ ${PROGRAM} knowledge use github --repo <owner/name> [--branch b] [--path p]
79
+ keep both in a private repository, over the GitHub API
80
+ [--migrate] moves what is already stored, in one commit
81
+ [--no-migrate] switches and leaves it where it is
82
+ [--keep] moves it, and leaves the local copies unread
83
+ ${PROGRAM} knowledge use local [--migrate] bring them back onto this target
84
+
69
85
  ${PROGRAM} vault list names only, never values
70
86
  ${PROGRAM} vault get <id> [--show|--raw]
71
87
  ${PROGRAM} vault set <id> [--description d] value on stdin
@@ -96,9 +112,13 @@ ${style.bold('Attachments')}
96
112
  ${PROGRAM} attach <file> --connection <provider>.<account>
97
113
  stage a file, print a handle to send it by
98
114
 
99
- ${style.bold('Global flags')}
100
- --profile <name> overrides LANES_LINK_PROFILE and the workspace default
101
- --target <name> overrides LANES_LINK_TARGET and instance.default_target
115
+ ${style.bold('Naming what a command acts on')}
116
+ --profile <name> required by every command that reads or writes a profile
117
+ --target <name> required by every command that opens a target's stores.
118
+ There is no default and no environment variable: a
119
+ command that names neither refuses and lists what exists.
120
+
121
+ ${style.bold('Other flags')}
102
122
  --connection <id> which memory/skills/vault connection, if a profile has several
103
123
  --yes skip the confirmation a destructive command would ask for
104
124
  --json machine-readable output, where a command offers it
@@ -107,7 +127,10 @@ ${style.bold('Global flags')}
107
127
  --accept-broad-scopes agree in advance to scopes broader than a provider needs
108
128
  --own-client register your own OAuth client instead of using the
109
129
  one this project operates (connect only)
130
+ --auth <method> which way in, where a provider offers two (connect
131
+ only). "oauth" is the browser; the other is named
132
+ in the choice connect prints
110
133
  --port <n> override the configured port (start only)
111
134
 
112
- Every command prints the resolved profile and target before acting.
135
+ Every command prints the profile and target it is acting on, before it acts.
113
136
  `;
@@ -14,6 +14,7 @@ transport asks when it has a token to send and no request to attach it to.
14
14
  | `api-key/` | `api_key` | a key, in a header or the query string |
15
15
  | `basic/` | `basic` | `username:password`, RFC 7617's own encoding |
16
16
  | `oauth-authcode/` | `oauth` | a refresh token, exchanged on every use |
17
+ | `oauth-jwt/` | `oauth` + `assertion` | a private key, signed into an assertion per exchange |
17
18
  | `strategy/` | `strategy` | the escape hatch — per-vendor code, none registered |
18
19
 
19
20
  ## Adding one
@@ -22,13 +23,18 @@ A folder, a member of `authSchema` in `../manifest/auth.ts`, and a case in
22
23
  `resolve.ts` (plus `authorize.ts` if it touches the request). Nothing else in
23
24
  the codebase learns about it — that is the point of the split.
24
25
 
26
+ `oauth-jwt/` is the exception that proves the shape rather than breaking it. It
27
+ is not a `kind`, because it is a second way into a provider that already has
28
+ one, so it hangs off the OAuth block as `auth.assertion` and is selected by the
29
+ shape of the stored credential. Everything else about it is an ordinary folder
30
+ here.
31
+
25
32
  These are named in the credential-type list this design is measured against and
26
33
  are **not built**:
27
34
 
28
35
  - `sigv4/` — AWS SigV4 request signing
29
36
  - `gcp-token/` — service account → GCP access token
30
37
  - `gcp-iap/` — service account → an IAP-signed JWT
31
- - `oauth-jwt/` — OAuth 2.0 JWT bearer (RFC 7523)
32
38
  - `oauth-client-creds/` — OAuth 2.0 client credentials
33
39
  - `body-param/` — the credential as a request body parameter
34
40
 
@@ -15,7 +15,7 @@ export function resolveBasic(value: string, ref: string, providerId: string): Re
15
15
  if (colon === -1) {
16
16
  throw new Error(
17
17
  `The credential at ${ref} is not a "username:password" pair, which basic auth requires. ` +
18
- `Re-run: lanes link connect ${providerId}`,
18
+ `Connect "${providerId}" again for this profile and target.`,
19
19
  );
20
20
  }
21
21
 
@@ -7,6 +7,10 @@
7
7
  * know the whole set: `resolve.ts` and `authorize.ts` for anything HTTP-shaped,
8
8
  * and `token.ts` for a transport that takes a bare token instead of a request.
9
9
  *
10
+ * `oauth-jwt/` is the one folder that is not a `kind`: it is a second way into
11
+ * a provider that already declares `oauth`, selected by the shape of what is
12
+ * stored rather than by the manifest. Its own README says why.
13
+ *
10
14
  * This is the axis the manifest's `auth:` block selects, and it is deliberately
11
15
  * independent of `../transports/` — which is why iCloud can speak IMAP with a
12
16
  * password while Gmail speaks HTTP with OAuth, and neither costs the other any
@@ -24,9 +28,19 @@ export {
24
28
  type OAuthProviderOptions,
25
29
  } from './oauth-authcode/provider.ts';
26
30
  export { resolveUpstreamToken } from './oauth-authcode/index.ts';
31
+ export {
32
+ ASSERTION_GRANT,
33
+ clearMintedTokens,
34
+ isStoredAssertion,
35
+ resolveAssertionToken,
36
+ storedAssertionFor,
37
+ type StoredAssertion,
38
+ } from './oauth-jwt/index.ts';
39
+ export { assertionKeySchema, parseAssertionKey, signAssertion, type AssertionKey } from './oauth-jwt/key.ts';
27
40
  export {
28
41
  BROKER_ORIGIN_ENV,
29
42
  BROKERED,
43
+ PASTED,
30
44
  BrokerError,
31
45
  brokerConfig,
32
46
  brokerExchange,
@@ -20,6 +20,17 @@
20
20
  */
21
21
  export const BROKERED = 'broker';
22
22
 
23
+ /**
24
+ * Stamped on a credential nobody here minted — the operator pasted it.
25
+ *
26
+ * The third answer, and the one that is not a client at all. It matters for the
27
+ * same reason as the other two and one more: a pasted token cannot be
28
+ * refreshed, cannot be attributed to a registration, and cannot be re-obtained
29
+ * by re-running a flow. `doctor` reads it to say so rather than offering a
30
+ * re-authorisation that would not apply.
31
+ */
32
+ export const PASTED = 'pasted';
33
+
23
34
  /** What the broker will authorise, and whether it is currently doing so. */
24
35
  export interface BrokerConfig {
25
36
  readonly clientId: string;
@@ -27,6 +38,20 @@ export interface BrokerConfig {
27
38
  readonly scopesSupported: readonly string[];
28
39
  /** Added to every request so the exchange returns an identity assertion. */
29
40
  readonly identityScopes: readonly string[];
41
+ /**
42
+ * Where the vendor sends the browser back, when it will not send it here.
43
+ *
44
+ * Absent for a vendor that accepts a loopback redirect, which is all of them
45
+ * but Slack: the listener names itself and the broker is only asked to
46
+ * redeem. Present where the vendor demands HTTPS — then the redirect lands on
47
+ * the broker's own origin and is bounced down to the listener, and this is
48
+ * the URL both legs of the flow have to agree on.
49
+ *
50
+ * Published rather than derived, because which URL is correct depends on
51
+ * which deployment answered `/config` — and a broker running on loopback for
52
+ * a test would otherwise need a flag of its own.
53
+ */
54
+ readonly redirectUri: string | undefined;
30
55
  readonly open: boolean;
31
56
  /** Why it is closed, or near capacity. The broker's words, printed verbatim. */
32
57
  readonly notice: string | undefined;
@@ -217,6 +242,7 @@ export async function brokerConfig(
217
242
  clientId,
218
243
  scopesSupported: strings(data['scopes_supported']),
219
244
  identityScopes: strings(data['identity_scopes']),
245
+ redirectUri: str(data['redirect_uri']),
220
246
  open: data['status'] !== 'closed',
221
247
  notice: str(data['notice']),
222
248
  docsUrl: str(data['docs_url']),
@@ -1,6 +1,7 @@
1
1
  import { auth } from '@modelcontextprotocol/client';
2
2
  import type { ProviderManifest } from '#connectivity';
3
3
  import type { SecretStore } from '#secrets';
4
+ import { resolveAssertionToken, storedAssertionFor } from '../oauth-jwt/index.ts';
4
5
  import { CredentialOAuthProvider, upstreamAccessToken } from './provider.ts';
5
6
  import { refreshDirectly } from './refresh.ts';
6
7
 
@@ -12,8 +13,11 @@ import { refreshDirectly } from './refresh.ts';
12
13
  * trip at connect time, a refresh on every use, and two different ways to run
13
14
  * that refresh depending on whether the provider has a metadata document.
14
15
  *
15
- * The other flows the credential-type list names — JWT bearer, client
16
- * credentials — are sibling folders that do not exist yet. See ../README.md.
16
+ * The other flows the credential-type list names — client credentials, SigV4 —
17
+ * are sibling folders that do not exist yet. See ../README.md. JWT bearer now
18
+ * does exist, in `../oauth-jwt/`, and is reached from here rather than from
19
+ * `resolve.ts`: a provider offering both declares one `auth.kind`, so the fork
20
+ * belongs at the point where the stored credential is first read.
17
21
  */
18
22
 
19
23
  export async function resolveUpstreamToken(
@@ -23,6 +27,16 @@ export async function resolveUpstreamToken(
23
27
  ): Promise<string | null> {
24
28
  if (manifest.auth.kind !== 'oauth') return null;
25
29
 
30
+ // Before anything is built, because the two arrangements share a ref and only
31
+ // what is stored there tells them apart. A `CredentialOAuthProvider` over an
32
+ // assertion pointer would find no `access_token`, conclude the connection was
33
+ // never authorised, and advise a browser flow the operator deliberately
34
+ // declined.
35
+ const assertion = await storedAssertionFor(manifest, connectionId, credentials);
36
+ if (assertion) {
37
+ return resolveAssertionToken({ manifest, connectionId, stored: assertion, credentials });
38
+ }
39
+
26
40
  const provider = new CredentialOAuthProvider({
27
41
  manifest,
28
42
  connectionId,
@@ -133,7 +133,7 @@ export class CredentialOAuthProvider {
133
133
  if (!this.#options.openBrowser) {
134
134
  throw new Error(
135
135
  `Connection ${this.#options.manifest.id}.${this.#options.connectionId} needs re-authorisation, ` +
136
- `which requires a browser. Run: lanes link connect ${this.#options.manifest.id}.${this.#options.connectionId}`,
136
+ `which requires a browser. Connect ${this.#options.manifest.id}.${this.#options.connectionId} again for this profile and target.`,
137
137
  );
138
138
  }
139
139
  this.#options.openBrowser(authorizationUrl);
@@ -24,7 +24,7 @@ export async function refreshDirectly(
24
24
 
25
25
  if (!refreshToken) {
26
26
  throw new Error(
27
- `No refresh token stored for ${manifest.id}. Run: lanes link connect ${manifest.id}`,
27
+ `No refresh token stored for ${manifest.id}. Connecting it again for this profile and target would store one.`,
28
28
  );
29
29
  }
30
30
 
@@ -72,7 +72,7 @@ async function viaBroker(
72
72
  const notice = cause instanceof BrokerError && cause.notice ? `\n${cause.notice}` : '';
73
73
  throw new Error(
74
74
  `The credential for ${manifest.id} could not be refreshed. ` +
75
- `Re-authorise with: lanes link connect ${manifest.id}\n${String(
75
+ `Re-authorise ${manifest.id} for this profile and target.\n${String(
76
76
  cause instanceof Error ? cause.message : cause,
77
77
  ).slice(0, 200)}${notice}`,
78
78
  );
@@ -110,7 +110,7 @@ async function viaStoredClient(
110
110
  // is always the same, so say it rather than surfacing the raw grant error.
111
111
  throw new Error(
112
112
  `The credential for ${manifest.id} could not be refreshed (${response.status}). ` +
113
- `Re-authorise with: lanes link connect ${manifest.id}\n${text.slice(0, 200)}`,
113
+ `Re-authorise ${manifest.id} for this profile and target.\n${text.slice(0, 200)}`,
114
114
  );
115
115
  }
116
116
 
@@ -0,0 +1,33 @@
1
+ # OAuth 2.0 JWT bearer (RFC 7523)
2
+
3
+ A key the operator holds, signed into a short-lived assertion and exchanged for
4
+ an access token. No browser, and **no refresh token** — there is nothing to
5
+ refresh, because a new assertion is signed whenever the last token ages out.
6
+ That is the point: an authorization-code refresh token lives or dies by the
7
+ issuer's policy, and a key does not.
8
+
9
+ Not a `kind` of its own. It is declared as `auth.assertion` on an existing
10
+ `oauth` block, because it is a second arrangement for the same provider rather
11
+ than a different provider — so `credentialRefForConnection`, `setupRequirements`
12
+ and the deploy grants all stay as they were. Which arrangement a connection uses
13
+ is decided by the *shape* of what is stored at `<provider>/<connection>`:
14
+ `isStoredAssertion` is that test, and `resolve.ts` asks it before building an
15
+ authorization-code provider.
16
+
17
+ | File | What it owns |
18
+ |---|---|
19
+ | `key.ts` | the key file's layout, PEM to DER, and the signed claim set — no I/O |
20
+ | `index.ts` | reading the pointer, the exchange, the process-lifetime token cache |
21
+
22
+ The endpoint comes from `token_uri` **inside the key file**, never from a
23
+ constant here, which is what keeps this folder free of any vendor. A second
24
+ vendor offering the same grant is a manifest and no code.
25
+
26
+ ## What it cannot do
27
+
28
+ An assertion authenticates the key, and a key is not a person. It reaches only
29
+ what has been shared with its address — unless the identity provider is
30
+ configured to let it act as someone, which is an administrator's grant and not
31
+ the operator's. `auth.assertion.delegation` says which of the two a provider is,
32
+ and `cli/commands/connect/method.ts` is where that becomes a sentence someone
33
+ reads before choosing.