@lanes-sh/link 0.1.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 (276) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +144 -0
  3. package/bin/lanes +42 -0
  4. package/instructions/agents/lanes-link-scout.md +73 -0
  5. package/instructions/skills/lanes-link/SKILL.md +187 -0
  6. package/package.json +95 -0
  7. package/src/audit/chain.ts +278 -0
  8. package/src/audit/conformance.ts +374 -0
  9. package/src/audit/fanout.ts +97 -0
  10. package/src/audit/index.ts +218 -0
  11. package/src/audit/stdout.ts +60 -0
  12. package/src/auth/index.ts +220 -0
  13. package/src/auth/oauth/metadata.ts +75 -0
  14. package/src/auth/oauth/server.ts +385 -0
  15. package/src/auth/oauth/store.ts +215 -0
  16. package/src/auth/oidc.ts +206 -0
  17. package/src/auth/remote.ts +72 -0
  18. package/src/cli/argv.ts +94 -0
  19. package/src/cli/callback-page.ts +256 -0
  20. package/src/cli/commands/connect/accounts.ts +94 -0
  21. package/src/cli/commands/connect/authorise.ts +298 -0
  22. package/src/cli/commands/connect/client.ts +284 -0
  23. package/src/cli/commands/connect/index.ts +398 -0
  24. package/src/cli/commands/connect/outcome.ts +119 -0
  25. package/src/cli/commands/connect/requirements.ts +103 -0
  26. package/src/cli/commands/connect/scopes-gate.ts +146 -0
  27. package/src/cli/commands/connect/settle.ts +136 -0
  28. package/src/cli/commands/connect/setup.ts +276 -0
  29. package/src/cli/commands/mcp/assets.ts +189 -0
  30. package/src/cli/commands/mcp/harnesses.ts +143 -0
  31. package/src/cli/commands/mcp/list.ts +70 -0
  32. package/src/cli/commands/mcp/register.ts +199 -0
  33. package/src/cli/commands/mcp/stdio.ts +57 -0
  34. package/src/cli/commands/mcp.ts +22 -0
  35. package/src/cli/commands/operate/attach.ts +121 -0
  36. package/src/cli/commands/operate/audit.ts +119 -0
  37. package/src/cli/commands/operate/inspect.ts +355 -0
  38. package/src/cli/commands/operate/outputs.ts +206 -0
  39. package/src/cli/commands/operate/policy.ts +80 -0
  40. package/src/cli/commands/operate/serve.ts +55 -0
  41. package/src/cli/commands/operate/status.ts +133 -0
  42. package/src/cli/commands/operate/token.ts +70 -0
  43. package/src/cli/commands/operate.ts +27 -0
  44. package/src/cli/commands/owner/memory.ts +110 -0
  45. package/src/cli/commands/owner/shared.ts +125 -0
  46. package/src/cli/commands/owner/skills.ts +92 -0
  47. package/src/cli/commands/owner/vault.ts +140 -0
  48. package/src/cli/commands/owner.ts +46 -0
  49. package/src/cli/commands/profile/removal.ts +278 -0
  50. package/src/cli/commands/profile/remove.ts +315 -0
  51. package/src/cli/commands/profile.ts +156 -0
  52. package/src/cli/commands/secrets.ts +176 -0
  53. package/src/cli/commands/setup.ts +150 -0
  54. package/src/cli/commands/target.ts +310 -0
  55. package/src/cli/config-edit.ts +397 -0
  56. package/src/cli/endpoint-url.ts +77 -0
  57. package/src/cli/identity.ts +109 -0
  58. package/src/cli/lanes.ts +78 -0
  59. package/src/cli/main.ts +333 -0
  60. package/src/cli/oauth-error.ts +13 -0
  61. package/src/cli/oauth-exchange.ts +146 -0
  62. package/src/cli/oauth.ts +354 -0
  63. package/src/cli/output.ts +184 -0
  64. package/src/cli/prompt.ts +180 -0
  65. package/src/cli/publish.ts +185 -0
  66. package/src/cli/runtime/discovery.ts +123 -0
  67. package/src/cli/runtime/open.ts +342 -0
  68. package/src/cli/runtime/registry.ts +185 -0
  69. package/src/cli/runtime/select.ts +124 -0
  70. package/src/cli/runtime.ts +34 -0
  71. package/src/cli/scopes.ts +63 -0
  72. package/src/cli/usage.ts +111 -0
  73. package/src/cli/version.ts +25 -0
  74. package/src/connectivity/auth/README.md +36 -0
  75. package/src/connectivity/auth/api-key/index.ts +43 -0
  76. package/src/connectivity/auth/authorize.ts +49 -0
  77. package/src/connectivity/auth/basic/index.ts +68 -0
  78. package/src/connectivity/auth/bearer/index.ts +13 -0
  79. package/src/connectivity/auth/credential.ts +19 -0
  80. package/src/connectivity/auth/header/index.ts +18 -0
  81. package/src/connectivity/auth/index.ts +35 -0
  82. package/src/connectivity/auth/none/index.ts +12 -0
  83. package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
  84. package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
  85. package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
  86. package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
  87. package/src/connectivity/auth/resolve.ts +61 -0
  88. package/src/connectivity/auth/strategy/index.ts +14 -0
  89. package/src/connectivity/capability.ts +164 -0
  90. package/src/connectivity/connector.ts +175 -0
  91. package/src/connectivity/context.ts +77 -0
  92. package/src/connectivity/index.ts +94 -0
  93. package/src/connectivity/mail/attachments.ts +368 -0
  94. package/src/connectivity/mail/compose.ts +73 -0
  95. package/src/connectivity/mail/index.ts +40 -0
  96. package/src/connectivity/mail/message.ts +82 -0
  97. package/src/connectivity/mail/nodemailer.d.ts +83 -0
  98. package/src/connectivity/mail/staging.ts +143 -0
  99. package/src/connectivity/mail/url.ts +301 -0
  100. package/src/connectivity/manifest/auth.ts +135 -0
  101. package/src/connectivity/manifest/bundles.ts +23 -0
  102. package/src/connectivity/manifest/connector.ts +168 -0
  103. package/src/connectivity/manifest/credential-ref.ts +73 -0
  104. package/src/connectivity/manifest/identity.ts +43 -0
  105. package/src/connectivity/manifest/index.ts +46 -0
  106. package/src/connectivity/manifest/primitives.ts +20 -0
  107. package/src/connectivity/manifest/provider.ts +208 -0
  108. package/src/connectivity/manifest/requirements.ts +146 -0
  109. package/src/connectivity/manifest/setup.ts +55 -0
  110. package/src/connectivity/provider.ts +163 -0
  111. package/src/connectivity/transports/README.md +33 -0
  112. package/src/connectivity/transports/composite/index.ts +68 -0
  113. package/src/connectivity/transports/dav/calendar.ts +217 -0
  114. package/src/connectivity/transports/dav/capabilities.ts +151 -0
  115. package/src/connectivity/transports/dav/client.ts +226 -0
  116. package/src/connectivity/transports/dav/contacts.ts +75 -0
  117. package/src/connectivity/transports/dav/ical.ts +412 -0
  118. package/src/connectivity/transports/dav/index.ts +143 -0
  119. package/src/connectivity/transports/dav/operations.ts +43 -0
  120. package/src/connectivity/transports/dav/request.ts +161 -0
  121. package/src/connectivity/transports/dav/xml.ts +123 -0
  122. package/src/connectivity/transports/factory.ts +181 -0
  123. package/src/connectivity/transports/fs/capabilities.ts +88 -0
  124. package/src/connectivity/transports/fs/commands.ts +258 -0
  125. package/src/connectivity/transports/fs/index.ts +121 -0
  126. package/src/connectivity/transports/fs/operations.ts +45 -0
  127. package/src/connectivity/transports/fs/paths.ts +120 -0
  128. package/src/connectivity/transports/fs/result.ts +12 -0
  129. package/src/connectivity/transports/http/index.ts +255 -0
  130. package/src/connectivity/transports/imap/attachment.ts +166 -0
  131. package/src/connectivity/transports/imap/capabilities.ts +158 -0
  132. package/src/connectivity/transports/imap/client.ts +398 -0
  133. package/src/connectivity/transports/imap/commands.ts +385 -0
  134. package/src/connectivity/transports/imap/index.ts +152 -0
  135. package/src/connectivity/transports/imap/operations.ts +64 -0
  136. package/src/connectivity/transports/imap/parse.ts +130 -0
  137. package/src/connectivity/transports/imap/parser.ts +272 -0
  138. package/src/connectivity/transports/imap/result.ts +15 -0
  139. package/src/connectivity/transports/imap/send.ts +92 -0
  140. package/src/connectivity/transports/imap/socket.ts +111 -0
  141. package/src/connectivity/transports/imap/utf7.ts +136 -0
  142. package/src/connectivity/transports/index.ts +20 -0
  143. package/src/connectivity/transports/local/index.ts +173 -0
  144. package/src/connectivity/transports/mcp/index.ts +215 -0
  145. package/src/deployments/README.md +63 -0
  146. package/src/deployments/adapters/audit-blob.ts +203 -0
  147. package/src/deployments/adapters/filesystem.ts +184 -0
  148. package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
  149. package/src/deployments/adapters/gcs.ts +191 -0
  150. package/src/deployments/adapters/otlp.ts +128 -0
  151. package/src/deployments/adapters/s3.ts +195 -0
  152. package/src/deployments/azure/README.md +21 -0
  153. package/src/deployments/bootstrap.ts +177 -0
  154. package/src/deployments/deploy.ts +290 -0
  155. package/src/deployments/driver.ts +157 -0
  156. package/src/deployments/drivers.ts +35 -0
  157. package/src/deployments/gcp/Dockerfile +70 -0
  158. package/src/deployments/gcp/cloudbuild.yaml +31 -0
  159. package/src/deployments/gcp/driver.ts +175 -0
  160. package/src/deployments/gcp/gcloud.ts +178 -0
  161. package/src/deployments/gcp/provision.ts +290 -0
  162. package/src/deployments/gcp/survey.ts +319 -0
  163. package/src/deployments/local/README.md +12 -0
  164. package/src/deployments/prepare.ts +257 -0
  165. package/src/deployments/steps.ts +137 -0
  166. package/src/deployments/target.ts +295 -0
  167. package/src/deployments/upload.ts +207 -0
  168. package/src/dispatch/context.ts +195 -0
  169. package/src/dispatch/dispatch.ts +350 -0
  170. package/src/dispatch/index.ts +32 -0
  171. package/src/dispatch/staging.ts +102 -0
  172. package/src/policy/index.ts +179 -0
  173. package/src/policy/limits.ts +77 -0
  174. package/src/profile/authorization.ts +81 -0
  175. package/src/profile/files.ts +71 -0
  176. package/src/profile/index.ts +76 -0
  177. package/src/profile/layout.ts +123 -0
  178. package/src/profile/load.ts +199 -0
  179. package/src/profile/primitives.ts +45 -0
  180. package/src/profile/schema.ts +347 -0
  181. package/src/profile/secret-detection.ts +162 -0
  182. package/src/profile/targets.ts +152 -0
  183. package/src/profile/workspace.ts +262 -0
  184. package/src/providers/custom/index.ts +21 -0
  185. package/src/providers/custom/load.ts +115 -0
  186. package/src/providers/custom/template.ts +156 -0
  187. package/src/providers/example/provider.ts +207 -0
  188. package/src/providers/google/calendar/index.ts +66 -0
  189. package/src/providers/google/calendar/redact.ts +40 -0
  190. package/src/providers/google/contacts/index.ts +50 -0
  191. package/src/providers/google/contacts/redact.ts +21 -0
  192. package/src/providers/google/docs/index.ts +45 -0
  193. package/src/providers/google/drive/hints.ts +28 -0
  194. package/src/providers/google/drive/index.ts +34 -0
  195. package/src/providers/google/drive/redact.ts +39 -0
  196. package/src/providers/google/drive-mcp/index.ts +21 -0
  197. package/src/providers/google/gmail/api.ts +42 -0
  198. package/src/providers/google/gmail/attachment.ts +142 -0
  199. package/src/providers/google/gmail/hints.ts +55 -0
  200. package/src/providers/google/gmail/index.ts +112 -0
  201. package/src/providers/google/gmail/redact.ts +56 -0
  202. package/src/providers/google/gmail/send.ts +365 -0
  203. package/src/providers/google/gmail-mcp/index.ts +35 -0
  204. package/src/providers/google/index.ts +10 -0
  205. package/src/providers/google/shared/oauth.ts +122 -0
  206. package/src/providers/google/shared/scopes.ts +99 -0
  207. package/src/providers/google/shared/setup.ts +80 -0
  208. package/src/providers/google/sheets/hints.ts +45 -0
  209. package/src/providers/google/sheets/index.ts +70 -0
  210. package/src/providers/google/sheets/redact.ts +45 -0
  211. package/src/providers/google/specs/calendar.v3.json +1829 -0
  212. package/src/providers/google/specs/docs.v1.json +381 -0
  213. package/src/providers/google/specs/drive.v3.json +2208 -0
  214. package/src/providers/google/specs/gmail.v1.json +2578 -0
  215. package/src/providers/google/specs/people.v1.json +506 -0
  216. package/src/providers/google/specs/sheets.v4.json +1269 -0
  217. package/src/providers/google/specs/tasks.v1.json +840 -0
  218. package/src/providers/google/specs/vendor.ts +661 -0
  219. package/src/providers/google/tasks/index.ts +53 -0
  220. package/src/providers/google/tasks/redact.ts +34 -0
  221. package/src/providers/harness.ts +95 -0
  222. package/src/providers/icloud/calendar/index.ts +27 -0
  223. package/src/providers/icloud/contacts/index.ts +17 -0
  224. package/src/providers/icloud/drive/index.ts +47 -0
  225. package/src/providers/icloud/index.ts +8 -0
  226. package/src/providers/icloud/mail/index.ts +37 -0
  227. package/src/providers/icloud/shared/setup.ts +66 -0
  228. package/src/providers/index.ts +93 -0
  229. package/src/providers/linear/index.ts +11 -0
  230. package/src/providers/linear/scopes.ts +7 -0
  231. package/src/providers/memory/provider.ts +429 -0
  232. package/src/providers/notion/index.ts +19 -0
  233. package/src/providers/owner.ts +49 -0
  234. package/src/providers/scopes.ts +26 -0
  235. package/src/providers/setup/plan.ts +141 -0
  236. package/src/providers/setup/provider.ts +323 -0
  237. package/src/providers/shared/frontmatter.ts +119 -0
  238. package/src/providers/skills/provider.ts +283 -0
  239. package/src/providers/skills/store.ts +252 -0
  240. package/src/providers/vault/provider.ts +194 -0
  241. package/src/registry/index.ts +36 -0
  242. package/src/registry/policy-bridge.ts +32 -0
  243. package/src/registry/reconcile.ts +313 -0
  244. package/src/registry/registry.ts +240 -0
  245. package/src/secrets/document.ts +293 -0
  246. package/src/secrets/index.ts +154 -0
  247. package/src/secrets/system.ts +173 -0
  248. package/src/secrets/vault.ts +336 -0
  249. package/src/server/attachments.ts +197 -0
  250. package/src/server/container.ts +96 -0
  251. package/src/server/edge.ts +53 -0
  252. package/src/server/endpoint.ts +352 -0
  253. package/src/server/generations.ts +362 -0
  254. package/src/server/harness.ts +400 -0
  255. package/src/server/index.ts +331 -0
  256. package/src/server/logging.ts +41 -0
  257. package/src/server/mcp/build.ts +68 -0
  258. package/src/server/mcp/icon.ts +145 -0
  259. package/src/server/mcp/index.ts +32 -0
  260. package/src/server/mcp/instructions.ts +245 -0
  261. package/src/server/mcp/naming.ts +39 -0
  262. package/src/server/mcp/prompts.ts +78 -0
  263. package/src/server/mcp/resources.ts +106 -0
  264. package/src/server/mcp/routing.ts +117 -0
  265. package/src/server/mcp/schema.ts +78 -0
  266. package/src/server/mcp/tools.ts +186 -0
  267. package/src/server/mcp/visibility.ts +132 -0
  268. package/src/server/oauth.ts +222 -0
  269. package/src/server/rebinding.ts +53 -0
  270. package/src/server/stdio.ts +192 -0
  271. package/src/stores/blobs/conformance.ts +223 -0
  272. package/src/stores/blobs/index.ts +125 -0
  273. package/src/stores/blobs/testing.ts +49 -0
  274. package/src/stores/state/index.ts +247 -0
  275. package/src/stores/state/keys.ts +68 -0
  276. package/src/stores/state/testing.ts +41 -0
