@lanes-sh/link 0.2.2 → 0.3.1

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 (143) hide show
  1. package/README.md +22 -8
  2. package/instructions/skills/lanes-link/SKILL.md +42 -14
  3. package/package.json +1 -1
  4. package/src/cli/argv.ts +50 -0
  5. package/src/cli/brand.ts +178 -0
  6. package/src/cli/callback-page.ts +108 -128
  7. package/src/cli/commands/connect/accounts.ts +5 -0
  8. package/src/cli/commands/connect/assertion.ts +187 -0
  9. package/src/cli/commands/connect/authorise.ts +61 -17
  10. package/src/cli/commands/connect/client.ts +37 -9
  11. package/src/cli/commands/connect/discover.ts +94 -0
  12. package/src/cli/commands/connect/family.ts +72 -0
  13. package/src/cli/commands/connect/index.ts +113 -115
  14. package/src/cli/commands/connect/method.ts +237 -0
  15. package/src/cli/commands/connect/outcome.ts +42 -1
  16. package/src/cli/commands/connect/pasted-token.ts +66 -0
  17. package/src/cli/commands/connect/requirements.ts +60 -8
  18. package/src/cli/commands/connect/setup.ts +16 -5
  19. package/src/cli/commands/connect/target-note.ts +34 -0
  20. package/src/cli/commands/identity.ts +258 -0
  21. package/src/cli/commands/knowledge/index.ts +390 -0
  22. package/src/cli/commands/knowledge/migrate.ts +180 -0
  23. package/src/cli/commands/knowledge/setup.ts +144 -0
  24. package/src/cli/commands/knowledge.ts +10 -0
  25. package/src/cli/commands/mcp/harnesses.ts +16 -2
  26. package/src/cli/commands/mcp/register.ts +9 -1
  27. package/src/cli/commands/mcp/stdio.ts +21 -0
  28. package/src/cli/commands/operate/dashboard.ts +107 -0
  29. package/src/cli/commands/operate/findings.ts +151 -0
  30. package/src/cli/commands/operate/inspect.ts +63 -164
  31. package/src/cli/commands/operate/outputs.ts +38 -11
  32. package/src/cli/commands/operate/policy.ts +7 -7
  33. package/src/cli/commands/operate/serve.ts +3 -0
  34. package/src/cli/commands/operate/status.ts +108 -1
  35. package/src/cli/commands/operate/token.ts +1 -1
  36. package/src/cli/commands/operate.ts +2 -0
  37. package/src/cli/commands/profile/declare.ts +154 -0
  38. package/src/cli/commands/profile/removal.ts +17 -0
  39. package/src/cli/commands/profile/remove.ts +5 -5
  40. package/src/cli/commands/profile.ts +83 -35
  41. package/src/cli/commands/secrets.ts +6 -6
  42. package/src/cli/commands/setup.ts +22 -6
  43. package/src/cli/commands/sync.ts +262 -0
  44. package/src/cli/commands/target.ts +65 -83
  45. package/src/cli/config-edit.ts +53 -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/dispatch-owner.ts +93 -0
  50. package/src/cli/identity.ts +12 -1
  51. package/src/cli/main.ts +90 -61
  52. package/src/cli/nearest.ts +45 -0
  53. package/src/cli/oauth-callback.ts +187 -0
  54. package/src/cli/oauth-exchange.ts +57 -15
  55. package/src/cli/oauth.ts +67 -177
  56. package/src/cli/output.ts +21 -5
  57. package/src/cli/provider-marks.ts +45 -0
  58. package/src/cli/runtime/open.ts +74 -51
  59. package/src/cli/runtime/registry.ts +60 -2
  60. package/src/cli/runtime/select.ts +26 -13
  61. package/src/cli/runtime/vault.ts +61 -0
  62. package/src/cli/runtime.ts +2 -1
  63. package/src/cli/selection.ts +365 -0
  64. package/src/cli/usage.ts +42 -11
  65. package/src/connectivity/auth/README.md +7 -1
  66. package/src/connectivity/auth/basic/index.ts +1 -1
  67. package/src/connectivity/auth/index.ts +14 -0
  68. package/src/connectivity/auth/oauth-authcode/broker.ts +26 -0
  69. package/src/connectivity/auth/oauth-authcode/index.ts +16 -2
  70. package/src/connectivity/auth/oauth-authcode/provider.ts +1 -1
  71. package/src/connectivity/auth/oauth-authcode/refresh.ts +3 -3
  72. package/src/connectivity/auth/oauth-jwt/README.md +33 -0
  73. package/src/connectivity/auth/oauth-jwt/index.ts +237 -0
  74. package/src/connectivity/auth/oauth-jwt/key.ts +148 -0
  75. package/src/connectivity/auth/resolve.ts +1 -1
  76. package/src/connectivity/auth/token.ts +11 -0
  77. package/src/connectivity/index.ts +2 -0
  78. package/src/connectivity/manifest/auth.ts +99 -2
  79. package/src/connectivity/manifest/identity.ts +12 -0
  80. package/src/connectivity/manifest/index.ts +3 -1
  81. package/src/connectivity/manifest/provider.ts +37 -8
  82. package/src/connectivity/manifest/requirements.ts +109 -6
  83. package/src/deployments/adapters/filesystem.ts +10 -1
  84. package/src/deployments/adapters/github-api.ts +106 -0
  85. package/src/deployments/adapters/github-commit.ts +103 -0
  86. package/src/deployments/adapters/github-repo.ts +356 -0
  87. package/src/deployments/adapters/github-testing.ts +258 -0
  88. package/src/deployments/adapters/github.ts +125 -0
  89. package/src/deployments/deploy.ts +94 -114
  90. package/src/deployments/discover.ts +103 -0
  91. package/src/deployments/driver.ts +8 -1
  92. package/src/deployments/gcp/driver.ts +3 -1
  93. package/src/deployments/knowledge.ts +119 -0
  94. package/src/deployments/prepare.ts +12 -6
  95. package/src/deployments/report.ts +117 -0
  96. package/src/deployments/servable.ts +82 -0
  97. package/src/deployments/serving.ts +165 -0
  98. package/src/deployments/sync-apply.ts +276 -0
  99. package/src/deployments/sync.ts +136 -0
  100. package/src/deployments/target.ts +3 -2
  101. package/src/deployments/upload.ts +19 -12
  102. package/src/dispatch/dispatch.ts +1 -1
  103. package/src/profile/deployments.ts +80 -0
  104. package/src/profile/identity.ts +60 -0
  105. package/src/profile/index.ts +23 -5
  106. package/src/profile/knowledge.ts +124 -0
  107. package/src/profile/load.ts +17 -5
  108. package/src/profile/primitives.ts +24 -1
  109. package/src/profile/schema.ts +81 -3
  110. package/src/profile/targets.ts +122 -109
  111. package/src/profile/workspace.ts +139 -79
  112. package/src/providers/google/calendar/index.ts +2 -0
  113. package/src/providers/google/contacts/index.ts +2 -0
  114. package/src/providers/google/docs/index.ts +2 -0
  115. package/src/providers/google/drive/index.ts +2 -0
  116. package/src/providers/google/gmail/index.ts +2 -0
  117. package/src/providers/google/gmail-imap/index.ts +125 -0
  118. package/src/providers/google/index.ts +2 -1
  119. package/src/providers/google/shared/oauth.ts +18 -6
  120. package/src/providers/google/shared/service-account.ts +110 -0
  121. package/src/providers/google/shared/setup.ts +21 -3
  122. package/src/providers/google/sheets/index.ts +2 -0
  123. package/src/providers/google/tasks/index.ts +2 -0
  124. package/src/providers/identity/provider.ts +166 -0
  125. package/src/providers/index.ts +3 -0
  126. package/src/providers/owner.ts +10 -2
  127. package/src/providers/scopes.ts +2 -0
  128. package/src/providers/setup/plan.ts +31 -9
  129. package/src/providers/setup/provider.ts +23 -0
  130. package/src/providers/slack/index.ts +81 -33
  131. package/src/providers/slack/oauth.ts +103 -0
  132. package/src/providers/slack/scopes.ts +37 -0
  133. package/src/server/container.ts +18 -1
  134. package/src/server/cors.ts +252 -0
  135. package/src/server/dashboard.ts +208 -0
  136. package/src/server/endpoint.ts +45 -1
  137. package/src/server/generations.ts +11 -2
  138. package/src/server/harness.ts +7 -0
  139. package/src/server/index.ts +45 -6
  140. package/src/server/mcp/index.ts +1 -0
  141. package/src/server/mcp/instructions.ts +28 -1
  142. package/src/server/mcp/visibility.ts +33 -0
  143. package/src/stores/blobs/route.ts +123 -0
