@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,319 @@
|
|
|
1
|
+
import { ConfigError, type DeployConfig, type TargetConfig } from '#profile';
|
|
2
|
+
import { heading, print, style, waiting } from '#cli/output.ts';
|
|
3
|
+
import { ask, confirm } from '#cli/prompt.ts';
|
|
4
|
+
import type { SurveyInput, SurveyResult } from '../driver.ts';
|
|
5
|
+
import { activeProject, openBillingAccounts, projectExists } from './gcloud.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Asking for what the config does not say yet.
|
|
9
|
+
*
|
|
10
|
+
* `deploy` used to refuse a target with no deployment block and tell the
|
|
11
|
+
* operator which four keys to go and write. That is a correct error message and
|
|
12
|
+
* a bad first run: the command knows what it needs, it can offer a sensible
|
|
13
|
+
* value for every one of them, and the answers belong in the file rather than in
|
|
14
|
+
* a flag the operator has to remember next time.
|
|
15
|
+
*
|
|
16
|
+
* It then refused a target that did not exist at all, which was the same mistake
|
|
17
|
+
* one level up. `credentials`, `storage` and `vault` have exactly one workable
|
|
18
|
+
* answer each on Cloud Run — Secret Manager, a bucket, and the sealed document
|
|
19
|
+
* beside the credentials — so a hand-written block could only ever be a
|
|
20
|
+
* transcription, and a wrong transcription fails somewhere unhelpful. The
|
|
21
|
+
* `filesystem` adapter in particular *appears* to work up here and loses
|
|
22
|
+
* everything on the next instance recycle.
|
|
23
|
+
*
|
|
24
|
+
* Every default is something already true rather than something invented — the
|
|
25
|
+
* project `gcloud` is pointed at, the profile's own name — so the common case is
|
|
26
|
+
* a handful of presses of return.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const DEFAULT_REGION = 'europe-west1';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Five random letters, for the two namespaces that are global to all of Google.
|
|
33
|
+
*
|
|
34
|
+
* A project id and a bucket name are both unique across every Google Cloud
|
|
35
|
+
* customer, so a memorable name is a name somebody already has. Google's own
|
|
36
|
+
* console does the same thing for the same reason.
|
|
37
|
+
*
|
|
38
|
+
* Letters rather than digits so the result is still pronounceable, and drawn
|
|
39
|
+
* from `crypto` rather than `Math.random` — not because a guessable bucket name
|
|
40
|
+
* is an vulnerability on its own, but because these names end up published in a
|
|
41
|
+
* URL and an unseeded PRNG that repeats across two machines produces a collision
|
|
42
|
+
* that looks like a permissions bug.
|
|
43
|
+
*/
|
|
44
|
+
function randomSuffix(length = 5): string {
|
|
45
|
+
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
|
|
46
|
+
const bytes = crypto.getRandomValues(new Uint8Array(length));
|
|
47
|
+
|
|
48
|
+
return [...bytes].map((byte) => alphabet[byte % alphabet.length]).join('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The globally-unique name this deploy proposes for itself.
|
|
53
|
+
*
|
|
54
|
+
* One suffix, one name, used for **both** the project and the bucket — they are
|
|
55
|
+
* separate namespaces, so the same string is free in one when it is free in the
|
|
56
|
+
* other, and a deployment whose project and bucket are called the same thing is
|
|
57
|
+
* one you can find either way round from either console.
|
|
58
|
+
*
|
|
59
|
+
* Generated exactly once. Every later deploy reads the name back out of the
|
|
60
|
+
* profile, because the survey only runs for what the config does not say — a
|
|
61
|
+
* second suffix would name a second empty project beside the one holding
|
|
62
|
+
* everything.
|
|
63
|
+
*
|
|
64
|
+
* Sixteen characters, inside the project id limit of 30 and the bucket limit of
|
|
65
|
+
* 63, with no profile name in it: the profile is in the *service* name, which is
|
|
66
|
+
* the thing that has to differ when one project holds two of these.
|
|
67
|
+
*/
|
|
68
|
+
export function proposedName(suffix = randomSuffix()): string {
|
|
69
|
+
return `lanes-link-${suffix}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* What the service is called when nobody says.
|
|
74
|
+
*
|
|
75
|
+
* It used to be the bare profile name — `personal`, in a project that may hold
|
|
76
|
+
* a dozen unrelated services, saying nothing about what it is or who put it
|
|
77
|
+
* there. `mcp` is the useful half: what this serves is an MCP endpoint, and that
|
|
78
|
+
* is what someone finds it looking for months later.
|
|
79
|
+
*
|
|
80
|
+
* The profile stays in the middle because one workspace can deploy several, and
|
|
81
|
+
* two profiles deploying to one project must not collide on a name.
|
|
82
|
+
*/
|
|
83
|
+
export function defaultServiceName(profile: string): string {
|
|
84
|
+
return `lanes-link-${profile}-mcp`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The local part of the runtime service account, which has its own length rule.
|
|
89
|
+
*
|
|
90
|
+
* Service account ids are capped at 30 characters and a Cloud Run service name
|
|
91
|
+
* at 49, so a legal service name can derive an illegal account. That failure is
|
|
92
|
+
* one `gcloud iam service-accounts create` away from being silent — the step
|
|
93
|
+
* tolerates failure, so the deploy would carry on and roll a revision with no
|
|
94
|
+
* identity, and the first symptom would be it failing to read a secret.
|
|
95
|
+
*/
|
|
96
|
+
export function accountId(service: string): string {
|
|
97
|
+
const proposed = `${service}-run`;
|
|
98
|
+
if (proposed.length <= 30) return proposed;
|
|
99
|
+
|
|
100
|
+
// Trim the service, not the suffix: `-run` is what says which account this is.
|
|
101
|
+
return `${service.slice(0, 30 - '-run'.length).replace(/-+$/, '')}-run`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function surveyCloudRun(input: SurveyInput): Promise<SurveyResult> {
|
|
105
|
+
const { current, profile, gated, adapters } = input;
|
|
106
|
+
|
|
107
|
+
heading('Where should this deploy?');
|
|
108
|
+
print(style.dim(' Answers are written into your profile, so this is asked once.'));
|
|
109
|
+
print('');
|
|
110
|
+
|
|
111
|
+
const project = await askProject(current.project);
|
|
112
|
+
const billing = await askBilling(project, current.billing_account);
|
|
113
|
+
const region = await askWithDefault(' Region', current.region ?? DEFAULT_REGION);
|
|
114
|
+
const service = await askWithDefault(
|
|
115
|
+
' Cloud Run service name',
|
|
116
|
+
current.service ?? defaultServiceName(profile),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Not asked. A revision needs an identity that can read the credential store,
|
|
120
|
+
// and the only interesting choice is *whether* it is dedicated — which it
|
|
121
|
+
// should always be, because the default compute account is shared with
|
|
122
|
+
// everything else in the project.
|
|
123
|
+
const serviceAccount =
|
|
124
|
+
current.service_account ?? `${accountId(service)}@${project}.iam.gserviceaccount.com`;
|
|
125
|
+
|
|
126
|
+
const access = await askAccess(current.access, gated);
|
|
127
|
+
|
|
128
|
+
const deploy: DeployConfig = {
|
|
129
|
+
platform: 'cloudrun',
|
|
130
|
+
project,
|
|
131
|
+
region,
|
|
132
|
+
service,
|
|
133
|
+
access,
|
|
134
|
+
service_account: serviceAccount,
|
|
135
|
+
...(billing ? { billing_account: billing } : {}),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Only the `deploy` block was missing: the adapters are already chosen, and
|
|
139
|
+
// re-asking would be offering to overwrite a working target.
|
|
140
|
+
if (!adapters) return { target: { deploy } as TargetConfig };
|
|
141
|
+
|
|
142
|
+
// The project's own name. One deployment, one string: the bucket namespace is
|
|
143
|
+
// separate from the project namespace, so a name free in one is free in the
|
|
144
|
+
// other, and finding either from the other needs nothing written down.
|
|
145
|
+
const bucket = await askBucket(project);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
target: {
|
|
149
|
+
credentials: { adapter: 'gcp-secret-manager', project },
|
|
150
|
+
storage: { adapter: 'gcs', bucket },
|
|
151
|
+
// Not optional, and not a preference. The default is `file`, which wants a
|
|
152
|
+
// filesystem path, and this workspace lives in a bucket — so a target
|
|
153
|
+
// without this line parses, deploys, and refuses to boot.
|
|
154
|
+
vault: { adapter: 'secret' },
|
|
155
|
+
deploy,
|
|
156
|
+
} as TargetConfig,
|
|
157
|
+
...(access === 'public' && !gated && (await askRemoteClients()) ? { authorization: { mode: 'self' } } : {}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Which project this deploys into, or the one it is about to create.
|
|
163
|
+
*
|
|
164
|
+
* The default used to be whatever `gcloud config` was pointed at, which is
|
|
165
|
+
* whichever project you last worked on — a value that is *always* set, rarely
|
|
166
|
+
* the right one, and wrong in the most expensive direction: accepting it deploys
|
|
167
|
+
* a personal gateway holding live refresh tokens into a project shared with
|
|
168
|
+
* something else. A fresh id proposes the opposite default, and typing an
|
|
169
|
+
* existing name over it is one line.
|
|
170
|
+
*/
|
|
171
|
+
async function askProject(current: string | undefined): Promise<string> {
|
|
172
|
+
const active = await waiting('reading your gcloud configuration', activeProject);
|
|
173
|
+
|
|
174
|
+
print(' A project of its own is the cleanest home for this — it holds the bucket,');
|
|
175
|
+
print(
|
|
176
|
+
style.dim(
|
|
177
|
+
' the credential store, and nothing else. A new id is proposed; type the name\n' +
|
|
178
|
+
` of an existing project to deploy into that instead${active ? ` (yours is ${active})` : ''}.`,
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return askWithDefault(' Google Cloud project', current ?? proposedName());
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The billing account, asked only when there is a project to attach it to.
|
|
187
|
+
*
|
|
188
|
+
* Skipped entirely for a project that already exists — it is already billed, or
|
|
189
|
+
* it is not and that is not this command's business. Asking anyway would be a
|
|
190
|
+
* question with no consequence, which is the kind that teaches people to press
|
|
191
|
+
* return without reading.
|
|
192
|
+
*
|
|
193
|
+
* Refused rather than defaulted when there is no open account: a project created
|
|
194
|
+
* without billing enables no APIs, and every step after it fails with a message
|
|
195
|
+
* about the API rather than about billing.
|
|
196
|
+
*/
|
|
197
|
+
async function askBilling(project: string, current: string | undefined): Promise<string | undefined> {
|
|
198
|
+
if (current) return current;
|
|
199
|
+
if (await waiting(`checking whether ${project} exists`, () => projectExists(project))) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const accounts = await waiting('listing your billing accounts', openBillingAccounts);
|
|
204
|
+
print('');
|
|
205
|
+
print(` ${style.bold(project)} does not exist yet, so this deploy will create it.`);
|
|
206
|
+
print(style.dim(' A new project needs a billing account before any API can be enabled.'));
|
|
207
|
+
|
|
208
|
+
if (accounts.length === 0) {
|
|
209
|
+
throw new ConfigError(
|
|
210
|
+
`No open billing account found for this login, and "${project}" would have to be created.\n` +
|
|
211
|
+
' Either name an existing project at the prompt, or set one up at\n' +
|
|
212
|
+
' https://console.cloud.google.com/billing and re-run.',
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const account of accounts) print(style.dim(` ${account.id} ${account.name}`));
|
|
217
|
+
return askWithDefault(' Billing account', accounts[0]!.id);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The one derived name that can collide with a stranger's.
|
|
222
|
+
*
|
|
223
|
+
* Bucket names are global across every Google Cloud customer, so this is asked
|
|
224
|
+
* rather than assumed even though the default is almost always free. `taken` is
|
|
225
|
+
* not a failure worth a stack trace three steps later.
|
|
226
|
+
*/
|
|
227
|
+
async function askBucket(project: string): Promise<string> {
|
|
228
|
+
print('');
|
|
229
|
+
print(' A bucket holds everything this endpoint remembers:');
|
|
230
|
+
print(
|
|
231
|
+
style.dim(
|
|
232
|
+
' its config, its connection state, the audit log, memory, skills and\n' +
|
|
233
|
+
' attachments. Named after the project by default; bucket names are\n' +
|
|
234
|
+
' globally unique, so this one may be taken even when the project was not.',
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
return askWithDefault(' Bucket', project);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Whether a remote client has to reach this, which is what `mode: self` decides.
|
|
242
|
+
*
|
|
243
|
+
* Asked, and asked here, because nothing else in a first run does. `access:
|
|
244
|
+
* public` opens the platform's door; it does not give a Claude or ChatGPT
|
|
245
|
+
* connector any way to *obtain* a token, and those clients have nowhere to paste
|
|
246
|
+
* a fixed one. Without `auth.authorization` the endpoint answers a connector's
|
|
247
|
+
* handshake with a 401 carrying nothing to act on, which reads as a broken
|
|
248
|
+
* server rather than a missing setting — so the previous default outcome of a
|
|
249
|
+
* first deploy was an endpoint that could not be added to a phone, and no
|
|
250
|
+
* message anywhere said why.
|
|
251
|
+
*/
|
|
252
|
+
async function askRemoteClients(): Promise<boolean> {
|
|
253
|
+
print('');
|
|
254
|
+
print(' Will you add this to Claude or ChatGPT, including on a phone?');
|
|
255
|
+
print(
|
|
256
|
+
style.dim(
|
|
257
|
+
' yes this endpoint issues its own tokens — the client registers itself,\n' +
|
|
258
|
+
' a browser opens on its approval page, you paste the token once.\n' +
|
|
259
|
+
' Nothing to set up: no OAuth client, no console, no redirect URI.\n' +
|
|
260
|
+
' no the bearer token is the only way in, which is all a local\n' +
|
|
261
|
+
' registration needs. Add it later under auth.authorization.',
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
return confirm(' Issue tokens for remote clients?');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Which door the platform leaves open.
|
|
269
|
+
*
|
|
270
|
+
* Asked rather than assumed, and asked with the consequence attached, because
|
|
271
|
+
* the two answers fail in opposite directions and neither failure is legible
|
|
272
|
+
* from the outside: `iam` looks like a broken endpoint to every MCP client, and
|
|
273
|
+
* `public` on a profile with no gate of its own is an open endpoint that answers
|
|
274
|
+
* normally.
|
|
275
|
+
*/
|
|
276
|
+
async function askAccess(
|
|
277
|
+
current: DeployConfig['access'] | undefined,
|
|
278
|
+
gated: boolean,
|
|
279
|
+
): Promise<DeployConfig['access']> {
|
|
280
|
+
const proposed = current ?? (gated ? 'public' : 'iam');
|
|
281
|
+
|
|
282
|
+
print('');
|
|
283
|
+
print(' Who may reach the service?');
|
|
284
|
+
print(
|
|
285
|
+
style.dim(
|
|
286
|
+
' iam the platform checks a Google identity token first. Nothing that\n' +
|
|
287
|
+
' cannot mint one gets through — which includes every agent harness.\n' +
|
|
288
|
+
' public the platform lets the request in and this endpoint authenticates it.',
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
if (gated) {
|
|
292
|
+
print(
|
|
293
|
+
style.dim(
|
|
294
|
+
' This profile authenticates requests itself, so "public" is the working\n' +
|
|
295
|
+
' choice — "iam" in front of it would lock out the clients it exists for.',
|
|
296
|
+
),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const answer = (await askWithDefault(' Access', proposed)).toLowerCase();
|
|
301
|
+
return answer === 'public' ? 'public' : 'iam';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Ask, showing the default, and take it on an empty answer.
|
|
306
|
+
*
|
|
307
|
+
* Refuses an empty value with no default rather than writing one: an empty
|
|
308
|
+
* service name parses, reaches `gcloud`, and fails several minutes into a build.
|
|
309
|
+
*/
|
|
310
|
+
async function askWithDefault(question: string, fallback: string | null | undefined): Promise<string> {
|
|
311
|
+
const shown = fallback ? `${question} ${style.dim(`[${fallback}]`)}` : question;
|
|
312
|
+
|
|
313
|
+
for (;;) {
|
|
314
|
+
const answer = (await ask(shown)).trim();
|
|
315
|
+
if (answer) return answer;
|
|
316
|
+
if (fallback) return fallback;
|
|
317
|
+
print(style.dim(' Required.'));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Local — the default target
|
|
2
|
+
|
|
3
|
+
SQLite, an encrypted file, and a directory. No network, no account, no
|
|
4
|
+
credential beyond the two encryption keys, and everything under one workspace
|
|
5
|
+
root that a `cp -R` backs up.
|
|
6
|
+
|
|
7
|
+
There is no `target.ts` here: the local branches of `../target.ts` are three
|
|
8
|
+
lines each and inlining them keeps the whole mapping readable in one place. A
|
|
9
|
+
deployment earns a folder when it has machinery of its own — `../gcp/` has a
|
|
10
|
+
Dockerfile, a build config, and a rollout driver.
|
|
11
|
+
|
|
12
|
+
What lives on disk, and where, is `#profile`'s `workspace.ts`.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { credentialRefFor, ownClientRefsFor, rotatableCredentialRefsFor } from '#registry';
|
|
2
|
+
import { listProfiles, loadProfileConfig, type Config, type TargetConfig } from '#profile';
|
|
3
|
+
import { VAULT_DOCUMENT_REF, VAULT_KEY_REF, generateVaultKey, type SecretStore } from '#secrets';
|
|
4
|
+
import { ok, print, style, warn } from '#cli/output.ts';
|
|
5
|
+
import { buildRegistryWithWorkspace, ensureProfileToken } from '#cli/runtime.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Getting the target's credential store to the state a revision can boot from.
|
|
9
|
+
*
|
|
10
|
+
* This began as a refusal — list what is missing, name the command that stores
|
|
11
|
+
* each one, exit. That is a correct refusal and three round trips: the operator
|
|
12
|
+
* runs `deploy`, reads the list, runs `token rotate`, runs `secrets set`, runs
|
|
13
|
+
* `deploy` again. Two of those values the command can produce or ask for right
|
|
14
|
+
* here, and the third is genuinely somebody else's to create.
|
|
15
|
+
*
|
|
16
|
+
* What blocks versus what warns follows what the runtime does with each: without
|
|
17
|
+
* the profile token the endpoint refuses to start, and without the database URL
|
|
18
|
+
* it cannot open a database at all. A connection credential that is missing
|
|
19
|
+
* leaves that connection `unauthorized`, which is a documented state rather than
|
|
20
|
+
* an outage — so it is said out loud and not treated as a failure.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface PrepareInput {
|
|
24
|
+
readonly config: Config;
|
|
25
|
+
readonly credentials: SecretStore;
|
|
26
|
+
readonly root: string;
|
|
27
|
+
readonly target: string;
|
|
28
|
+
/** Never prompt; report only. `--dry-run` must not write to a credential store. */
|
|
29
|
+
readonly readOnly: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PrepareResult {
|
|
33
|
+
/** Refs still missing after any seeding, which a revision cannot start without. */
|
|
34
|
+
readonly blocking: readonly string[];
|
|
35
|
+
readonly warnings: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Every credential reference a *running* revision rewrites — what `provision`
|
|
40
|
+
* needs in order to grant `secretVersionAdder` on each one and nothing else.
|
|
41
|
+
*
|
|
42
|
+
* Kept apart from the connection loop below rather than derived from it, because
|
|
43
|
+
* the two answer different questions and diverge on purpose: that loop asks
|
|
44
|
+
* where a connection's credential lives, which a hand-placed `credential_ref`
|
|
45
|
+
* may decide, and this asks what gets written while serving, which the refresh
|
|
46
|
+
* path derives from the manifest and has never read config for. Sharing one list
|
|
47
|
+
* would silently pick one answer for both.
|
|
48
|
+
*
|
|
49
|
+
* **Scoped exactly as the upload is**, for the reason `repairSetupSurface`
|
|
50
|
+
* states and one more: a profile this deploy sends is a profile the endpoint may
|
|
51
|
+
* serve, and a connection whose secret nobody bound fails an hour after the
|
|
52
|
+
* revision reports healthy. The asymmetry decides it — an extra binding is a
|
|
53
|
+
* resource-level grant on a secret the operator already owns, and a missing one
|
|
54
|
+
* is a 403 in somebody's chat window.
|
|
55
|
+
*
|
|
56
|
+
* A profile that cannot be read is skipped rather than fatal, matching the
|
|
57
|
+
* repair: `deploy` validates the profile it resolved, and a broken sibling
|
|
58
|
+
* should not cost the rollout. It goes up either way, and if it is served at
|
|
59
|
+
* all it is served broken for reasons that have nothing to do with IAM.
|
|
60
|
+
*
|
|
61
|
+
* Called before `provision`, which is before any store is opened — so it reads
|
|
62
|
+
* config and manifests only, and touches no credential.
|
|
63
|
+
*/
|
|
64
|
+
export async function rotatableRefs(root: string, profile: string | undefined): Promise<string[]> {
|
|
65
|
+
const refs = new Set<string>();
|
|
66
|
+
|
|
67
|
+
for (const name of await listProfiles(root)) {
|
|
68
|
+
if (profile !== undefined && name !== profile) continue;
|
|
69
|
+
|
|
70
|
+
let config: Config;
|
|
71
|
+
try {
|
|
72
|
+
({ config } = await loadProfileConfig(root, name));
|
|
73
|
+
} catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Inside the loop because manifests are the profile's own (ADR-030): a
|
|
78
|
+
// connection in `work` naming a provider only `work` declares resolves to
|
|
79
|
+
// nothing against `personal`'s registry, and a missed ref here is a 403 an
|
|
80
|
+
// hour after the revision reports healthy.
|
|
81
|
+
const registry = await buildRegistryWithWorkspace(root, name);
|
|
82
|
+
|
|
83
|
+
for (const connection of config.connections) {
|
|
84
|
+
const manifest = registry.manifest(connection.provider);
|
|
85
|
+
for (const ref of rotatableCredentialRefsFor(connection, manifest)) refs.add(ref);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Sorted, and a set: two Gmail connections share one dynamically registered
|
|
90
|
+
// client ref, and two profiles may name the same connection. A step list that
|
|
91
|
+
// repeated one would read as two different grants.
|
|
92
|
+
return [...refs].sort();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Every credential reference a running revision *reads*, so provision can bind
|
|
97
|
+
* `secretAccessor` on each one instead of on the whole project.
|
|
98
|
+
*
|
|
99
|
+
* The project-level grant this replaces was not an oversight — the adapter
|
|
100
|
+
* argued for it, on the grounds that the line worth defending is
|
|
101
|
+
* `secrets.create` — but it is broader than the revision ever needs, and the
|
|
102
|
+
* survey happily points a deploy at a project the operator already uses for
|
|
103
|
+
* other things. An SSRF or an RCE in the endpoint should reach this profile's
|
|
104
|
+
* own credentials, not every secret sharing a project with it.
|
|
105
|
+
*
|
|
106
|
+
* Feasible because the serving path only ever reads by explicit ref: `list()`
|
|
107
|
+
* is a CLI call, and `secretAccessor` never granted `secrets.list` anyway. So
|
|
108
|
+
* the set is knowable at deploy time, and it is the same walk `rotatableRefs`
|
|
109
|
+
* does — profiles, their connections, and the manifests behind them.
|
|
110
|
+
*
|
|
111
|
+
* Deliberately a superset, matching the asymmetry `rotatableRefs` states: an
|
|
112
|
+
* extra resource-level binding on a secret the operator already owns costs
|
|
113
|
+
* nothing, and a missing one is a 403 an hour after the revision reports
|
|
114
|
+
* healthy. Both `credentialRefFor` and `rotatableCredentialRefsFor` go in
|
|
115
|
+
* because the first honours a hand-placed `credential_ref` and the second is
|
|
116
|
+
* blind to it by design — the revision may read either.
|
|
117
|
+
*
|
|
118
|
+
* The same deploy-time bound the write side already lives with applies here: a
|
|
119
|
+
* connection authorised after a rollout is unreadable until the next one. That
|
|
120
|
+
* is not new, and `provision` is re-run by every deploy.
|
|
121
|
+
*/
|
|
122
|
+
export async function readableRefs(
|
|
123
|
+
root: string,
|
|
124
|
+
profile: string | undefined,
|
|
125
|
+
declared: TargetConfig | undefined,
|
|
126
|
+
): Promise<string[]> {
|
|
127
|
+
const refs = new Set<string>();
|
|
128
|
+
|
|
129
|
+
if (declared?.vault?.adapter === 'secret') {
|
|
130
|
+
refs.add(VAULT_KEY_REF);
|
|
131
|
+
refs.add(declared.vault.ref ?? VAULT_DOCUMENT_REF);
|
|
132
|
+
} else if (declared?.vault?.adapter === 'blob') {
|
|
133
|
+
// Ciphertext lives in the bucket, but the key that opens it is still here.
|
|
134
|
+
refs.add(VAULT_KEY_REF);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const name of await listProfiles(root)) {
|
|
138
|
+
if (profile !== undefined && name !== profile) continue;
|
|
139
|
+
|
|
140
|
+
let config: Config;
|
|
141
|
+
try {
|
|
142
|
+
({ config } = await loadProfileConfig(root, name));
|
|
143
|
+
} catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
refs.add(config.auth.token_ref);
|
|
148
|
+
// The OIDC audience check reads this on every verify (`server/endpoint.ts`).
|
|
149
|
+
if (config.auth.authorization?.mode === 'oidc') {
|
|
150
|
+
refs.add(config.auth.authorization.client_id_ref);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Per profile, for the reason `rotatableRefs` gives: a manifest is this
|
|
154
|
+
// profile's, so the registry that resolves its connections must be too.
|
|
155
|
+
const registry = await buildRegistryWithWorkspace(root, name);
|
|
156
|
+
|
|
157
|
+
for (const connection of config.connections) {
|
|
158
|
+
const manifest = registry.manifest(connection.provider);
|
|
159
|
+
const ref = credentialRefFor(connection, manifest);
|
|
160
|
+
if (ref) refs.add(ref);
|
|
161
|
+
for (const rotatable of rotatableCredentialRefsFor(connection, manifest)) refs.add(rotatable);
|
|
162
|
+
for (const client of ownClientRefsFor(manifest, config.oauth_apps)) refs.add(client);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return [...refs].sort();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function prepareSecrets(input: PrepareInput): Promise<PrepareResult> {
|
|
170
|
+
const { config, credentials, root, target, readOnly } = input;
|
|
171
|
+
const blocking: string[] = [];
|
|
172
|
+
const warnings: string[] = [];
|
|
173
|
+
|
|
174
|
+
await seedProfileToken({ config, credentials, readOnly, blocking });
|
|
175
|
+
await seedVaultKey({ declared: config.targets[target], credentials, readOnly });
|
|
176
|
+
|
|
177
|
+
// Connection credentials are written by `connect`, against a real account, in
|
|
178
|
+
// a browser. Nothing here can produce one, and a deploy that stopped for one
|
|
179
|
+
// would be refusing to roll a revision over a mailbox that has not been
|
|
180
|
+
// authorised yet — which is a thing you might reasonably want to do.
|
|
181
|
+
//
|
|
182
|
+
// The command is spelled out rather than described. This is the one manual
|
|
183
|
+
// step a deploy genuinely cannot take for you, so it should cost a paste
|
|
184
|
+
// rather than a trip to the docs to work out how the id is spelled.
|
|
185
|
+
const registry = await buildRegistryWithWorkspace(root, config.instance.profile);
|
|
186
|
+
for (const connection of config.connections) {
|
|
187
|
+
const ref = credentialRefFor(connection, registry.manifest(connection.provider));
|
|
188
|
+
if (!ref || (await credentials.has(ref))) continue;
|
|
189
|
+
warnings.push(
|
|
190
|
+
`${connection.provider}.${connection.id} is not authorised yet — no credential at "${ref}"\n` +
|
|
191
|
+
` lanes link connect ${connection.provider} --target ${target} --id ${connection.id}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { blocking, warnings };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The key that seals the vault document.
|
|
200
|
+
*
|
|
201
|
+
* Minted here for the same reason the profile token is: it is a random string
|
|
202
|
+
* this process generates correctly and nobody can usefully choose. It used to be
|
|
203
|
+
* three manual commands in `docs/detailed/deployment-cloudrun.md` — generate, store,
|
|
204
|
+
* mount — whose only failure mode was forgetting them and finding out when the
|
|
205
|
+
* first `vault.*` call failed against a revision that had booted healthy.
|
|
206
|
+
*
|
|
207
|
+
* Only for a target whose credential store can hold it. A `file` vault is a
|
|
208
|
+
* local run, where the environment variable is the operator's own business.
|
|
209
|
+
*
|
|
210
|
+
* Never regenerated. A second key does not fail — it decrypts nothing, and the
|
|
211
|
+
* document it cannot open is the one holding every password the owner put
|
|
212
|
+
* there.
|
|
213
|
+
*/
|
|
214
|
+
async function seedVaultKey(input: {
|
|
215
|
+
declared: TargetConfig | undefined;
|
|
216
|
+
credentials: SecretStore;
|
|
217
|
+
readOnly: boolean;
|
|
218
|
+
}): Promise<void> {
|
|
219
|
+
const adapter = input.declared?.vault?.adapter;
|
|
220
|
+
if (adapter !== 'secret' && adapter !== 'blob') return;
|
|
221
|
+
if (input.readOnly || (await input.credentials.has(VAULT_KEY_REF))) return;
|
|
222
|
+
|
|
223
|
+
await input.credentials.set(VAULT_KEY_REF, generateVaultKey());
|
|
224
|
+
print(ok(`minted the vault key at "${VAULT_KEY_REF}" — the revision reads it from there`));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The endpoint's own bearer token.
|
|
229
|
+
*
|
|
230
|
+
* Minted rather than demanded, and not even asked about: it is a random string
|
|
231
|
+
* this process generates correctly and the operator cannot usefully choose, so a
|
|
232
|
+
* prompt would be a question with one answer. `ensureProfileToken` is the same
|
|
233
|
+
* call `outputs` makes for a local target, which is what keeps "the token" one
|
|
234
|
+
* thing rather than a per-command notion.
|
|
235
|
+
*
|
|
236
|
+
* The deployed container deliberately will *not* do this — a token invented
|
|
237
|
+
* inside something that scales to zero is a token nobody can read back, and the
|
|
238
|
+
* endpoint would come up healthy while rejecting every agent.
|
|
239
|
+
*/
|
|
240
|
+
async function seedProfileToken(input: {
|
|
241
|
+
config: Config;
|
|
242
|
+
credentials: SecretStore;
|
|
243
|
+
readOnly: boolean;
|
|
244
|
+
blocking: string[];
|
|
245
|
+
}): Promise<void> {
|
|
246
|
+
const ref = input.config.auth.token_ref;
|
|
247
|
+
if (await input.credentials.has(ref)) return;
|
|
248
|
+
|
|
249
|
+
if (input.readOnly) {
|
|
250
|
+
input.blocking.push(`the endpoint bearer token — nothing at "${ref}"`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const { created } = await ensureProfileToken(input.credentials, ref);
|
|
255
|
+
if (created) print(ok(`minted an endpoint token at "${ref}" — read it with: lanes link token show`));
|
|
256
|
+
}
|
|
257
|
+
|