@@ -0,0 +1,60 @@
1
+ import { newRunId, type AuditDraft, type AuditEvent, type AuditSink } from './index.ts';
2
+
3
+ /**
4
+ * The log as structured lines on stdout.
5
+ *
6
+ * On Cloud Run this is the cheapest durable copy available: the platform ships
7
+ * stdout to Cloud Logging with no configuration, no bucket write per call, and
8
+ * no dependency to declare. `container.ts` already writes its startup lines
9
+ * there for the same reason.
10
+ *
11
+ * Opt-in, and never the primary. The platform's shipper buffers, so an event
12
+ * written here has been *handed over* rather than stored — which is exactly the
13
+ * property the primary sink must not have. Configure it alongside the blob
14
+ * sink, or make it primary deliberately if you would rather trade the guarantee
15
+ * for the bucket write on every call.
16
+ *
17
+ * One JSON object per line, `kind: "audit"`, so a log filter can separate these
18
+ * from the plain startup lines beside them without matching on message text.
19
+ */
20
+
21
+ export interface StdoutAuditOptions {
22
+ readonly write?: (line: string) => void;
23
+ readonly now?: () => Date;
24
+ readonly run?: string;
25
+ }
26
+
27
+ export function createStdoutAuditSink(options: StdoutAuditOptions = {}): AuditSink {
28
+ const write = options.write ?? ((line: string) => process.stdout.write(line));
29
+ const now = options.now ?? ((): Date => new Date());
30
+ const run = options.run ?? newRunId();
31
+ let seq = 0;
32
+
33
+ return {
34
+ async append(draft: AuditDraft): Promise<AuditEvent> {
35
+ const event: AuditEvent = { ...draft, id: `evt_${crypto.randomUUID()}`, timestamp: now() };
36
+
37
+ // `run` and `seq` but no `prev`: a hash chain over interleaved instances
38
+ // means nothing, and a chain nobody can re-read cannot be verified
39
+ // anyway. The pair still makes a gap visible, which is the part of the
40
+ // guarantee that survives a log this does not own.
41
+ write(
42
+ `${JSON.stringify({
43
+ kind: 'audit',
44
+ run,
45
+ seq: seq++,
46
+ ...event,
47
+ timestamp: event.timestamp.toISOString(),
48
+ })}\n`,
49
+ );
50
+
51
+ return event;
52
+ },
53
+
54
+ async close(): Promise<void> {
55
+ // Nothing to close. There is no marker either: a marker exists so
56
+ // `verify` can tell a finished run from a truncated one, and nothing
57
+ // here can be verified.
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Client identity.
3
+ *
4
+ * M1: one bearer token per profile, resolved from the credential store. The
5
+ * token is the identity — holding it makes you the profile's owner principal.
6
+ *
7
+ * Target: the OAuth 2.1 resource-server model the MCP spec expects, where this
8
+ * module validates tokens issued by an external authorization server. Client
9
+ * identity, authentication, and authorization are kept behind separate
10
+ * interfaces precisely so that drops in without touching a single provider.
11
+ * Do not invent cryptography here.
12
+ *
13
+ * KNOWN LIMITATION, stated rather than papered over: bearer tokens are bearer
14
+ * authorization. Anyone holding the token is the principal, tokens are not
15
+ * bound to a device, and agent config files typically sit in plaintext on
16
+ * disk — so a token is roughly as protected as that file. Revocation means
17
+ * rotating the token and reconciling.
18
+ */
19
+
20
+ import { timingSafeEqual } from 'node:crypto';
21
+ import type { SecretRef, SecretStore } from '#secrets';
22
+
23
+ /**
24
+ * Who is acting. M1 resolves exactly one per profile, but the dispatch path
25
+ * takes a principal rather than assuming the owner, so delegated access is
26
+ * additive later instead of a rewrite.
27
+ */
28
+ export interface Principal {
29
+ readonly id: string;
30
+ readonly profile: string;
31
+ readonly kind: 'owner';
32
+ }
33
+
34
+ export function ownerPrincipal(profile: string): Principal {
35
+ return { id: `${profile}:owner`, profile, kind: 'owner' };
36
+ }
37
+
38
+ export type AuthOutcome =
39
+ | { readonly ok: true; readonly principal: Principal }
40
+ | { readonly ok: false; readonly reason: 'missing' | 'malformed' | 'invalid' | 'not_configured' };
41
+
42
+ /**
43
+ * Anything that can turn an `Authorization` header into a principal.
44
+ *
45
+ * Extracted so the endpoint can accept more than one kind of proof without the
46
+ * request path learning what kinds exist. There are two: a static token the
47
+ * operator holds, and a token this endpoint or an issuer handed to a client
48
+ * that completed an authorization flow. Both arrive in the same header, and the
49
+ * server does not care which answered.
50
+ */
51
+ export interface Authenticator {
52
+ authenticate(authorizationHeader: string | null | undefined): Promise<AuthOutcome>;
53
+ invalidateCache?(): void;
54
+ }
55
+
56
+ /**
57
+ * Try each in order; the first to recognise the credential wins.
58
+ *
59
+ * Order is not arbitrary — the static token first, because it is a local
60
+ * constant-time comparison against a cached value and covers the CLI, `outputs`,
61
+ * and every local registration. Putting a network round trip in front of that
62
+ * would make the common case the slow one.
63
+ *
64
+ * The reported reason is the most specific failure any link produced. A chain
65
+ * that reported `missing` because the last link saw no credential of *its* kind
66
+ * would describe a rejected token as an absent one, which sends whoever is
67
+ * debugging it to entirely the wrong place.
68
+ */
69
+ export class AuthenticatorChain implements Authenticator {
70
+ readonly #links: readonly Authenticator[];
71
+
72
+ constructor(links: readonly Authenticator[]) {
73
+ this.#links = links;
74
+ }
75
+
76
+ async authenticate(authorizationHeader: string | null | undefined): Promise<AuthOutcome> {
77
+ const rank = { invalid: 3, not_configured: 2, malformed: 1, missing: 0 } as const;
78
+ let worst: Extract<AuthOutcome, { ok: false }> = { ok: false, reason: 'missing' };
79
+
80
+ for (const link of this.#links) {
81
+ const outcome = await link.authenticate(authorizationHeader);
82
+ if (outcome.ok) return outcome;
83
+ if (rank[outcome.reason] > rank[worst.reason]) worst = outcome;
84
+ }
85
+
86
+ return worst;
87
+ }
88
+
89
+ invalidateCache(): void {
90
+ for (const link of this.#links) link.invalidateCache?.();
91
+ }
92
+ }
93
+
94
+ const BEARER_PATTERN = /^Bearer[ ]+(.+)$/i;
95
+
96
+ /** Extract a bearer token from an Authorization header value. */
97
+ export function parseBearer(header: string | null | undefined): string | null {
98
+ if (!header) return null;
99
+ const match = header.trim().match(BEARER_PATTERN);
100
+ return match?.[1]?.trim() || null;
101
+ }
102
+
103
+ /**
104
+ * Compare in constant time.
105
+ *
106
+ * `timingSafeEqual` throws on length mismatch, which would itself leak length,
107
+ * so both sides are hashed to a fixed width first. Comparing raw tokens with
108
+ * `===` would leak the shared prefix a byte at a time.
109
+ */
110
+ export function tokensMatch(a: string, b: string): boolean {
111
+ const hash = (value: string): Buffer =>
112
+ Buffer.from(new Bun.CryptoHasher('sha256').update(value, 'utf8').digest());
113
+ return timingSafeEqual(hash(a), hash(b));
114
+ }
115
+
116
+ export interface AuthenticatorOptions {
117
+ readonly profile: string;
118
+ readonly tokenRef: SecretRef;
119
+ readonly credentials: SecretStore;
120
+ /** Injectable for tests. Only the cache window reads it. */
121
+ readonly now?: () => number;
122
+ }
123
+
124
+ /**
125
+ * How long a cached token may answer before the store is consulted again.
126
+ *
127
+ * The cache is here so the common case — a valid token, on every request — is a
128
+ * comparison rather than a file read or a Secret Manager call. What it must not
129
+ * do is outlive a rotation. `lanes link token rotate` is the only revocation
130
+ * this system has, and an unbounded cache meant a revoked token kept opening the
131
+ * endpoint until the process restarted, while the replacement was refused.
132
+ *
133
+ * Five seconds makes rotation effectively immediate and still collapses a burst
134
+ * of calls onto one read.
135
+ */
136
+ const CACHE_TTL_MS = 5_000;
137
+
138
+ export class BearerAuthenticator implements Authenticator {
139
+ readonly #options: AuthenticatorOptions;
140
+ readonly #now: () => number;
141
+ #cached: string | null = null;
142
+ #readAt = 0;
143
+
144
+ constructor(options: AuthenticatorOptions) {
145
+ this.#options = options;
146
+ this.#now = options.now ?? Date.now;
147
+ }
148
+
149
+ async authenticate(authorizationHeader: string | null | undefined): Promise<AuthOutcome> {
150
+ const { profile } = this.#options;
151
+
152
+ const presented = parseBearer(authorizationHeader);
153
+ if (presented === null) {
154
+ return { ok: false, reason: authorizationHeader ? 'malformed' : 'missing' };
155
+ }
156
+
157
+ const fresh = this.#cached !== null && this.#now() - this.#readAt < CACHE_TTL_MS;
158
+ let expected = fresh ? this.#cached : await this.#reload();
159
+
160
+ // A mismatch against a *cached* value is ambiguous: either the credential is
161
+ // wrong, or it is the right one and this process has not seen the rotation
162
+ // that produced it. One re-read separates the two, and it is what makes a
163
+ // rotated-in token work on its first call rather than after the window.
164
+ // Only a cached comparison can be wrong this way, so a fresh read never
165
+ // pays for a second one — which is what keeps a wrong token from costing a
166
+ // store read per attempt.
167
+ if (fresh && (expected === null || !tokensMatch(presented, expected))) {
168
+ expected = await this.#reload();
169
+ }
170
+
171
+ if (expected === null) {
172
+ // The profile has no token yet. Fail closed and let `lanes link doctor` explain.
173
+ return { ok: false, reason: 'not_configured' };
174
+ }
175
+
176
+ return tokensMatch(presented, expected)
177
+ ? { ok: true, principal: ownerPrincipal(profile) }
178
+ : { ok: false, reason: 'invalid' };
179
+ }
180
+
181
+ async #reload(): Promise<string | null> {
182
+ // Both caches, or neither: the store holds its own decrypted copy, so
183
+ // re-reading without dropping that first re-reads the same stale value.
184
+ this.#options.credentials.refresh?.();
185
+ this.#cached = await this.#options.credentials.get(this.#options.tokenRef);
186
+ this.#readAt = this.#now();
187
+ return this.#cached;
188
+ }
189
+
190
+ /**
191
+ * Drop the cached value immediately.
192
+ *
193
+ * The window above already bounds how long a rotation goes unnoticed, so this
194
+ * is an optimisation rather than the mechanism — nothing's correctness may
195
+ * depend on it being called, because for a long time nothing called it.
196
+ */
197
+ invalidateCache(): void {
198
+ this.#cached = null;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Mint a profile token: 32 random bytes, base64url, prefixed so it is
204
+ * recognisable in a config file and greppable in a leak.
205
+ */
206
+ export function generateProfileToken(): string {
207
+ return `llk_${Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url')}`;
208
+ }
209
+
210
+ export {
211
+ authorizationServerMetadata,
212
+ challenge,
213
+ protectedResourceMetadata,
214
+ MCP_SCOPE,
215
+ type ResourceIdentity,
216
+ } from './oauth/metadata.ts';
217
+ export { OAuthServer, matchesRegistered, pkceChallengeFor, type AuthorizeRequest, type OAuthResult } from './oauth/server.ts';
218
+ export { OAuthStore, hashToken, randomToken } from './oauth/store.ts';
219
+ export { OidcVerifier, type OidcVerifierOptions, type VerifiedSubject } from './oidc.ts';
220
+ export { IssuedTokenAuthenticator, OidcAuthenticator } from './remote.ts';
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The two documents a client reads before it tries to authenticate.
3
+ *
4
+ * Protected-resource metadata (RFC 9728) says *who* protects this resource;
5
+ * authorization-server metadata (RFC 8414) says how to talk to that protector.
6
+ * A client finds the first from the `WWW-Authenticate` header on a `401` and the
7
+ * second from the `authorization_servers` entry in the first.
8
+ *
9
+ * Both are built from the request rather than from config, because the value
10
+ * that has to be right is the one the user typed into their client — and this
11
+ * endpoint does not know its own public URL. Cloud Run assigns a hostname with a
12
+ * project hash in it, terminates TLS, and forwards the original host in a
13
+ * header. A document built from config would name whatever the config guessed;
14
+ * a document built from `request.url` alone would say `http`.
15
+ */
16
+
17
+ export interface ResourceIdentity {
18
+ /** The MCP endpoint URL as the client addresses it, e.g. `https://x.run.app/mcp`. */
19
+ readonly resource: string;
20
+ /** Where the authorization server lives — this origin, or an external issuer. */
21
+ readonly issuer: string;
22
+ }
23
+
24
+ /**
25
+ * One scope, and it is not a permission axis.
26
+ *
27
+ * What a caller may do is decided by the profile's policy, per capability, per
28
+ * call, and recorded in the audit log. A second permission system expressed as
29
+ * scopes could only either duplicate that or disagree with it, and a client
30
+ * cannot be trusted to ask for less than it wants anyway. The scope exists
31
+ * because the protocol has a slot for one.
32
+ */
33
+ export const MCP_SCOPE = 'mcp';
34
+
35
+ export function protectedResourceMetadata(identity: ResourceIdentity): Record<string, unknown> {
36
+ return {
37
+ resource: identity.resource,
38
+ authorization_servers: [identity.issuer],
39
+ scopes_supported: [MCP_SCOPE],
40
+ bearer_methods_supported: ['header'],
41
+ };
42
+ }
43
+
44
+ export function authorizationServerMetadata(origin: string): Record<string, unknown> {
45
+ return {
46
+ issuer: origin,
47
+ authorization_endpoint: `${origin}/authorize`,
48
+ token_endpoint: `${origin}/token`,
49
+ registration_endpoint: `${origin}/register`,
50
+ scopes_supported: [MCP_SCOPE],
51
+ response_types_supported: ['code'],
52
+ grant_types_supported: ['authorization_code', 'refresh_token'],
53
+ // Advertised because a spec-compliant client checks for it before starting
54
+ // a flow, and refuses rather than silently downgrading to `plain`.
55
+ code_challenge_methods_supported: ['S256'],
56
+ // Clients registered here are public: they run on a phone or a laptop and
57
+ // cannot keep a secret. Saying so is what lets them authenticate as `none`
58
+ // at the token endpoint instead of inventing a credential to send.
59
+ token_endpoint_auth_methods_supported: ['none'],
60
+ };
61
+ }
62
+
63
+ /**
64
+ * The `WWW-Authenticate` value on a 401.
65
+ *
66
+ * The `resource_metadata` pointer is the whole handshake: without it a client
67
+ * has to guess the document's location by probing well-known paths, which costs
68
+ * round trips and fails entirely on a host that does not serve them. Clients do
69
+ * not honour this header on a `200`, so the status has to be right too.
70
+ */
71
+ export function challenge(metadataUrl: string | null): string {
72
+ return metadataUrl
73
+ ? `Bearer realm="lanes-link", resource_metadata="${metadataUrl}"`
74
+ : 'Bearer realm="lanes-link"';
75
+ }