@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,240 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RESERVED_PROVIDER_IDS,
|
|
3
|
+
type Capability,
|
|
4
|
+
type DiscoveredCapability,
|
|
5
|
+
type ProviderDefinition,
|
|
6
|
+
type ProviderManifest,
|
|
7
|
+
} from '#connectivity';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The provider registry.
|
|
11
|
+
*
|
|
12
|
+
* A provider is a **manifest**. Local providers additionally carry code, so the
|
|
13
|
+
* registry holds one shape regardless of connectivity: every entry has a
|
|
14
|
+
* manifest, and `definition` is present only for `local`.
|
|
15
|
+
*
|
|
16
|
+
* Capabilities come from two places and the difference is the point of this
|
|
17
|
+
* milestone:
|
|
18
|
+
*
|
|
19
|
+
* - **local** providers declare them in code (Zod input schemas)
|
|
20
|
+
* - **mcp / http** providers have them **discovered** — from an upstream
|
|
21
|
+
* server's `tools/list` or from an OpenAPI document — and cached in the
|
|
22
|
+
* database, so a stateless instance serves without a discovery round trip
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface RegisteredProvider {
|
|
26
|
+
readonly manifest: ProviderManifest;
|
|
27
|
+
/** Present only for `local` connectors. */
|
|
28
|
+
readonly definition?: ProviderDefinition;
|
|
29
|
+
/** Where this came from, so `provider list` can distinguish shipped from yours. */
|
|
30
|
+
readonly origin: 'builtin' | 'workspace';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RegisteredCapability {
|
|
34
|
+
/** Fully qualified: `notion.search`. What policy rules and audit events name. */
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly provider: string;
|
|
37
|
+
/** For local providers. */
|
|
38
|
+
readonly capability?: Capability;
|
|
39
|
+
/** For discovered providers: re-derived at startup where the spec is committed, cached otherwise. */
|
|
40
|
+
readonly discovered?: DiscoveredCapability;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class ProviderRegistry {
|
|
44
|
+
readonly #providers = new Map<string, RegisteredProvider>();
|
|
45
|
+
/** provider id → discovered capabilities. See `cli/runtime/discovery.ts` for where they come from. */
|
|
46
|
+
readonly #discovered = new Map<string, DiscoveredCapability[]>();
|
|
47
|
+
readonly #allowReserved: boolean;
|
|
48
|
+
#revision = 0;
|
|
49
|
+
|
|
50
|
+
constructor(options: { allowReserved?: boolean } = {}) {
|
|
51
|
+
this.#allowReserved = options.allowReserved ?? false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Bumped whenever the capability set changes.
|
|
56
|
+
*
|
|
57
|
+
* The endpoint memoises work derived from this registry — which wire names it
|
|
58
|
+
* advertises, which ids exist — and those memos were computed once and could
|
|
59
|
+
* not notice a `replace`. Reading a number is cheaper than recomputing them
|
|
60
|
+
* per request and safer than assuming they never go stale.
|
|
61
|
+
*/
|
|
62
|
+
get revision(): number {
|
|
63
|
+
return this.#revision;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* `memory`, `skills`, and `vault` are refused until the owner layer ships.
|
|
68
|
+
* Reserving them costs nothing today; reclaiming a namespace after providers
|
|
69
|
+
* exist in the wild would silently change what a policy rule means.
|
|
70
|
+
*/
|
|
71
|
+
register(entry: ProviderDefinition | ProviderManifest, origin: 'builtin' | 'workspace' = 'builtin'): void {
|
|
72
|
+
const manifest = 'manifest' in entry ? entry.manifest : entry;
|
|
73
|
+
const definition = 'manifest' in entry ? entry : undefined;
|
|
74
|
+
|
|
75
|
+
if (this.#providers.has(manifest.id)) {
|
|
76
|
+
throw new Error(`Provider "${manifest.id}" is already registered`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!this.#allowReserved && RESERVED_PROVIDER_IDS.includes(manifest.id)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Provider id "${manifest.id}" is reserved for the owner layer (memory, skills, vault) and cannot be claimed.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.#providers.set(manifest.id, {
|
|
86
|
+
manifest,
|
|
87
|
+
...(definition ? { definition } : {}),
|
|
88
|
+
origin,
|
|
89
|
+
});
|
|
90
|
+
this.#revision++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Swap a provider already registered under this id, keeping its origin.
|
|
95
|
+
*
|
|
96
|
+
* Only `skills` uses this, and only because each skill is its own capability:
|
|
97
|
+
* a skill written after the process started is invisible until the registry
|
|
98
|
+
* holds it, and restarting an endpoint to pick up a file is the friction
|
|
99
|
+
* ADR-014 set out to remove.
|
|
100
|
+
*
|
|
101
|
+
* **Not a general hot-reload facility.** Replacing a provider mid-flight
|
|
102
|
+
* changes what policy is evaluated against between one call and the next, so
|
|
103
|
+
* the only safe subjects are providers whose capabilities are pure data. The
|
|
104
|
+
* vault deliberately does *not* use this — see ADR-012 §3, where an item
|
|
105
|
+
* becoming readable only after a restart is the property, not the delay.
|
|
106
|
+
*/
|
|
107
|
+
replace(entry: ProviderDefinition | ProviderManifest): void {
|
|
108
|
+
const manifest = 'manifest' in entry ? entry.manifest : entry;
|
|
109
|
+
const definition = 'manifest' in entry ? entry : undefined;
|
|
110
|
+
|
|
111
|
+
const existing = this.#providers.get(manifest.id);
|
|
112
|
+
if (!existing) {
|
|
113
|
+
throw new Error(`Provider "${manifest.id}" is not registered, so there is nothing to replace`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.#providers.set(manifest.id, {
|
|
117
|
+
manifest,
|
|
118
|
+
...(definition ? { definition } : {}),
|
|
119
|
+
origin: existing.origin,
|
|
120
|
+
});
|
|
121
|
+
this.#revision++;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get(id: string): RegisteredProvider | undefined {
|
|
125
|
+
return this.#providers.get(id);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
manifest(id: string): ProviderManifest | undefined {
|
|
129
|
+
return this.#providers.get(id)?.manifest;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
has(id: string): boolean {
|
|
133
|
+
return this.#providers.has(id);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
list(): RegisteredProvider[] {
|
|
137
|
+
return [...this.#providers.values()].sort((a, b) => a.manifest.id.localeCompare(b.manifest.id));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Load discovered capabilities for a provider, from the database cache.
|
|
142
|
+
*
|
|
143
|
+
* Discovery itself happens in the CLI at connect and refresh time. The server
|
|
144
|
+
* only ever reads what was cached, which is what keeps it stateless.
|
|
145
|
+
*/
|
|
146
|
+
setDiscovered(providerId: string, capabilities: readonly DiscoveredCapability[]): void {
|
|
147
|
+
this.#discovered.set(providerId, [...capabilities]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
discovered(providerId: string): readonly DiscoveredCapability[] {
|
|
151
|
+
return this.#discovered.get(providerId) ?? [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Every capability across every registered provider, fully qualified.
|
|
156
|
+
*
|
|
157
|
+
* Hand-written and discovered are added together rather than either/or. A
|
|
158
|
+
* `local` provider still has only the first and an `mcp` provider only the
|
|
159
|
+
* second, but a remote provider may now carry a handful of authored
|
|
160
|
+
* capabilities beside the ones its OpenAPI document describes — which is how a
|
|
161
|
+
* mail API gets a send that assembles MIME here, since the generic HTTP
|
|
162
|
+
* transport can only issue a JSON body and no document describes composing
|
|
163
|
+
* one.
|
|
164
|
+
*
|
|
165
|
+
* Authored wins on a name collision. The overlap is the reason to author one at
|
|
166
|
+
* all: the discovered version is the thing being replaced.
|
|
167
|
+
*/
|
|
168
|
+
capabilities(): RegisteredCapability[] {
|
|
169
|
+
return this.list().flatMap((entry): RegisteredCapability[] => {
|
|
170
|
+
const providerId = entry.manifest.id;
|
|
171
|
+
const authored = entry.definition?.capabilities ?? [];
|
|
172
|
+
const names = new Set(authored.map((capability) => capability.name));
|
|
173
|
+
|
|
174
|
+
return [
|
|
175
|
+
...authored.map((capability) => ({
|
|
176
|
+
id: `${providerId}.${capability.name}`,
|
|
177
|
+
provider: providerId,
|
|
178
|
+
capability,
|
|
179
|
+
})),
|
|
180
|
+
...this.discovered(providerId)
|
|
181
|
+
.filter((capability) => !names.has(capability.name))
|
|
182
|
+
.map((capability) => ({
|
|
183
|
+
id: `${providerId}.${capability.name}`,
|
|
184
|
+
provider: providerId,
|
|
185
|
+
discovered: capability,
|
|
186
|
+
})),
|
|
187
|
+
];
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
findCapability(id: string): RegisteredCapability | undefined {
|
|
192
|
+
return this.capabilities().find((entry) => entry.id === id);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Which capability ids a bundle expands to.
|
|
197
|
+
*
|
|
198
|
+
* Local bundles name capabilities exactly. Discovered bundles match by glob,
|
|
199
|
+
* because the names are not known when the manifest is written — and for
|
|
200
|
+
* `http` connectors the bundle is decided by HTTP method, so the connector
|
|
201
|
+
* assigns it during discovery rather than the manifest guessing.
|
|
202
|
+
*/
|
|
203
|
+
expandBundle(providerId: string, bundleName: string): string[] {
|
|
204
|
+
const entry = this.#providers.get(providerId);
|
|
205
|
+
if (!entry) return [];
|
|
206
|
+
|
|
207
|
+
const bundle = entry.manifest.bundles?.find((candidate) => candidate.name === bundleName);
|
|
208
|
+
const discovered = this.discovered(providerId);
|
|
209
|
+
|
|
210
|
+
if (discovered.length > 0) {
|
|
211
|
+
return discovered
|
|
212
|
+
.filter((capability) =>
|
|
213
|
+
bundle
|
|
214
|
+
? capability.bundle === bundleName ||
|
|
215
|
+
bundle.capabilities.some((pattern) => matchesGlob(pattern, capability.name))
|
|
216
|
+
: capability.bundle === bundleName,
|
|
217
|
+
)
|
|
218
|
+
.map((capability) => `${providerId}.${capability.name}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return (bundle?.capabilities ?? []).map((name) => `${providerId}.${name}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
defaultBundle(providerId: string): string | undefined {
|
|
225
|
+
const manifest = this.#providers.get(providerId)?.manifest;
|
|
226
|
+
return (manifest?.bundles?.find((bundle) => bundle.default) ?? manifest?.bundles?.[0])?.name;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** `*` matches any run of characters. Deliberately not a full glob dialect. */
|
|
231
|
+
export function matchesGlob(pattern: string, value: string): boolean {
|
|
232
|
+
if (!pattern.includes('*')) return pattern === value;
|
|
233
|
+
|
|
234
|
+
const expression = pattern
|
|
235
|
+
.split('*')
|
|
236
|
+
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
237
|
+
.join('.*');
|
|
238
|
+
|
|
239
|
+
return new RegExp(`^${expression}$`).test(value);
|
|
240
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
5
|
+
import type { BlobStore } from '#stores/blobs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One encrypted document, and everything that goes into keeping one.
|
|
9
|
+
*
|
|
10
|
+
* This exists because there were two of these. The system credential store and
|
|
11
|
+
* the vault had each grown their own AES-256-GCM envelope, their own
|
|
12
|
+
* write-then-rename, their own "read the key from an env var, else a sibling
|
|
13
|
+
* `.key` file, else mint one at 0600", and their own decrypt-failure message —
|
|
14
|
+
* differing in a magic string and nothing else that mattered. Two
|
|
15
|
+
* implementations of a format is two chances to get a format wrong, and the one
|
|
16
|
+
* this project cannot afford to get wrong is this one.
|
|
17
|
+
*
|
|
18
|
+
* What is *not* merged is the two stores themselves. They keep separate
|
|
19
|
+
* documents, separate keys, and separate environment variables, because that is
|
|
20
|
+
* the boundary `docs/detailed/security.md` is built on: one master secret reused across
|
|
21
|
+
* purposes turns any single compromise into a total one. Sharing the code that
|
|
22
|
+
* seals a document is the opposite of sharing the key that opens it.
|
|
23
|
+
*
|
|
24
|
+
* **The whole document is encrypted, names included.** A credential store whose
|
|
25
|
+
* key names were readable would disclose which accounts exist and how many;
|
|
26
|
+
* a vault whose item names were readable would disclose what the owner keeps.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const FORMAT_VERSION = 1;
|
|
30
|
+
export const KEY_BYTES = 32;
|
|
31
|
+
const IV_BYTES = 12; // GCM standard; 96 bits is what the mode is specified for.
|
|
32
|
+
|
|
33
|
+
interface EncryptedDocument {
|
|
34
|
+
readonly magic: string;
|
|
35
|
+
readonly version: number;
|
|
36
|
+
readonly iv: string;
|
|
37
|
+
readonly tag: string;
|
|
38
|
+
readonly ciphertext: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Where a document is kept.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately smaller than a storage interface: read it, write it, and say what
|
|
45
|
+
* to call it in an error. Nothing about the ciphertext depends on which
|
|
46
|
+
* implementation is in use, which is what lets a target switch without the
|
|
47
|
+
* format changing.
|
|
48
|
+
*/
|
|
49
|
+
export interface DocumentIO {
|
|
50
|
+
/** The stored document, or null when nothing has been written yet. */
|
|
51
|
+
read(): Promise<string | null>;
|
|
52
|
+
write(text: string): Promise<void>;
|
|
53
|
+
/** Named in every error, so "could not decrypt" says which document. */
|
|
54
|
+
readonly label: string;
|
|
55
|
+
/** Where to look for the key, appended to the remediation in a decrypt error. */
|
|
56
|
+
readonly keyHint: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Resolve the 32-byte key, however this target supplies one. */
|
|
60
|
+
export type KeySource = () => Promise<Uint8Array>;
|
|
61
|
+
|
|
62
|
+
export function fileDocumentIO(path: string, keyPath: string): DocumentIO {
|
|
63
|
+
return {
|
|
64
|
+
label: path,
|
|
65
|
+
keyHint: ` or ${keyPath}`,
|
|
66
|
+
|
|
67
|
+
async read() {
|
|
68
|
+
return existsSync(path) ? readFile(path, 'utf8') : null;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async write(text) {
|
|
72
|
+
await mkdir(dirname(path), { recursive: true });
|
|
73
|
+
|
|
74
|
+
// Write-then-rename so a crash cannot leave a truncated store.
|
|
75
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
76
|
+
await writeFile(temporary, text, { mode: 0o600 });
|
|
77
|
+
await rename(temporary, path);
|
|
78
|
+
await chmod(path, 0o600);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function blobDocumentIO(store: BlobStore, key: string): DocumentIO {
|
|
84
|
+
return {
|
|
85
|
+
label: key,
|
|
86
|
+
keyHint: '',
|
|
87
|
+
|
|
88
|
+
async read() {
|
|
89
|
+
const bytes = await store.get(key);
|
|
90
|
+
return bytes === null ? null : new TextDecoder().decode(bytes);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
async write(text) {
|
|
94
|
+
await store.put(key, new TextEncoder().encode(text), { contentType: 'application/json' });
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The document as one entry in a secret store.
|
|
101
|
+
*
|
|
102
|
+
* Structurally typed rather than importing `SecretStore` from `./index.ts`,
|
|
103
|
+
* which imports this file — a type-only cycle would erase cleanly, but the two
|
|
104
|
+
* methods used are the whole of what is needed and naming them is clearer than
|
|
105
|
+
* pointing back up.
|
|
106
|
+
*
|
|
107
|
+
* The size guard is Secret Manager's payload limit. Hitting it should say what
|
|
108
|
+
* happened and what the ceiling is, rather than surfacing as a REST error from
|
|
109
|
+
* inside a write that already looked like it was going to work.
|
|
110
|
+
*/
|
|
111
|
+
export function secretDocumentIO(
|
|
112
|
+
store: {
|
|
113
|
+
get(ref: string): Promise<string | null>;
|
|
114
|
+
set(ref: string, value: string): Promise<void>;
|
|
115
|
+
},
|
|
116
|
+
ref: string,
|
|
117
|
+
limitBytes = 64 * 1024,
|
|
118
|
+
): DocumentIO {
|
|
119
|
+
return {
|
|
120
|
+
label: ref,
|
|
121
|
+
keyHint: '',
|
|
122
|
+
|
|
123
|
+
read: () => store.get(ref),
|
|
124
|
+
|
|
125
|
+
async write(text) {
|
|
126
|
+
const size = new TextEncoder().encode(text).byteLength;
|
|
127
|
+
if (size > limitBytes) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`The vault document is ${size} bytes, over the ${limitBytes}-byte limit for one secret. ` +
|
|
130
|
+
'A vault holds keys and passwords; something much larger than that belongs in a file.',
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
await store.set(ref, text);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A key from the environment, from a sibling file, or newly minted.
|
|
140
|
+
*
|
|
141
|
+
* The order matters and so does the fallback's absence in the blob case: a file
|
|
142
|
+
* store may mint a key on first use because it can write it beside the document
|
|
143
|
+
* at 0600 and that file survives the process. A deployment has nowhere
|
|
144
|
+
* equivalent to put one — writing it beside the ciphertext it protects would
|
|
145
|
+
* encrypt nothing, and generating a fresh key per revision would make every
|
|
146
|
+
* previously stored item permanently unreadable while looking like it worked.
|
|
147
|
+
*/
|
|
148
|
+
export function fileKeySource(input: {
|
|
149
|
+
readonly keyPath: string;
|
|
150
|
+
readonly envVar: string;
|
|
151
|
+
readonly env: Record<string, string | undefined>;
|
|
152
|
+
readonly explicit?: Uint8Array | undefined;
|
|
153
|
+
}): KeySource {
|
|
154
|
+
return async () => {
|
|
155
|
+
const explicit = explicitKey(input.explicit, input.env, input.envVar);
|
|
156
|
+
if (explicit) return explicit;
|
|
157
|
+
|
|
158
|
+
if (existsSync(input.keyPath)) {
|
|
159
|
+
const decoded = decodeKey(await readFile(input.keyPath, 'utf8'));
|
|
160
|
+
assertKeyLength(decoded, input.keyPath);
|
|
161
|
+
return decoded;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const generated = new Uint8Array(randomBytes(KEY_BYTES));
|
|
165
|
+
await mkdir(dirname(input.keyPath), { recursive: true });
|
|
166
|
+
// Written 0600 from the start rather than chmod-ed afterwards, so there is
|
|
167
|
+
// no window in which the key is world-readable.
|
|
168
|
+
await writeFile(input.keyPath, Buffer.from(generated).toString('base64'), { mode: 0o600 });
|
|
169
|
+
await chmod(input.keyPath, 0o600);
|
|
170
|
+
return generated;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function envOnlyKeySource(input: {
|
|
175
|
+
readonly envVar: string;
|
|
176
|
+
readonly env: Record<string, string | undefined>;
|
|
177
|
+
readonly explicit?: Uint8Array | undefined;
|
|
178
|
+
readonly label: string;
|
|
179
|
+
/** The command that mints one, named because the caller knows which store this is. */
|
|
180
|
+
readonly remedy: string;
|
|
181
|
+
}): KeySource {
|
|
182
|
+
return async () => {
|
|
183
|
+
const explicit = explicitKey(input.explicit, input.env, input.envVar);
|
|
184
|
+
if (explicit) return explicit;
|
|
185
|
+
|
|
186
|
+
throw new Error(
|
|
187
|
+
`${input.label}: ${input.envVar} is required for a blob-backed store. ` +
|
|
188
|
+
`Generate one with "${input.remedy}" and store it in the deployment's secret manager — ` +
|
|
189
|
+
'this adapter will not mint a key, because a key it generated would be lost with the ' +
|
|
190
|
+
'process and take every stored item with it.',
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function explicitKey(
|
|
196
|
+
provided: Uint8Array | undefined,
|
|
197
|
+
env: Record<string, string | undefined>,
|
|
198
|
+
envVar: string,
|
|
199
|
+
): Uint8Array | null {
|
|
200
|
+
if (provided) {
|
|
201
|
+
assertKeyLength(provided, 'the key passed to the store');
|
|
202
|
+
return provided;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const fromEnv = env[envVar];
|
|
206
|
+
if (!fromEnv) return null;
|
|
207
|
+
|
|
208
|
+
const decoded = decodeKey(fromEnv);
|
|
209
|
+
assertKeyLength(decoded, envVar);
|
|
210
|
+
return decoded;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function decodeKey(value: string): Uint8Array {
|
|
214
|
+
return new Uint8Array(Buffer.from(value.trim(), 'base64'));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function assertKeyLength(key: Uint8Array, source: string): void {
|
|
218
|
+
if (key.length !== KEY_BYTES) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`${source} must decode to ${KEY_BYTES} bytes, got ${key.length}. ` +
|
|
221
|
+
'Generate one with: openssl rand -base64 32',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** A fresh key, base64, printed once and stored by the operator. */
|
|
227
|
+
export function generateKey(): string {
|
|
228
|
+
return randomBytes(KEY_BYTES).toString('base64');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Seal a payload into the on-disk envelope. */
|
|
232
|
+
export function seal(magic: string, key: Uint8Array, payload: unknown): string {
|
|
233
|
+
const iv = randomBytes(IV_BYTES); // Fresh per write; reuse under GCM is catastrophic.
|
|
234
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
235
|
+
|
|
236
|
+
const ciphertext = Buffer.concat([
|
|
237
|
+
cipher.update(JSON.stringify(payload), 'utf8'),
|
|
238
|
+
cipher.final(),
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
const document: EncryptedDocument = {
|
|
242
|
+
magic,
|
|
243
|
+
version: FORMAT_VERSION,
|
|
244
|
+
iv: iv.toString('base64'),
|
|
245
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
246
|
+
ciphertext: ciphertext.toString('base64'),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return JSON.stringify(document, null, 2);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Open a sealed document, or refuse.
|
|
254
|
+
*
|
|
255
|
+
* GCM authentication failing means the wrong key or an altered document. Both
|
|
256
|
+
* are refusals, never a partial read — a store that returned what it could
|
|
257
|
+
* decrypt would be a store an attacker can truncate.
|
|
258
|
+
*/
|
|
259
|
+
export function open<T>(
|
|
260
|
+
magic: string,
|
|
261
|
+
key: Uint8Array,
|
|
262
|
+
text: string,
|
|
263
|
+
io: Pick<DocumentIO, 'label' | 'keyHint'>,
|
|
264
|
+
envVar: string,
|
|
265
|
+
): T {
|
|
266
|
+
const document = JSON.parse(text) as EncryptedDocument;
|
|
267
|
+
|
|
268
|
+
if (document.magic !== magic) {
|
|
269
|
+
throw new Error(`${io.label}: not a ${magic} document (found ${JSON.stringify(document.magic)}).`);
|
|
270
|
+
}
|
|
271
|
+
if (document.version !== FORMAT_VERSION) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`${io.label}: format version ${document.version}, but this build understands ${FORMAT_VERSION}.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(document.iv, 'base64'));
|
|
279
|
+
decipher.setAuthTag(Buffer.from(document.tag, 'base64'));
|
|
280
|
+
|
|
281
|
+
const plaintext = Buffer.concat([
|
|
282
|
+
decipher.update(Buffer.from(document.ciphertext, 'base64')),
|
|
283
|
+
decipher.final(),
|
|
284
|
+
]).toString('utf8');
|
|
285
|
+
|
|
286
|
+
return JSON.parse(plaintext) as T;
|
|
287
|
+
} catch {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`${io.label}: could not decrypt. The key is wrong or the document has been modified. ` +
|
|
290
|
+
`Check ${envVar}${io.keyHint}.`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets — one component, one format, two stores that must never become one.
|
|
3
|
+
*
|
|
4
|
+
* "Secrets" means two different things here, and collapsing them is the most
|
|
5
|
+
* dangerous mistake available in this codebase:
|
|
6
|
+
*
|
|
7
|
+
* 1. **System credentials** (`./system.ts`) — OAuth refresh tokens, OAuth app
|
|
8
|
+
* client secrets, the profile API token. These authorise the system
|
|
9
|
+
* itself. They are NEVER reachable from MCP, in any form, for any client.
|
|
10
|
+
* If an agent could read the Gmail refresh token it would simply call
|
|
11
|
+
* Google directly, and the entire policy layer would become decorative.
|
|
12
|
+
*
|
|
13
|
+
* 2. **Vault items** (`./vault.ts`) — the owner's own passwords and API keys,
|
|
14
|
+
* which an agent may legitimately be granted access to, one at a time,
|
|
15
|
+
* under policy.
|
|
16
|
+
*
|
|
17
|
+
* They used to be two components with two implementations of the same
|
|
18
|
+
* AES-256-GCM document, two write-then-rename routines, and two copies of "read
|
|
19
|
+
* the key from an env var, else a sibling file, else mint one". That
|
|
20
|
+
* duplication was worth removing and is gone: `./document.ts` is the single
|
|
21
|
+
* format, and both stores are built on it with the same adapters.
|
|
22
|
+
*
|
|
23
|
+
* What is deliberately *not* shared is the part that matters — a separate
|
|
24
|
+
* document, a separate key, a separate environment variable. One master secret
|
|
25
|
+
* reused across purposes turns any single compromise into a total one.
|
|
26
|
+
* Two tests hold that line, each living with its own subject:
|
|
27
|
+
* `#dispatch`'s `control-plane.test.ts` asserts a `ProviderContext` has no path
|
|
28
|
+
* to a full store, and `#providers/vault`'s own test asserts that provider
|
|
29
|
+
* never so much as names one.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A key into a secret store, e.g. `google/client_secret`, `gmail/main`,
|
|
34
|
+
* `profile/token`. Config files carry these references; never values.
|
|
35
|
+
*/
|
|
36
|
+
export type SecretRef = string;
|
|
37
|
+
|
|
38
|
+
/** Matches `segment/segment[/segment...]`, lowercase, no traversal. */
|
|
39
|
+
const SECRET_REF_PATTERN = /^[a-z0-9][a-z0-9_-]*(?:\/[a-z0-9][a-z0-9_-]*)+$/;
|
|
40
|
+
|
|
41
|
+
export function isValidSecretRef(ref: string): boolean {
|
|
42
|
+
return SECRET_REF_PATTERN.test(ref);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function assertValidSecretRef(ref: string): void {
|
|
46
|
+
if (!isValidSecretRef(ref)) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Malformed secret reference: ${JSON.stringify(ref)}. ` +
|
|
49
|
+
'Expected lowercase segments separated by "/", e.g. "acme/main".',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The full store. Held by the CLI and by the dispatch layer; never handed to a
|
|
56
|
+
* provider, and never exposed through MCP.
|
|
57
|
+
*
|
|
58
|
+
* `list` returns keys only. There is no operation that enumerates values.
|
|
59
|
+
*/
|
|
60
|
+
export interface SecretStore {
|
|
61
|
+
get(ref: SecretRef): Promise<string | null>;
|
|
62
|
+
set(ref: SecretRef, value: string): Promise<void>;
|
|
63
|
+
has(ref: SecretRef): Promise<boolean>;
|
|
64
|
+
delete(ref: SecretRef): Promise<void>;
|
|
65
|
+
list(prefix?: string): Promise<SecretRef[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Drop whatever is held in memory so the next read reaches the store.
|
|
68
|
+
*
|
|
69
|
+
* Optional because most adapters hold nothing to drop. It exists for the one
|
|
70
|
+
* caller that has to see a value it did not write itself: the endpoint
|
|
71
|
+
* checking a bearer token that `lanes link token rotate` replaced from a
|
|
72
|
+
* different process. The file adapter keeps the whole decrypted document
|
|
73
|
+
* cached, so without this a re-read is served the copy it already had.
|
|
74
|
+
*/
|
|
75
|
+
refresh?(): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* What a provider sees: read-only, and only the references belonging to the one
|
|
80
|
+
* connection it was invoked for.
|
|
81
|
+
*
|
|
82
|
+
* A Gmail provider handling `gmail.main` cannot read `gmail.side`'s refresh
|
|
83
|
+
* token, cannot read `google/client_secret` unless its connection declares it,
|
|
84
|
+
* and cannot enumerate anything.
|
|
85
|
+
*/
|
|
86
|
+
export interface ScopedSecrets {
|
|
87
|
+
get(ref: SecretRef): Promise<string | null>;
|
|
88
|
+
has(ref: SecretRef): Promise<boolean>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Restrict a store to an explicit allowlist of references.
|
|
93
|
+
*
|
|
94
|
+
* Allowlist rather than prefix match: a provider's reachable secrets are exactly
|
|
95
|
+
* what its connection declares. A prefix rule would silently widen as new refs
|
|
96
|
+
* appear under the same prefix.
|
|
97
|
+
*/
|
|
98
|
+
export function scopeSecrets(
|
|
99
|
+
base: SecretStore,
|
|
100
|
+
allowed: readonly SecretRef[],
|
|
101
|
+
): ScopedSecrets {
|
|
102
|
+
const allowlist = new Set(allowed);
|
|
103
|
+
|
|
104
|
+
const check = (ref: SecretRef): void => {
|
|
105
|
+
if (!allowlist.has(ref)) {
|
|
106
|
+
// Deliberately does not reveal whether the ref exists in the underlying
|
|
107
|
+
// store — an out-of-scope ref is indistinguishable from a missing one.
|
|
108
|
+
throw new Error(`Secret ${JSON.stringify(ref)} is not in scope for this connection`);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
async get(ref) {
|
|
114
|
+
check(ref);
|
|
115
|
+
return base.get(ref);
|
|
116
|
+
},
|
|
117
|
+
async has(ref) {
|
|
118
|
+
check(ref);
|
|
119
|
+
return base.has(ref);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// The two stores
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
export {
|
|
129
|
+
createBlobSecretStore,
|
|
130
|
+
createFileSecretStore,
|
|
131
|
+
generateCredentialKey,
|
|
132
|
+
type BlobSecretsOptions,
|
|
133
|
+
type FileSystemSecretsOptions,
|
|
134
|
+
} from './system.ts';
|
|
135
|
+
|
|
136
|
+
export {
|
|
137
|
+
DEFAULT_SECRET_REF as VAULT_DOCUMENT_REF,
|
|
138
|
+
VAULT_ITEM_ID,
|
|
139
|
+
VAULT_KEY_ENV,
|
|
140
|
+
VAULT_KEY_REF,
|
|
141
|
+
assertItemId,
|
|
142
|
+
createBlobVaultStore,
|
|
143
|
+
createFileVaultStore,
|
|
144
|
+
createMemoryVaultStore,
|
|
145
|
+
createSecretVaultStore,
|
|
146
|
+
generateVaultKey,
|
|
147
|
+
type BlobVaultStoreOptions,
|
|
148
|
+
type FileVaultStoreOptions,
|
|
149
|
+
type SecretVaultStoreOptions,
|
|
150
|
+
type VaultItem,
|
|
151
|
+
type VaultStore,
|
|
152
|
+
} from './vault.ts';
|
|
153
|
+
|
|
154
|
+
export { generateKey, type DocumentIO } from './document.ts';
|