@@ -14,16 +14,28 @@
14
14
  export {
15
15
  SUPPORTED_CONTRACT,
16
16
  configSchema,
17
+ deploymentRecordSchema,
17
18
  workspaceSchema,
18
19
  type AuthorizationConfig,
19
20
  type Config,
20
21
  type ConnectionConfig,
21
22
  type DeployConfig,
23
+ type DeploymentRecord,
24
+ type IdentityEntry,
22
25
  type PolicyRuleConfig,
23
26
  type TargetConfig,
24
27
  type WorkspaceConfig,
25
28
  } from './schema.ts';
26
29
 
30
+ export {
31
+ KNOWLEDGE_LAYOUT,
32
+ knowledgeRoot,
33
+ knowledgeTargetSchema,
34
+ parseRepository,
35
+ type KnowledgeArea,
36
+ type KnowledgeConfig,
37
+ } from './knowledge.ts';
38
+
27
39
  export {
28
40
  ConfigError,
29
41
  loadConfigFile,
@@ -41,23 +53,28 @@ export {
41
53
 
42
54
  export {
43
55
  WORKSPACE_FILE,
44
- describeSelection,
45
56
  installRoot,
46
57
  listProfiles,
47
58
  loadProfileConfig,
59
+ loadWorkspaceProfiles,
60
+ noProfileNamed,
48
61
  profilePath,
49
62
  readWorkspace,
50
63
  resolveSelection,
51
64
  resolveWorkspaceRoot,
65
+ targetsByName,
52
66
  workspacePath,
67
+ type LoadedProfile,
68
+ type WorkspaceProfiles,
53
69
  } from './workspace.ts';
54
70
  export {
55
- TARGET_ENV,
56
- askedTarget,
57
- resolveDeployTarget,
58
- resolveTarget,
71
+ LEGACY_TARGET_ENV,
72
+ noTargetInWorkspace,
73
+ noTargetNamed,
74
+ requireTarget,
59
75
  undeclaredTarget,
60
76
  } from './targets.ts';
77
+ export { findDeployment, readDeployments, recordDeployment } from './deployments.ts';
61
78
  export {
62
79
  isRemoteWorkspace,
63
80
  readWorkspaceFile,
@@ -65,6 +82,7 @@ export {
65
82
  writeWorkspaceFile,
66
83
  } from './files.ts';
67
84
  export {
85
+ type ProfileSelection,
68
86
  type Resolution,
69
87
  type ResolveOptions,
70
88
  } from './workspace.ts';
@@ -0,0 +1,124 @@
1
+ import { z } from 'zod';
2
+ import { credentialRef } from './primitives.ts';
3
+
4
+ /**
5
+ * Where a profile's memory and skills live, when that is not where everything
6
+ * else lives.
7
+ *
8
+ * A target's `storage:` block names one backend for everything a profile holds
9
+ * — runtime state, the audit log, memory entries, skills, cached attachments.
10
+ * That is right for most of it and wrong for two: state and the log are
11
+ * artefacts of one installation, and memory entries and skills are documents
12
+ * the owner wrote. Documents want history, review, and to be readable from more
13
+ * than one machine, and the backend that suits an object nobody reads is not
14
+ * the backend that suits those.
15
+ *
16
+ * So this block moves exactly those two, and it is the whole of what it can
17
+ * move. There is no `credentials` or `vault` value here and there will not be:
18
+ * a repository is a place to publish, and those two hold the material whose
19
+ * entire value is that it is not published (`docs/detailed/security.md`). The
20
+ * exclusion is structural — a field that does not exist cannot be set by
21
+ * accident, by a flag, or by an operator following an example.
22
+ *
23
+ * `adapter` is an enum of one, the same shape `vaultTargetSchema` uses, so a
24
+ * second host is a case rather than a schema rewrite.
25
+ *
26
+ * Absent means what has always happened, which is why every field that could
27
+ * carry a default does. A profile written before this block existed loads
28
+ * unchanged and stores its memory and skills exactly where it did.
29
+ *
30
+ * ADR-041.
31
+ */
32
+
33
+ /**
34
+ * `owner/name`, as GitHub spells it everywhere a person types one.
35
+ *
36
+ * A URL is deliberately not accepted. The browser URL, the SSH remote, and this
37
+ * are three spellings of one value, and a schema that takes all three has to
38
+ * normalise them somewhere — at which point the file no longer says what the
39
+ * operator wrote. The CLI accepts a pasted URL and reduces it to this before
40
+ * writing, which puts the leniency where a person is typing rather than in the
41
+ * contract.
42
+ */
43
+ const repository = z
44
+ .string()
45
+ .regex(
46
+ /^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/,
47
+ 'must be "owner/name" — not a URL, and not a local path',
48
+ );
49
+
50
+ /**
51
+ * A directory inside the repository, for one that holds other things too.
52
+ *
53
+ * Normalised to no leading or trailing slash so `knowledge`, `/knowledge` and
54
+ * `knowledge/` are one prefix rather than three key spaces that look identical
55
+ * in a diff and address different objects.
56
+ */
57
+ const repositoryPath = z
58
+ .string()
59
+ .refine((value) => !value.split('/').includes('..'), {
60
+ message: 'must not contain ".." — it is a prefix inside the repository, not a path to walk',
61
+ })
62
+ .transform((value) => value.replace(/^\/+/, '').replace(/\/+$/, ''));
63
+
64
+ export const knowledgeTargetSchema = z.object({
65
+ adapter: z.literal('github'),
66
+ repo: repository,
67
+ /**
68
+ * Defaults at open time rather than here, to the repository's own default
69
+ * branch. Writing `main` into every generated config would be a guess, and a
70
+ * wrong one for anybody whose default is still `master` — a guess that then
71
+ * fails as "branch not found" rather than as "you did not say".
72
+ */
73
+ branch: z.string().min(1).optional(),
74
+ path: repositoryPath.optional(),
75
+ /**
76
+ * A reference, never the token.
77
+ *
78
+ * Its own ref rather than a reuse of the `github` provider connection's,
79
+ * which holds a token for GitHub's MCP server. That one needs Contents:
80
+ * *read*; this one needs Contents: *write*, has a different lifetime, and
81
+ * should be revocable on its own — revoking an MCP connection must not
82
+ * silently empty somebody's memory.
83
+ */
84
+ token_ref: credentialRef.default('knowledge/token'),
85
+ });
86
+
87
+ export type KnowledgeConfig = z.infer<typeof knowledgeTargetSchema>;
88
+
89
+ /**
90
+ * Where each area sits inside the repository.
91
+ *
92
+ * Two directories, named after the two things that move. `memory` is also the
93
+ * memory provider's own blob namespace — the prefix core scopes it into under
94
+ * the profile's blob root — which is why the same word does both jobs: the
95
+ * route that redirects it and the directory it lands in are the same fact, and
96
+ * spelling them separately is how they would come to disagree.
97
+ *
98
+ * Declared here, beside the schema, so the runtime that opens these stores and
99
+ * the command that migrates into them read one definition.
100
+ */
101
+ export const KNOWLEDGE_LAYOUT = {
102
+ memory: 'memory',
103
+ skills: 'skills',
104
+ } as const;
105
+
106
+ export type KnowledgeArea = keyof typeof KNOWLEDGE_LAYOUT;
107
+
108
+ /** The directory one area occupies, under the profile's optional path prefix. */
109
+ export function knowledgeRoot(knowledge: KnowledgeConfig, area: KnowledgeArea): string {
110
+ const directory = KNOWLEDGE_LAYOUT[area];
111
+ return knowledge.path ? `${knowledge.path}/${directory}` : directory;
112
+ }
113
+
114
+ /** `owner/name`, out of whatever a person pasted. Null when it is not one. */
115
+ export function parseRepository(given: string): string | null {
116
+ const trimmed = given
117
+ .trim()
118
+ .replace(/^https?:\/\/(?:www\.)?github\.com\//i, '')
119
+ .replace(/^(?:ssh:\/\/)?git@[^:]+[:/]/i, '')
120
+ .replace(/\.git$/i, '')
121
+ .replace(/\/+$/, '');
122
+
123
+ return repository.safeParse(trimmed).success ? trimmed : null;
124
+ }
@@ -114,11 +114,9 @@ function assertReferentialIntegrity(config: Config, source: string): void {
114
114
  if (targetNames.size === 0) {
115
115
  problems.push('targets: at least one target must be declared');
116
116
  }
117
- if (!targetNames.has(config.instance.default_target)) {
118
- problems.push(
119
- `instance.default_target: "${config.instance.default_target}" is not a declared target (have: ${[...targetNames].join(', ') || 'none'})`,
120
- );
121
- }
117
+ // `instance.default_target` is deliberately not checked. Nothing reads it
118
+ // (ADR-037), so validating it would be validating a comment — and failing
119
+ // `check` on a stale value would teach that the key still matters.
122
120
 
123
121
  // Only what holds for every platform. What one platform needs and the next
124
122
  // has no concept of — a GCP project, an AWS role ARN — is refused by the
@@ -146,6 +144,20 @@ function assertReferentialIntegrity(config: Config, source: string): void {
146
144
  connectionKeys.add(key);
147
145
  });
148
146
 
147
+ // Same reason as a duplicate connection: two entries with the same kind and
148
+ // value cannot both be meant, and the one that loses is invisible. It matters
149
+ // more here than it looks, because the two would usually differ only in their
150
+ // `note` — so the discarded one is precisely the guidance someone wrote down
151
+ // to stop an agent picking wrong.
152
+ const identityKeys = new Set<string>();
153
+ config.identity.forEach((entry, index) => {
154
+ const key = `${entry.kind}=${entry.value}`;
155
+ if (identityKeys.has(key)) {
156
+ problems.push(`identity[${index}]: duplicate entry "${entry.kind}: ${entry.value}"`);
157
+ }
158
+ identityKeys.add(key);
159
+ });
160
+
149
161
  const checkRules = (rules: readonly PolicyRuleConfig[], field: 'allow' | 'deny'): void => {
150
162
  rules.forEach((rule, index) => {
151
163
  const where = `policy.${field}[${index}]`;
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  /**
4
- * The three string shapes the config contract is built out of.
4
+ * The four string shapes the config contract is built out of.
5
5
  *
6
6
  * Their own file because more than one schema module needs them, and the
7
7
  * alternative was either a circular import or a second copy of a regex that
@@ -43,3 +43,26 @@ export const capabilityPattern = z
43
43
  /^(?:\*|[a-z][a-z0-9_]*\.(?:\*|[A-Za-z0-9][A-Za-z0-9_-]*(?:\.[A-Za-z0-9][A-Za-z0-9_-]*)*(?:\.\*)?))$/,
44
44
  'must be "*", "gmail.*", "gmail.users.*", or "gmail.users.drafts.send"',
45
45
  );
46
+
47
+ /**
48
+ * A browser origin, exactly — scheme, host, and port, with nothing after it. Or
49
+ * `*`, which is what an absent list means anyway and is accepted so that saying
50
+ * it explicitly is not an error.
51
+ *
52
+ * Checked by round trip rather than by a regex, because `URL` already knows what
53
+ * an origin is and the failure this catches is a trailing slash or a path that
54
+ * looks harmless and matches nothing: a browser sends `Origin: https://x.example`
55
+ * and a configured `https://x.example/` would compare unequal, silently refusing
56
+ * the origin its owner believed they had allowed.
57
+ */
58
+ export const browserOrigin = z.string().refine(
59
+ (value) => {
60
+ if (value === '*') return true;
61
+ try {
62
+ return new URL(value).origin === value;
63
+ } catch {
64
+ return false;
65
+ }
66
+ },
67
+ 'must be "*" or an origin with no trailing slash or path, e.g. "https://chat.example"',
68
+ );
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
- import { capabilityPattern, credentialRef, identifier } from './primitives.ts';
2
+ import { browserOrigin, capabilityPattern, credentialRef, identifier } from './primitives.ts';
3
3
  import { authorizationSchema } from './authorization.ts';
4
+ import { identitySchema } from './identity.ts';
5
+ import { knowledgeTargetSchema } from './knowledge.ts';
4
6
 
5
7
  /**
6
8
  * Configuration is declarative desired state. This file is the source of truth
@@ -214,6 +216,15 @@ export const targetSchema = z
214
216
  audit: auditTargetSchema.optional(),
215
217
  storage: storageTargetSchema,
216
218
  vault: vaultTargetSchema.optional(),
219
+ /**
220
+ * Memory and skills, somewhere other than `storage` above.
221
+ *
222
+ * Optional and absent by default, so every profile written before it keeps
223
+ * storing both exactly where it did. See `knowledge.ts` for why these two
224
+ * are separable from the rest and why the credential store and the vault
225
+ * are not.
226
+ */
227
+ knowledge: knowledgeTargetSchema.optional(),
217
228
  deploy: deployTargetSchema.optional(),
218
229
  /** @deprecated Write `deploy` with `platform: cloudrun`. */
219
230
  cloudrun: legacyCloudRunSchema.optional(),
@@ -313,7 +324,22 @@ export const configSchema = z.object({
313
324
 
314
325
  instance: z.object({
315
326
  profile: identifier,
316
- default_target: z.string().min(1),
327
+ /**
328
+ * @deprecated Parsed, never read. See ADR-037.
329
+ *
330
+ * Every command names its target on the command line now, so nothing
331
+ * consults this. It stays *declared* rather than being dropped, and that is
332
+ * the whole point: an undeclared key is stripped silently by the schema,
333
+ * which would leave `check` and `doctor` with nothing to report and an
334
+ * operator staring at a line they reasonably believe still selects
335
+ * something. Declaring it is what lets them be told it is inert.
336
+ *
337
+ * Optional, so a profile written today needs no such line, and unvalidated,
338
+ * so a stale value naming a target that no longer exists is harmless rather
339
+ * than a failure on a key nothing reads. The `database:` note above records
340
+ * the same decision for the same reason.
341
+ */
342
+ default_target: z.string().min(1).optional(),
317
343
  port: z.number().int().min(1).max(65535).default(7337),
318
344
  /**
319
345
  * Loopback by default. Binding elsewhere is possible but the server
@@ -334,6 +360,15 @@ export const configSchema = z.object({
334
360
  * get one. Omitting it leaves every existing profile behaving identically.
335
361
  */
336
362
  authorization: authorizationSchema.optional(),
363
+ /**
364
+ * Browser origins that may call a *deployment's* MCP endpoint. Absent
365
+ * means `*`, so naming any is a narrowing and nothing needs setting.
366
+ *
367
+ * A deployment only, and this cannot widen that: a loopback endpoint
368
+ * refuses every cross-origin request and must keep doing so. Why the
369
+ * default is a wildcard is `src/server/cors.ts` and ADR-039.
370
+ */
371
+ allowed_origins: z.array(browserOrigin).optional(),
337
372
  })
338
373
  .default({ mode: 'bearer', token_ref: 'profile/token' }),
339
374
 
@@ -353,6 +388,13 @@ export const configSchema = z.object({
353
388
  */
354
389
  connections: z.array(connectionSchema).default([]),
355
390
  policy: policySchema.default({ allow: [], deny: [] }),
391
+
392
+ /**
393
+ * Who the owner is, for anything written as them. Optional and additive, so
394
+ * every profile written before it keeps loading unchanged — the same reasoning
395
+ * as `auth.authorization` above, and the reason `contract` does not move.
396
+ */
397
+ identity: identitySchema.default([]),
356
398
  });
357
399
 
358
400
  export type Config = z.infer<typeof configSchema>;
@@ -361,12 +403,48 @@ export type PolicyRuleConfig = z.infer<typeof policyRuleSchema>;
361
403
  export type TargetConfig = z.infer<typeof targetSchema>;
362
404
  export type DeployConfig = z.infer<typeof deployTargetSchema>;
363
405
  export type AuthorizationConfig = z.infer<typeof authorizationSchema>;
364
- export { authorizationSchema };
406
+ export { authorizationSchema, identitySchema };
407
+ export type { IdentityEntry } from './identity.ts';
408
+
409
+ /**
410
+ * Where a deployment lives — an index, not configuration.
411
+ *
412
+ * **Nothing resolves from this.** A target is still declared by the profile,
413
+ * and a command still acts on what the profile says. This exists because the
414
+ * profile file was the *only* record that a deployment existed, so rewriting
415
+ * one erased the sole pointer to a live service, a bucket holding every byte
416
+ * the endpoint remembered, and a credential store — none of which had gone
417
+ * anywhere. There was no command that could find them again.
418
+ *
419
+ * It lives in `lanes-link.yaml`, outside any profile, for exactly that reason:
420
+ * a record kept inside the thing it describes cannot survive the thing being
421
+ * lost. `lanes link sync targets` reads it, and reads it *only* to know where
422
+ * to look (ADR-044).
423
+ *
424
+ * Written by `deploy`, after an upload has succeeded — so it never names a
425
+ * workspace nothing was ever put in.
426
+ */
427
+ export const deploymentRecordSchema = z.object({
428
+ target: identifier,
429
+ /** Where the deployed endpoint reads its config: `gs://bucket[/prefix]`. */
430
+ workspace: z.string().min(1),
431
+ /**
432
+ * Whose bearer token opens this endpoint (ADR-009).
433
+ *
434
+ * Recorded rather than inferred. One endpoint serves every profile in the
435
+ * bucket under one token, and which profile's token that is decides who gets
436
+ * in — the one question about a deployment that must not be guessed at.
437
+ */
438
+ primary: identifier.optional(),
439
+ last_deploy: z.string().optional(),
440
+ });
365
441
 
366
442
  /** The workspace file: `lanes-link.yaml`, alongside a `profiles/` directory. */
367
443
  export const workspaceSchema = z.object({
368
444
  contract: z.number().int().positive(),
369
445
  default_profile: identifier.optional(),
446
+ deployments: z.array(deploymentRecordSchema).default([]),
370
447
  });
371
448
 
372
449
  export type WorkspaceConfig = z.infer<typeof workspaceSchema>;
450
+ export type DeploymentRecord = z.infer<typeof deploymentRecordSchema>;