@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.
- package/LICENSE +202 -0
- package/README.md +144 -0
- package/bin/lanes +42 -0
- package/instructions/agents/lanes-link-scout.md +73 -0
- package/instructions/skills/lanes-link/SKILL.md +187 -0
- package/package.json +95 -0
- package/src/audit/chain.ts +278 -0
- package/src/audit/conformance.ts +374 -0
- package/src/audit/fanout.ts +97 -0
- package/src/audit/index.ts +218 -0
- package/src/audit/stdout.ts +60 -0
- package/src/auth/index.ts +220 -0
- package/src/auth/oauth/metadata.ts +75 -0
- package/src/auth/oauth/server.ts +385 -0
- package/src/auth/oauth/store.ts +215 -0
- package/src/auth/oidc.ts +206 -0
- package/src/auth/remote.ts +72 -0
- package/src/cli/argv.ts +94 -0
- package/src/cli/callback-page.ts +256 -0
- package/src/cli/commands/connect/accounts.ts +94 -0
- package/src/cli/commands/connect/authorise.ts +298 -0
- package/src/cli/commands/connect/client.ts +284 -0
- package/src/cli/commands/connect/index.ts +398 -0
- package/src/cli/commands/connect/outcome.ts +119 -0
- package/src/cli/commands/connect/requirements.ts +103 -0
- package/src/cli/commands/connect/scopes-gate.ts +146 -0
- package/src/cli/commands/connect/settle.ts +136 -0
- package/src/cli/commands/connect/setup.ts +276 -0
- package/src/cli/commands/mcp/assets.ts +189 -0
- package/src/cli/commands/mcp/harnesses.ts +143 -0
- package/src/cli/commands/mcp/list.ts +70 -0
- package/src/cli/commands/mcp/register.ts +199 -0
- package/src/cli/commands/mcp/stdio.ts +57 -0
- package/src/cli/commands/mcp.ts +22 -0
- package/src/cli/commands/operate/attach.ts +121 -0
- package/src/cli/commands/operate/audit.ts +119 -0
- package/src/cli/commands/operate/inspect.ts +355 -0
- package/src/cli/commands/operate/outputs.ts +206 -0
- package/src/cli/commands/operate/policy.ts +80 -0
- package/src/cli/commands/operate/serve.ts +55 -0
- package/src/cli/commands/operate/status.ts +133 -0
- package/src/cli/commands/operate/token.ts +70 -0
- package/src/cli/commands/operate.ts +27 -0
- package/src/cli/commands/owner/memory.ts +110 -0
- package/src/cli/commands/owner/shared.ts +125 -0
- package/src/cli/commands/owner/skills.ts +92 -0
- package/src/cli/commands/owner/vault.ts +140 -0
- package/src/cli/commands/owner.ts +46 -0
- package/src/cli/commands/profile/removal.ts +278 -0
- package/src/cli/commands/profile/remove.ts +315 -0
- package/src/cli/commands/profile.ts +156 -0
- package/src/cli/commands/secrets.ts +176 -0
- package/src/cli/commands/setup.ts +150 -0
- package/src/cli/commands/target.ts +310 -0
- package/src/cli/config-edit.ts +397 -0
- package/src/cli/endpoint-url.ts +77 -0
- package/src/cli/identity.ts +109 -0
- package/src/cli/lanes.ts +78 -0
- package/src/cli/main.ts +333 -0
- package/src/cli/oauth-error.ts +13 -0
- package/src/cli/oauth-exchange.ts +146 -0
- package/src/cli/oauth.ts +354 -0
- package/src/cli/output.ts +184 -0
- package/src/cli/prompt.ts +180 -0
- package/src/cli/publish.ts +185 -0
- package/src/cli/runtime/discovery.ts +123 -0
- package/src/cli/runtime/open.ts +342 -0
- package/src/cli/runtime/registry.ts +185 -0
- package/src/cli/runtime/select.ts +124 -0
- package/src/cli/runtime.ts +34 -0
- package/src/cli/scopes.ts +63 -0
- package/src/cli/usage.ts +111 -0
- package/src/cli/version.ts +25 -0
- package/src/connectivity/auth/README.md +36 -0
- package/src/connectivity/auth/api-key/index.ts +43 -0
- package/src/connectivity/auth/authorize.ts +49 -0
- package/src/connectivity/auth/basic/index.ts +68 -0
- package/src/connectivity/auth/bearer/index.ts +13 -0
- package/src/connectivity/auth/credential.ts +19 -0
- package/src/connectivity/auth/header/index.ts +18 -0
- package/src/connectivity/auth/index.ts +35 -0
- package/src/connectivity/auth/none/index.ts +12 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
- package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
- package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
- package/src/connectivity/auth/resolve.ts +61 -0
- package/src/connectivity/auth/strategy/index.ts +14 -0
- package/src/connectivity/capability.ts +164 -0
- package/src/connectivity/connector.ts +175 -0
- package/src/connectivity/context.ts +77 -0
- package/src/connectivity/index.ts +94 -0
- package/src/connectivity/mail/attachments.ts +368 -0
- package/src/connectivity/mail/compose.ts +73 -0
- package/src/connectivity/mail/index.ts +40 -0
- package/src/connectivity/mail/message.ts +82 -0
- package/src/connectivity/mail/nodemailer.d.ts +83 -0
- package/src/connectivity/mail/staging.ts +143 -0
- package/src/connectivity/mail/url.ts +301 -0
- package/src/connectivity/manifest/auth.ts +135 -0
- package/src/connectivity/manifest/bundles.ts +23 -0
- package/src/connectivity/manifest/connector.ts +168 -0
- package/src/connectivity/manifest/credential-ref.ts +73 -0
- package/src/connectivity/manifest/identity.ts +43 -0
- package/src/connectivity/manifest/index.ts +46 -0
- package/src/connectivity/manifest/primitives.ts +20 -0
- package/src/connectivity/manifest/provider.ts +208 -0
- package/src/connectivity/manifest/requirements.ts +146 -0
- package/src/connectivity/manifest/setup.ts +55 -0
- package/src/connectivity/provider.ts +163 -0
- package/src/connectivity/transports/README.md +33 -0
- package/src/connectivity/transports/composite/index.ts +68 -0
- package/src/connectivity/transports/dav/calendar.ts +217 -0
- package/src/connectivity/transports/dav/capabilities.ts +151 -0
- package/src/connectivity/transports/dav/client.ts +226 -0
- package/src/connectivity/transports/dav/contacts.ts +75 -0
- package/src/connectivity/transports/dav/ical.ts +412 -0
- package/src/connectivity/transports/dav/index.ts +143 -0
- package/src/connectivity/transports/dav/operations.ts +43 -0
- package/src/connectivity/transports/dav/request.ts +161 -0
- package/src/connectivity/transports/dav/xml.ts +123 -0
- package/src/connectivity/transports/factory.ts +181 -0
- package/src/connectivity/transports/fs/capabilities.ts +88 -0
- package/src/connectivity/transports/fs/commands.ts +258 -0
- package/src/connectivity/transports/fs/index.ts +121 -0
- package/src/connectivity/transports/fs/operations.ts +45 -0
- package/src/connectivity/transports/fs/paths.ts +120 -0
- package/src/connectivity/transports/fs/result.ts +12 -0
- package/src/connectivity/transports/http/index.ts +255 -0
- package/src/connectivity/transports/imap/attachment.ts +166 -0
- package/src/connectivity/transports/imap/capabilities.ts +158 -0
- package/src/connectivity/transports/imap/client.ts +398 -0
- package/src/connectivity/transports/imap/commands.ts +385 -0
- package/src/connectivity/transports/imap/index.ts +152 -0
- package/src/connectivity/transports/imap/operations.ts +64 -0
- package/src/connectivity/transports/imap/parse.ts +130 -0
- package/src/connectivity/transports/imap/parser.ts +272 -0
- package/src/connectivity/transports/imap/result.ts +15 -0
- package/src/connectivity/transports/imap/send.ts +92 -0
- package/src/connectivity/transports/imap/socket.ts +111 -0
- package/src/connectivity/transports/imap/utf7.ts +136 -0
- package/src/connectivity/transports/index.ts +20 -0
- package/src/connectivity/transports/local/index.ts +173 -0
- package/src/connectivity/transports/mcp/index.ts +215 -0
- package/src/deployments/README.md +63 -0
- package/src/deployments/adapters/audit-blob.ts +203 -0
- package/src/deployments/adapters/filesystem.ts +184 -0
- package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
- package/src/deployments/adapters/gcs.ts +191 -0
- package/src/deployments/adapters/otlp.ts +128 -0
- package/src/deployments/adapters/s3.ts +195 -0
- package/src/deployments/azure/README.md +21 -0
- package/src/deployments/bootstrap.ts +177 -0
- package/src/deployments/deploy.ts +290 -0
- package/src/deployments/driver.ts +157 -0
- package/src/deployments/drivers.ts +35 -0
- package/src/deployments/gcp/Dockerfile +70 -0
- package/src/deployments/gcp/cloudbuild.yaml +31 -0
- package/src/deployments/gcp/driver.ts +175 -0
- package/src/deployments/gcp/gcloud.ts +178 -0
- package/src/deployments/gcp/provision.ts +290 -0
- package/src/deployments/gcp/survey.ts +319 -0
- package/src/deployments/local/README.md +12 -0
- package/src/deployments/prepare.ts +257 -0
- package/src/deployments/steps.ts +137 -0
- package/src/deployments/target.ts +295 -0
- package/src/deployments/upload.ts +207 -0
- package/src/dispatch/context.ts +195 -0
- package/src/dispatch/dispatch.ts +350 -0
- package/src/dispatch/index.ts +32 -0
- package/src/dispatch/staging.ts +102 -0
- package/src/policy/index.ts +179 -0
- package/src/policy/limits.ts +77 -0
- package/src/profile/authorization.ts +81 -0
- package/src/profile/files.ts +71 -0
- package/src/profile/index.ts +76 -0
- package/src/profile/layout.ts +123 -0
- package/src/profile/load.ts +199 -0
- package/src/profile/primitives.ts +45 -0
- package/src/profile/schema.ts +347 -0
- package/src/profile/secret-detection.ts +162 -0
- package/src/profile/targets.ts +152 -0
- package/src/profile/workspace.ts +262 -0
- package/src/providers/custom/index.ts +21 -0
- package/src/providers/custom/load.ts +115 -0
- package/src/providers/custom/template.ts +156 -0
- package/src/providers/example/provider.ts +207 -0
- package/src/providers/google/calendar/index.ts +66 -0
- package/src/providers/google/calendar/redact.ts +40 -0
- package/src/providers/google/contacts/index.ts +50 -0
- package/src/providers/google/contacts/redact.ts +21 -0
- package/src/providers/google/docs/index.ts +45 -0
- package/src/providers/google/drive/hints.ts +28 -0
- package/src/providers/google/drive/index.ts +34 -0
- package/src/providers/google/drive/redact.ts +39 -0
- package/src/providers/google/drive-mcp/index.ts +21 -0
- package/src/providers/google/gmail/api.ts +42 -0
- package/src/providers/google/gmail/attachment.ts +142 -0
- package/src/providers/google/gmail/hints.ts +55 -0
- package/src/providers/google/gmail/index.ts +112 -0
- package/src/providers/google/gmail/redact.ts +56 -0
- package/src/providers/google/gmail/send.ts +365 -0
- package/src/providers/google/gmail-mcp/index.ts +35 -0
- package/src/providers/google/index.ts +10 -0
- package/src/providers/google/shared/oauth.ts +122 -0
- package/src/providers/google/shared/scopes.ts +99 -0
- package/src/providers/google/shared/setup.ts +80 -0
- package/src/providers/google/sheets/hints.ts +45 -0
- package/src/providers/google/sheets/index.ts +70 -0
- package/src/providers/google/sheets/redact.ts +45 -0
- package/src/providers/google/specs/calendar.v3.json +1829 -0
- package/src/providers/google/specs/docs.v1.json +381 -0
- package/src/providers/google/specs/drive.v3.json +2208 -0
- package/src/providers/google/specs/gmail.v1.json +2578 -0
- package/src/providers/google/specs/people.v1.json +506 -0
- package/src/providers/google/specs/sheets.v4.json +1269 -0
- package/src/providers/google/specs/tasks.v1.json +840 -0
- package/src/providers/google/specs/vendor.ts +661 -0
- package/src/providers/google/tasks/index.ts +53 -0
- package/src/providers/google/tasks/redact.ts +34 -0
- package/src/providers/harness.ts +95 -0
- package/src/providers/icloud/calendar/index.ts +27 -0
- package/src/providers/icloud/contacts/index.ts +17 -0
- package/src/providers/icloud/drive/index.ts +47 -0
- package/src/providers/icloud/index.ts +8 -0
- package/src/providers/icloud/mail/index.ts +37 -0
- package/src/providers/icloud/shared/setup.ts +66 -0
- package/src/providers/index.ts +93 -0
- package/src/providers/linear/index.ts +11 -0
- package/src/providers/linear/scopes.ts +7 -0
- package/src/providers/memory/provider.ts +429 -0
- package/src/providers/notion/index.ts +19 -0
- package/src/providers/owner.ts +49 -0
- package/src/providers/scopes.ts +26 -0
- package/src/providers/setup/plan.ts +141 -0
- package/src/providers/setup/provider.ts +323 -0
- package/src/providers/shared/frontmatter.ts +119 -0
- package/src/providers/skills/provider.ts +283 -0
- package/src/providers/skills/store.ts +252 -0
- package/src/providers/vault/provider.ts +194 -0
- package/src/registry/index.ts +36 -0
- package/src/registry/policy-bridge.ts +32 -0
- package/src/registry/reconcile.ts +313 -0
- package/src/registry/registry.ts +240 -0
- package/src/secrets/document.ts +293 -0
- package/src/secrets/index.ts +154 -0
- package/src/secrets/system.ts +173 -0
- package/src/secrets/vault.ts +336 -0
- package/src/server/attachments.ts +197 -0
- package/src/server/container.ts +96 -0
- package/src/server/edge.ts +53 -0
- package/src/server/endpoint.ts +352 -0
- package/src/server/generations.ts +362 -0
- package/src/server/harness.ts +400 -0
- package/src/server/index.ts +331 -0
- package/src/server/logging.ts +41 -0
- package/src/server/mcp/build.ts +68 -0
- package/src/server/mcp/icon.ts +145 -0
- package/src/server/mcp/index.ts +32 -0
- package/src/server/mcp/instructions.ts +245 -0
- package/src/server/mcp/naming.ts +39 -0
- package/src/server/mcp/prompts.ts +78 -0
- package/src/server/mcp/resources.ts +106 -0
- package/src/server/mcp/routing.ts +117 -0
- package/src/server/mcp/schema.ts +78 -0
- package/src/server/mcp/tools.ts +186 -0
- package/src/server/mcp/visibility.ts +132 -0
- package/src/server/oauth.ts +222 -0
- package/src/server/rebinding.ts +53 -0
- package/src/server/stdio.ts +192 -0
- package/src/stores/blobs/conformance.ts +223 -0
- package/src/stores/blobs/index.ts +125 -0
- package/src/stores/blobs/testing.ts +49 -0
- package/src/stores/state/index.ts +247 -0
- package/src/stores/state/keys.ts +68 -0
- 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
|
+
}
|