@maxceem/agw 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +17 -2
  2. package/dist/agw.mjs +751 -149
  3. package/package.json +2 -2
package/dist/agw.mjs CHANGED
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // <define:__AGW_MANIFEST__>
9
- var define_AGW_MANIFEST_default = { version: "0.4.0", wrangler: "4.96.0", upgradeFrom: ["0.4.0"], release: { url: "https://github.com/maxceem/app-ai-gateway/releases/download/v0.4.0/gateway-0.4.0.tar.gz", sha256: "82e5cea16db3e01d3058bf966c3ea634adf8565658de1f4cddbe02997b2402d9", manifest: "56646f283de6e8683194e48f70a113f69120a35d724b7e0f1b5e4df92473c0cc" } };
9
+ var define_AGW_MANIFEST_default = { version: "0.5.1", wrangler: "4.96.0", upgradeFrom: ["0.5.1", "0.5.0", "0.4.0"], release: { url: "https://github.com/maxceem/app-ai-gateway/releases/download/v0.5.1/gateway-0.5.1.tar.gz", sha256: "f4461618d5882d9b32dc44d5d8db048bbb6d6df5ec9899c77c5877491296d77c", manifest: "f08d792d4b9f8b9537075a99583b39c5a355891a20d20632bb5ed957ad6eacf2" } };
10
10
 
11
11
  // src/main.ts
12
12
  import { Writable as Writable2 } from "node:stream";
@@ -14561,19 +14561,19 @@ function hasUnsafeCharacter(raw) {
14561
14561
  function reject(message) {
14562
14562
  return { ok: false, message };
14563
14563
  }
14564
- function hostProblem(hostname5) {
14565
- if (hostname5.startsWith("[")) {
14564
+ function hostProblem(hostname6) {
14565
+ if (hostname6.startsWith("[")) {
14566
14566
  return "Base URL must name a host, not an IPv6 address";
14567
14567
  }
14568
- if (IPV4_PATTERN.test(hostname5) || NUMERIC_HOST_PATTERN.test(hostname5)) {
14568
+ if (IPV4_PATTERN.test(hostname6) || NUMERIC_HOST_PATTERN.test(hostname6)) {
14569
14569
  return "Base URL must name a host, not an IP address";
14570
14570
  }
14571
- if (hostname5.endsWith(".")) {
14571
+ if (hostname6.endsWith(".")) {
14572
14572
  return "Base URL host must not end with a dot";
14573
14573
  }
14574
- const labels = hostname5.split(".");
14574
+ const labels = hostname6.split(".");
14575
14575
  if (labels.length < 2) {
14576
- return `Base URL host must be a public domain name; ${hostname5} has no domain suffix`;
14576
+ return `Base URL host must be a public domain name; ${hostname6} has no domain suffix`;
14577
14577
  }
14578
14578
  for (const label of labels) {
14579
14579
  if (!LABEL_PATTERN.test(label)) {
@@ -14823,7 +14823,7 @@ var PROVIDER_DESCRIPTORS = {
14823
14823
  // reads the older field.
14824
14824
  chatCompletionsCapField: "max_completion_tokens",
14825
14825
  // The two types whose Responses and transcription request shapes the
14826
- // gateway composes itself for named endpoints.
14826
+ // gateway composes itself for custom endpoints.
14827
14827
  endpointPaths: { responses: "v1/responses", audio_transcription: "v1/audio/transcriptions" },
14828
14828
  modelAuthor: "OpenAI"
14829
14829
  },
@@ -15512,6 +15512,7 @@ var RELEASE = manifest.release ?? null;
15512
15512
 
15513
15513
  // src/common.ts
15514
15514
  var CLOUD = "https://api.appaigateway.com";
15515
+ var MANAGEMENT_KEY_ENV = "AGW_MANAGEMENT_KEY";
15515
15516
  var CliError = class extends Error {
15516
15517
  code;
15517
15518
  nextAction;
@@ -15657,7 +15658,7 @@ async function confirm(message, flags) {
15657
15658
  // src/parser.ts
15658
15659
  var secret2 = "key-prompt! key-stdin! browser! no-open!";
15659
15660
  var browser = "no-open!";
15660
- var managementKey = "key-prompt! key-stdin!";
15661
+ var managementKey = "key-prompt! key-stdin! no-open!";
15661
15662
  var common = "json! no-input!";
15662
15663
  var commands = {
15663
15664
  "deployment setup": {
@@ -15815,7 +15816,7 @@ function parse3(argv) {
15815
15816
  }
15816
15817
  if (flags["key-prompt"] && flags["no-input"])
15817
15818
  fail("conflicting_flags", "--key-prompt cannot be used with --no-input.");
15818
- if (flags["no-open"] && !flags.browser && command !== "account claim")
15819
+ if (flags["no-open"] && !flags.browser && command !== "account claim" && command !== "account login" && command !== "deployment connect")
15819
15820
  fail("invalid_input", "--no-open requires --browser.");
15820
15821
  if (flags["no-open"] && (flags["key-prompt"] || flags["key-stdin"]))
15821
15822
  fail(
@@ -15841,8 +15842,10 @@ ${matches.map(
15841
15842
  )}
15842
15843
 
15843
15844
  --json: structured stdout; --no-input: never prompt; --help, -h: this help.
15845
+ Login: account login and deployment connect open the browser; check that it shows the pairing code the terminal prints. --no-open prints the URL instead; --key-stdin or --key-prompt takes a management key.
15846
+ Credentials: ${MANAGEMENT_KEY_ENV}, when set, is the management key for every command and overrides the saved login. account logout revokes the saved key.
15844
15847
  Secrets: hidden prompt by default; --key-stdin reads one line. Provider --browser submits credentials directly to your gateway.
15845
- Browser operations: --no-open, --json or --no-input returns pending; operation wait resumes.
15848
+ Browser operations: --json or --no-input returns pending; operation wait resumes.
15846
15849
  No account is created by help, status, list, validation or dry runs.
15847
15850
  `;
15848
15851
  }
@@ -16985,7 +16988,7 @@ function attestEnvironments(value) {
16985
16988
  return value.split(",").map((name) => name.trim()).filter((name) => name.length > 0);
16986
16989
  }
16987
16990
  async function remoteValidation(ctx, doc, id) {
16988
- if (!ctx.active?.credential)
16991
+ if (!ctx.managementKey)
16989
16992
  return {
16990
16993
  local: true,
16991
16994
  remote: false,
@@ -17143,7 +17146,7 @@ ${snippet}`;
17143
17146
  if (flags.endpoint) {
17144
17147
  const endpoint = doc.config.endpoints[flags.endpoint];
17145
17148
  if (!endpoint)
17146
- fail("endpoint_not_found", "Choose an existing named endpoint.");
17149
+ fail("endpoint_not_found", "Choose an existing custom endpoint.");
17147
17150
  const responses = endpoint.api_style === "responses";
17148
17151
  example = {
17149
17152
  target: { endpoint: flags.endpoint },
@@ -17601,7 +17604,14 @@ var ManagementKeySummarySchema = external_exports.object({
17601
17604
  }),
17602
17605
  expiresAt: external_exports.string().nullable(),
17603
17606
  createdAt: external_exports.string(),
17604
- revokedAt: external_exports.string().nullable()
17607
+ revokedAt: external_exports.string().nullable(),
17608
+ source: external_exports.string().meta({
17609
+ description: "Where the key was issued: `console`, `cli` for a key a CLI login received, or `bootstrap` for the key a CLI account starts with. Display only.",
17610
+ example: "cli"
17611
+ }),
17612
+ label: external_exports.string().nullable().meta({
17613
+ description: "Who holds it, as the issuing client described itself, e.g. `CLI on mac-studio`. Display only."
17614
+ })
17605
17615
  });
17606
17616
  var ManagementKeyListResponseSchema = external_exports.object({
17607
17617
  keys: external_exports.array(ManagementKeySummarySchema)
@@ -17635,7 +17645,7 @@ var ProviderSummarySchema = external_exports.object({
17635
17645
  description: "The client APIs this instance can be called with on its route. A direct instance lists every style its provider type does not narrow; the provider itself answers for paths it lacks."
17636
17646
  }),
17637
17647
  endpointStyles: external_exports.array(external_exports.enum(ENDPOINT_API_STYLES)).meta({
17638
- description: "The named-endpoint styles this instance can back on its route."
17648
+ description: "The custom-endpoint styles this instance can back on its route."
17639
17649
  }),
17640
17650
  modelPrefix: external_exports.string().nullable().meta({
17641
17651
  description: "The namespace the route adds to model IDs on the way out. Clients always send the provider's own ID; null means the route sends it unchanged."
@@ -17931,7 +17941,10 @@ var ModelPriceSchema = external_exports.object({
17931
17941
  long_input: external_exports.number().optional(),
17932
17942
  long_output: external_exports.number().optional(),
17933
17943
  long_cached_input: external_exports.number().optional(),
17934
- long_cache_write: external_exports.number().optional()
17944
+ long_cache_write: external_exports.number().optional(),
17945
+ retirement_date: external_exports.string().optional().meta({
17946
+ description: "The day (YYYY-MM-DD) the provider stops, or stopped, serving this model. The model stays priced after it."
17947
+ })
17935
17948
  });
17936
17949
  var PricesResponseSchema = external_exports.object({
17937
17950
  prices: external_exports.record(external_exports.string(), external_exports.record(external_exports.string(), ModelPriceSchema))
@@ -17940,8 +17953,11 @@ var PricesResponseSchema = external_exports.object({
17940
17953
  // ../src/contracts/cli.ts
17941
17954
  var CliProofSchema = external_exports.string().regex(/^[A-Za-z0-9_-]{32,256}$/);
17942
17955
  var CliBootstrapRequestSchema = external_exports.object({ token: CliProofSchema }).strict();
17956
+ var CliUserCodeSchema = external_exports.string().regex(/^[A-Za-z0-9]{4}-?[A-Za-z0-9]{4}$/);
17957
+ var CliSubmissionTokenSchema = external_exports.string().regex(/^(?:[A-Za-z0-9_-]{32,256}|[A-Za-z0-9]{4}-?[A-Za-z0-9]{4})$/);
17943
17958
  var CliOperationKindSchema = external_exports.enum([
17944
17959
  "bootstrap",
17960
+ "login",
17945
17961
  "claim",
17946
17962
  "app.add",
17947
17963
  "app.key.add",
@@ -17970,15 +17986,16 @@ var CliOperationRequestSchema = external_exports.discriminatedUnion("kind", [
17970
17986
  ),
17971
17987
  operationRequest("provider-gateway.rotate-key", HandoffRotatePayloadSchema, "always")
17972
17988
  ]);
17973
- var CliBrowserProofSchema = external_exports.object({ submissionToken: CliProofSchema }).strict();
17989
+ var CliBrowserProofSchema = external_exports.object({ submissionToken: CliSubmissionTokenSchema }).strict();
17974
17990
  var CliBrowserSubmitRequestSchema = external_exports.object({
17975
- submissionToken: CliProofSchema,
17991
+ submissionToken: CliSubmissionTokenSchema,
17976
17992
  approve: external_exports.literal(true),
17977
17993
  // Flag-free, because the source is published verbatim as an OpenAPI `pattern`.
17978
- secret: external_exports.string().max(16384).regex(/\S/, { error: "must not be blank" }).optional()
17994
+ secret: external_exports.string().max(16384).regex(/\S/, { error: "must not be blank" }).optional(),
17995
+ organizationId: external_exports.string().min(1).max(256).optional()
17979
17996
  }).strict();
17980
17997
  var CliBrowserRegisterRequestSchema = external_exports.object({
17981
- submissionToken: CliProofSchema,
17998
+ submissionToken: CliSubmissionTokenSchema,
17982
17999
  email: external_exports.email(),
17983
18000
  password: external_exports.string().min(8).max(256),
17984
18001
  name: external_exports.string().min(1).max(100)
@@ -17991,35 +18008,89 @@ var CliDeploymentSchema = external_exports.object({
17991
18008
  });
17992
18009
  var CliApprovalRefusalSchema = external_exports.enum([
17993
18010
  "registration_required",
17994
- "sign_out_required"
18011
+ "sign_out_required",
18012
+ /**
18013
+ * A login's approver belongs to no account they could log the CLI in to, and
18014
+ * this deployment does not give a person one of their own. Nothing on the
18015
+ * page can fix that; an owner has to add them to an account first.
18016
+ */
18017
+ "no_eligible_organization"
17995
18018
  ]);
17996
18019
  var CliViewerSchema = external_exports.object({
17997
18020
  name: external_exports.string().nullable(),
17998
18021
  email: external_exports.string().nullable()
17999
18022
  });
18023
+ var CliOperationClientSchema = external_exports.object({
18024
+ /** E.g. `CLI on mac-studio`. */
18025
+ label: external_exports.string().nullable(),
18026
+ os: external_exports.string().nullable(),
18027
+ /** The address the request came from, as the edge saw it. */
18028
+ ip: external_exports.string().nullable(),
18029
+ requestedAt: external_exports.string()
18030
+ });
18031
+ var CliLoginOrganizationSchema = external_exports.object({
18032
+ id: external_exports.string(),
18033
+ name: external_exports.string(),
18034
+ role: external_exports.enum(["owner", "admin", "member"])
18035
+ });
18000
18036
  var CliBrowserDetailsResponseSchema = external_exports.object({
18001
18037
  kind: CliOperationKindSchema,
18002
18038
  payload: external_exports.record(external_exports.string(), external_exports.unknown()),
18003
- account: OrganizationSummarySchema,
18039
+ /** Null only for a login, which lands in whichever account its approver picks. */
18040
+ account: OrganizationSummarySchema.nullable(),
18004
18041
  viewer: CliViewerSchema.nullable(),
18005
18042
  /**
18006
18043
  * What stands between this browser and the Approve button, or null when
18007
- * nothing does — which is every non-claim kind, since only a claim asks who
18008
- * is holding the browser. One field decides the whole screen: each value
18044
+ * nothing does — which is every kind but a claim and a login, since only
18045
+ * those two ask who is holding the browser. One field decides the whole screen: each value
18009
18046
  * names the single thing the page may offer, so the page never has to
18010
18047
  * consult the handoff kind to know what to put in front of a person.
18011
18048
  */
18012
18049
  blockedBy: CliApprovalRefusalSchema.nullable(),
18013
18050
  googleEnabled: external_exports.boolean(),
18014
- expiresAt: external_exports.string()
18051
+ expiresAt: external_exports.string(),
18052
+ /**
18053
+ * The code the terminal shows, however the page was reached, so a person
18054
+ * can check it against their terminal before approving. Null for a kind
18055
+ * that has none — every kind but a login.
18056
+ */
18057
+ userCode: external_exports.string().nullable(),
18058
+ /** Who asked, for a login; null for every other kind. */
18059
+ client: CliOperationClientSchema.nullable(),
18060
+ /** Whether approving hands the browser back to the CLI's local listener (`redirectUrl`). */
18061
+ hasLoopbackRedirect: external_exports.boolean(),
18062
+ /**
18063
+ * For a login only: the accounts the signed-in person may log the CLI in to,
18064
+ * so the page can ask which one when there is more than one.
18065
+ */
18066
+ organizations: external_exports.array(CliLoginOrganizationSchema).optional()
18015
18067
  });
18016
18068
  var CliHandoffContinuationSchema = external_exports.enum(["cli", "console"]);
18017
18069
  var CliBrowserSubmitResponseSchema = external_exports.object({
18018
18070
  state: external_exports.literal("completed"),
18019
18071
  /** The whole of what the page says once it is approved. */
18020
18072
  message: external_exports.string(),
18021
- continueTo: CliHandoffContinuationSchema
18073
+ continueTo: CliHandoffContinuationSchema,
18074
+ /**
18075
+ * Set when the CLI registered a local listener: the page navigates here, and
18076
+ * the one-time code in it is what the CLI redeems for its credential.
18077
+ */
18078
+ redirectUrl: external_exports.url().optional()
18022
18079
  });
18080
+ var CliBrowserDenyResponseSchema = external_exports.object({
18081
+ state: external_exports.literal("denied"),
18082
+ message: external_exports.string()
18083
+ });
18084
+ var CliBrowserLookupRequestSchema = external_exports.object({ userCode: external_exports.string().min(1).max(32) }).strict();
18085
+ var CliBrowserLookupResponseSchema = external_exports.discriminatedUnion("found", [
18086
+ external_exports.object({ found: external_exports.literal(false) }),
18087
+ external_exports.object({
18088
+ found: external_exports.literal(true),
18089
+ id: external_exports.string(),
18090
+ kind: CliOperationKindSchema,
18091
+ expiresAt: external_exports.string()
18092
+ })
18093
+ ]);
18023
18094
  var CliBrowserRegisterResponseSchema = external_exports.object({
18024
18095
  token: external_exports.string().nullable().optional(),
18025
18096
  redirect: external_exports.boolean().optional()
@@ -18031,16 +18102,43 @@ var CliBrowserGoogleResponseSchema = external_exports.object({
18031
18102
  var CliCredentialSchema = external_exports.object({
18032
18103
  token: external_exports.string()
18033
18104
  });
18105
+ var CliLoginRequestSchema = external_exports.object({
18106
+ token: CliProofSchema,
18107
+ client: external_exports.object({
18108
+ label: external_exports.string().trim().min(1).max(200),
18109
+ os: external_exports.string().trim().min(1).max(64).optional()
18110
+ }).strict(),
18111
+ // Ports 1–65535, spelled out because the source is published as an
18112
+ // OpenAPI `pattern` and has no range syntax.
18113
+ loopbackRedirect: external_exports.string().regex(
18114
+ /^http:\/\/127\.0\.0\.1:(?:[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])\/callback$/
18115
+ ).optional()
18116
+ }).strict();
18117
+ var CliLoginSchema = external_exports.object({
18118
+ id: external_exports.string(),
18119
+ kind: external_exports.literal("login"),
18120
+ /** `completed` when a login already approved is sent again: poll it, or redeem it, for the key. */
18121
+ state: external_exports.enum(["pending", "completed", "denied", "expired"]),
18122
+ url: external_exports.url().nullable(),
18123
+ userCode: external_exports.string().nullable(),
18124
+ expiresAt: external_exports.string()
18125
+ });
18126
+ var CliLoginRedeemRequestSchema = external_exports.object({ redeemCode: CliProofSchema }).strict();
18127
+ var CliLoginRedeemResponseSchema = external_exports.object({
18128
+ credential: CliCredentialSchema,
18129
+ account: OrganizationSummarySchema
18130
+ });
18131
+ var CliCredentialRevokeResponseSchema = external_exports.object({ revoked: external_exports.literal(true) });
18034
18132
  var UnclaimedAccessSchema = external_exports.object({ endsAt: external_exports.string(), limit: external_exports.number().int().optional() }).nullable();
18035
18133
  var CliOperationResultSchema = external_exports.object({
18036
- /** A bootstrap's management key. Never print or log it. */
18134
+ /** A bootstrap's or a login's management key. Never print or log it. */
18037
18135
  credential: CliCredentialSchema.optional(),
18038
18136
  /**
18039
18137
  * On a hosted deployment, when a bootstrapped account's free access ends and
18040
18138
  * the request allowance it has until then; null on a self-host.
18041
18139
  */
18042
18140
  unclaimedAccess: UnclaimedAccessSchema.optional(),
18043
- /** The account a claim acted on. */
18141
+ /** The account a claim acted on, or a login landed in. */
18044
18142
  accountId: external_exports.string().optional(),
18045
18143
  app: AppResponseSchema.shape.app.optional(),
18046
18144
  /**
@@ -18055,6 +18153,8 @@ var CliOperationSchema = external_exports.object({
18055
18153
  id: external_exports.string(),
18056
18154
  kind: CliOperationKindSchema,
18057
18155
  state: external_exports.enum(["pending", "completed", "expired"]),
18156
+ /** Set beside `state: "expired"` when a person declined the browser step rather than letting it lapse. */
18157
+ denied: external_exports.literal(true).optional(),
18058
18158
  expiresAt: external_exports.string(),
18059
18159
  url: external_exports.url().optional(),
18060
18160
  deployment: CliDeploymentSchema,
@@ -18092,7 +18192,12 @@ var CliCapabilitiesResponseSchema = external_exports.object({
18092
18192
  endpointStyles: external_exports.array(external_exports.string())
18093
18193
  })
18094
18194
  ),
18095
- providerGateways: external_exports.array(external_exports.object({ type: external_exports.string(), name: external_exports.string() }))
18195
+ providerGateways: external_exports.array(external_exports.object({ type: external_exports.string(), name: external_exports.string() })),
18196
+ /** What this deployment supports beyond protocol 1's baseline. Absent on older deployments. */
18197
+ features: external_exports.object({
18198
+ /** `POST /v1/cli/login`: a person approves a CLI in a browser and it receives a key of its own. */
18199
+ browserLogin: external_exports.boolean()
18200
+ }).optional()
18096
18201
  });
18097
18202
  var [SelfHostedAccessSchema, UnavailableAccessSchema, BilledAccessSchema] = GatewayBillingAccessSchema.options;
18098
18203
  var CliBillingAccessSchema = external_exports.discriminatedUnion("state", [
@@ -18131,7 +18236,7 @@ var CLI_ERRORS = {
18131
18236
  410: "The protected credential exchange has expired; no replacement is minted.",
18132
18237
  429: "The durable initiation or submission rate limit was reached."
18133
18238
  };
18134
- var BROWSER_HANDOFF_DESCRIPTION = "First-party browser only: both the request URL origin and exact Origin header must match consoleOrigin; a separate submissionToken is required. Identity approval also requires an interactive human session; registration is limited to a valid pending claim. Provider secret values are write-only.";
18239
+ var BROWSER_HANDOFF_DESCRIPTION = "First-party browser only: both the request URL origin and exact Origin header must match consoleOrigin; a separate submissionToken is required \u2014 the proof from the approval URL's fragment, or, for a login, the user code the terminal shows. Identity approval (a claim or a login) also requires an interactive human session; registration is limited to a valid pending claim or login. Provider secret values are write-only.";
18135
18240
  var SignUpRequestSchema = external_exports.object({
18136
18241
  name: external_exports.string(),
18137
18242
  email: external_exports.email(),
@@ -18323,8 +18428,8 @@ var CATALOG = {
18323
18428
  callNamedEndpoint: {
18324
18429
  method: "POST",
18325
18430
  path: "/v1/apps/{app}/endpoints/{slug}",
18326
- tags: ["Named endpoints"],
18327
- summary: "Call a server-configured named endpoint",
18431
+ tags: ["Custom endpoints"],
18432
+ summary: "Call a server-configured custom endpoint",
18328
18433
  description: "The endpoint's provider, model, fixed parameters, output cap, and fallback chain come from the application configuration, so you can change models without shipping a client release. Responses-style endpoints accept an OpenAI Responses body; audio_transcription endpoints accept an OpenAI audio transcription multipart body and may omit the model field. The successful response keeps the serving provider's native format and streaming behaviour.",
18329
18434
  security: "gateway",
18330
18435
  params: {
@@ -18954,12 +19059,51 @@ var CATALOG = {
18954
19059
  responseDescription: "Where the operation stands: completed with its result, or pending with the approval URL.",
18955
19060
  errors: CLI_ERRORS
18956
19061
  },
19062
+ openCliLogin: {
19063
+ method: "POST",
19064
+ path: "/v1/cli/login",
19065
+ tags: ["CLI"],
19066
+ summary: "Ask a person to log this CLI in",
19067
+ description: "Persist the random token before sending: its digest is the operation's id, and sending it again answers with where the login stands \u2014 `completed` once approved, when polling or redeeming collects the key \u2014 and never hands the key over itself. Needs no credential. Open `url` in a browser, or show `userCode` for the person to type on the console's code-entry page; once a signed-in person approves, the CLI receives a management key of its own in the account they chose. Without `loopbackRedirect`, poll `pollCliOperation` until it completes: the first completed answer carries the key in `result.credential`, and no later one does. With `loopbackRedirect`, the approval page sends the browser there with a one-time `code`, which `redeemCliLogin` exchanges for the key; polling then never returns it. A login expires after 15 minutes, and its record is kept for a day. Each network address may ask for 20 logins an hour \u2014 sending the same token again is not counted \u2014 and may have 10 waiting at once.",
19068
+ security: "public",
19069
+ request: CliLoginRequestSchema,
19070
+ response: CliLoginSchema,
19071
+ responseDescription: "The pending login, its approval URL and its user code.",
19072
+ errors: CLI_ERRORS
19073
+ },
19074
+ redeemCliLogin: {
19075
+ method: "POST",
19076
+ path: "/v1/cli/login/{id}/redeem",
19077
+ tags: ["CLI"],
19078
+ summary: "Exchange a login's redeem code for its credential",
19079
+ description: "For a login opened with `loopbackRedirect`: send the operation token as the bearer credential and the `code` the approval page delivered to the local listener. Answers once; a second redemption is refused with `already_completed`. Never print or log the credential.",
19080
+ security: "cliPoll",
19081
+ params: { id: {} },
19082
+ request: CliLoginRedeemRequestSchema,
19083
+ response: CliLoginRedeemResponseSchema,
19084
+ responseDescription: "The management key the login issued, and the account it belongs to.",
19085
+ errors: CLI_ERRORS
19086
+ },
19087
+ revokeCliCredential: {
19088
+ method: "DELETE",
19089
+ path: "/v1/cli/credential",
19090
+ tags: ["CLI"],
19091
+ summary: "Revoke the management key this request is sent with",
19092
+ description: "Logging a CLI out. Any management key may end itself, whatever its role and whatever the account's standing; a browser session cannot use this and signs out instead.",
19093
+ security: "management",
19094
+ // Holding a key is authority enough to end it: a member may, and an
19095
+ // account past its free window may.
19096
+ policy: { role: "member", access: "read" },
19097
+ response: CliCredentialRevokeResponseSchema,
19098
+ responseDescription: "The key is revoked; the next request made with it is refused.",
19099
+ errors: CLI_ERRORS
19100
+ },
18957
19101
  pollCliOperation: {
18958
19102
  method: "GET",
18959
19103
  path: "/v1/cli/operations/{id}",
18960
19104
  tags: ["CLI"],
18961
19105
  summary: "Poll a CLI operation",
18962
- description: "Only the operation's token can read it. Completed claims report the account they landed on; the CLI keeps the access it already had. Provider secrets are never returned; a one-time key an operation created is, for 15 minutes.",
19106
+ description: "Only the operation's token can read it. Completed claims report the account they landed on; the CLI keeps the access it already had. Provider secrets are never returned; a one-time key an operation created is, for 15 minutes. A login's key is returned once, by the first completed answer, and never when the login registered a loopback redirect. A step a person declined answers `expired` with `denied: true`.",
18963
19107
  security: "cliPoll",
18964
19108
  params: { id: {} },
18965
19109
  response: CliOperationSchema,
@@ -19020,7 +19164,32 @@ var CATALOG = {
19020
19164
  params: { id: {} },
19021
19165
  request: CliBrowserSubmitRequestSchema,
19022
19166
  response: CliBrowserSubmitResponseSchema,
19023
- responseDescription: "The handoff is approved and consumed. No submitted secret is ever echoed.",
19167
+ responseDescription: "The handoff is approved and consumed. No submitted secret is ever echoed. For a login with a loopback redirect, `redirectUrl` is where to send the browser next.",
19168
+ errors: CLI_ERRORS
19169
+ },
19170
+ cliBrowserDeny: {
19171
+ method: "POST",
19172
+ path: "/v1/cli/browser/{id}/deny",
19173
+ tags: ["CLI"],
19174
+ summary: "Browser handoff: deny",
19175
+ description: `${BROWSER_HANDOFF_DESCRIPTION} Anyone holding the link or the code may decline, signed in or not.`,
19176
+ security: "public",
19177
+ params: { id: {} },
19178
+ request: CliBrowserProofSchema,
19179
+ response: CliBrowserDenyResponseSchema,
19180
+ responseDescription: "The handoff is declined; the CLI's next poll reports it. Repeating a denial answers the same.",
19181
+ errors: CLI_ERRORS
19182
+ },
19183
+ cliBrowserLookup: {
19184
+ method: "POST",
19185
+ path: "/v1/cli/browser/lookup",
19186
+ tags: ["CLI"],
19187
+ summary: "Browser handoff: find a step by its user code",
19188
+ description: "First-party browser only, like the other handoff steps. Case and dashes are ignored. Continue with the code as `submissionToken` on the step it names. Rate limited per network address, because a code is short enough to guess in bulk.",
19189
+ security: "public",
19190
+ request: CliBrowserLookupRequestSchema,
19191
+ response: CliBrowserLookupResponseSchema,
19192
+ responseDescription: "Whether a pending step answers to the code, and which one.",
19024
19193
  errors: CLI_ERRORS
19025
19194
  },
19026
19195
  cliBrowserRegister: {
@@ -19033,7 +19202,7 @@ var CATALOG = {
19033
19202
  params: { id: {} },
19034
19203
  request: CliBrowserRegisterRequestSchema,
19035
19204
  response: CliBrowserRegisterResponseSchema,
19036
- responseDescription: "A new human identity for a pending claim, with its session set as a cookie.",
19205
+ responseDescription: "A new human identity for a pending claim or login, with its session set as a cookie.",
19037
19206
  errors: CLI_ERRORS
19038
19207
  },
19039
19208
  cliBrowserGoogle: {
@@ -19046,7 +19215,7 @@ var CATALOG = {
19046
19215
  params: { id: {} },
19047
19216
  request: CliBrowserProofSchema,
19048
19217
  response: CliBrowserGoogleResponseSchema,
19049
- responseDescription: "Where to send the browser to start Google consent for a pending claim.",
19218
+ responseDescription: "Where to send the browser to start Google consent for a pending claim or login.",
19050
19219
  errors: CLI_ERRORS
19051
19220
  }
19052
19221
  };
@@ -19068,6 +19237,167 @@ function operationPath(name, ...rest) {
19068
19237
  return query === void 0 ? filled : filled + searchSuffix(query);
19069
19238
  }
19070
19239
 
19240
+ // src/login.ts
19241
+ import { createServer } from "node:http";
19242
+ import { hostname as hostname4, platform as platform2, release } from "node:os";
19243
+ var REDEEM_CODE_PARAMETER = "code";
19244
+ var PAGE_HEADERS = {
19245
+ "content-type": "text/html; charset=utf-8",
19246
+ "cache-control": "no-store",
19247
+ "referrer-policy": "no-referrer",
19248
+ "x-content-type-options": "nosniff",
19249
+ connection: "close"
19250
+ };
19251
+ function page(title, text2) {
19252
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title></head><body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;line-height:1.5"><h1 style="font-size:1.4rem">${title}</h1><p>${text2}</p></body></html>`;
19253
+ }
19254
+ var CONNECTED_PAGE = page(
19255
+ "Your terminal is connected",
19256
+ "The CLI has its key. You can close this tab."
19257
+ );
19258
+ var FAILED_PAGE = page(
19259
+ "The login did not complete",
19260
+ "Return to your terminal to see what happened."
19261
+ );
19262
+ async function listenLoopback() {
19263
+ let deliver;
19264
+ const code = new Promise((resolve3) => {
19265
+ deliver = resolve3;
19266
+ });
19267
+ let waiting = null;
19268
+ let delivered = false;
19269
+ const server = createServer((request, response) => {
19270
+ const url2 = new URL(request.url ?? "/", "http://127.0.0.1");
19271
+ if (request.method !== "GET" || url2.pathname !== "/callback") {
19272
+ response.writeHead(404, PAGE_HEADERS).end(page("Not found", "Nothing is served here."));
19273
+ return;
19274
+ }
19275
+ const value = url2.searchParams.get(REDEEM_CODE_PARAMETER) ?? "";
19276
+ if (delivered || !CliProofSchema.safeParse(value).success) {
19277
+ response.writeHead(400, PAGE_HEADERS).end(FAILED_PAGE);
19278
+ return;
19279
+ }
19280
+ delivered = true;
19281
+ waiting = response;
19282
+ deliver(value);
19283
+ });
19284
+ await new Promise((resolve3, reject2) => {
19285
+ server.once("error", reject2);
19286
+ server.listen(0, "127.0.0.1", () => {
19287
+ server.off("error", reject2);
19288
+ resolve3();
19289
+ });
19290
+ });
19291
+ const { port } = server.address();
19292
+ let answered = Promise.resolve();
19293
+ const loopback = {
19294
+ redirect: `http://127.0.0.1:${port}/callback`,
19295
+ code,
19296
+ answer(connected) {
19297
+ const response = waiting;
19298
+ if (!response) return;
19299
+ waiting = null;
19300
+ answered = new Promise((resolve3) => {
19301
+ response.once("finish", resolve3);
19302
+ response.once("close", resolve3);
19303
+ });
19304
+ response.writeHead(200, PAGE_HEADERS).end(connected ? CONNECTED_PAGE : FAILED_PAGE);
19305
+ },
19306
+ async close() {
19307
+ loopback.answer(false);
19308
+ await answered;
19309
+ await new Promise((resolve3) => {
19310
+ const force = setTimeout(() => server.closeAllConnections(), 1e3);
19311
+ force.unref();
19312
+ server.close(() => {
19313
+ clearTimeout(force);
19314
+ resolve3();
19315
+ });
19316
+ server.closeIdleConnections();
19317
+ });
19318
+ }
19319
+ };
19320
+ return loopback;
19321
+ }
19322
+ function clientDescription() {
19323
+ return {
19324
+ label: `CLI on ${hostname4() || "this computer"}`.slice(0, 200),
19325
+ os: `${platform2()} ${release()}`.trim().slice(0, 64)
19326
+ };
19327
+ }
19328
+ function browserAvailable({
19329
+ env,
19330
+ platform: system,
19331
+ terminal
19332
+ } = {
19333
+ env: process.env,
19334
+ platform: platform2(),
19335
+ terminal: Boolean(process.stdin.isTTY && process.stderr.isTTY)
19336
+ }) {
19337
+ if (!terminal) return false;
19338
+ if (env["SSH_CONNECTION"] || env["SSH_TTY"]) return false;
19339
+ if (system === "linux" && !env["DISPLAY"] && !env["WAYLAND_DISPLAY"]) return false;
19340
+ return true;
19341
+ }
19342
+
19343
+ // src/style.ts
19344
+ import { Writable } from "node:stream";
19345
+ import { styleText } from "node:util";
19346
+ var STATES = /* @__PURE__ */ new Map([
19347
+ ["active", "green"],
19348
+ ["ready", "green"],
19349
+ ["completed", "green"],
19350
+ ["yes", "green"],
19351
+ ["pending", "yellow"],
19352
+ ["failed", "red"],
19353
+ ["expired", "red"],
19354
+ ["blocked", "red"],
19355
+ ["inactive", "red"],
19356
+ ["disabled", "red"],
19357
+ ["revoked", "red"],
19358
+ ["no", "red"]
19359
+ ]);
19360
+ function colorEnabled(sink, env = process.env) {
19361
+ if (env["NO_COLOR"]) return false;
19362
+ if (!(sink instanceof Writable)) return false;
19363
+ if (env["FORCE_COLOR"] && env["FORCE_COLOR"] !== "0") return true;
19364
+ if (!("isTTY" in sink && sink.isTTY === true)) return false;
19365
+ return styleText("dim", "?", { stream: sink, validateStream: true }) !== "?";
19366
+ }
19367
+ var plain = style(false);
19368
+ var styleFor = (sink) => style(colorEnabled(sink));
19369
+ function style(enabled) {
19370
+ const paint = (format, text2) => enabled ? styleText(format, text2, { validateStream: false }) : text2;
19371
+ const self = {
19372
+ enabled,
19373
+ headline: (text2) => paint("bold", text2),
19374
+ dim: (text2) => paint("dim", text2),
19375
+ alert: (text2) => paint("red", text2),
19376
+ warn: (text2) => paint("yellow", text2),
19377
+ state: (value) => {
19378
+ const tone = STATES.get(value.toLowerCase());
19379
+ return tone ? paint(tone, value) : value;
19380
+ },
19381
+ code: (text2, language) => {
19382
+ const rule = `\u2500\u2500 ${language} \u2500\u2500`;
19383
+ return [
19384
+ self.dim(rule),
19385
+ ...text2.split("\n").map((line) => self.dim(line)),
19386
+ self.dim("\u2500".repeat(rule.length))
19387
+ ];
19388
+ },
19389
+ json: (value) => JSON.stringify(value, null, 2).split("\n").map((line) => jsonLine(line, paint)).join("\n")
19390
+ };
19391
+ return self;
19392
+ }
19393
+ function jsonLine(line, paint) {
19394
+ const match = /^(\s*)(?:("(?:[^"\\]|\\.)*")(: ))?(.*?)(,?)$/.exec(line);
19395
+ if (!match) return line;
19396
+ const [, indent = "", key, colon, value = "", comma = ""] = match;
19397
+ const painted = value === "" ? "" : /^[[\]{}]+$/.test(value) ? paint("dim", value) : value.startsWith('"') ? value : paint("green", value);
19398
+ return indent + (key && colon ? paint("bold", key) + paint("dim", colon) : "") + painted + (comma ? paint("dim", comma) : "");
19399
+ }
19400
+
19071
19401
  // src/context.ts
19072
19402
  var CLI_OPERATIONS = /* @__PURE__ */ new Set([
19073
19403
  "getCliCapabilities",
@@ -19075,8 +19405,21 @@ var CLI_OPERATIONS = /* @__PURE__ */ new Set([
19075
19405
  "createCliOperation",
19076
19406
  "pollCliOperation",
19077
19407
  "getCliAccount",
19078
- "getCliUsage"
19408
+ "getCliUsage",
19409
+ "openCliLogin",
19410
+ "redeemCliLogin",
19411
+ "revokeCliCredential"
19079
19412
  ]);
19413
+ var LOGIN_TIMEOUT_SECONDS = 300;
19414
+ var LOGIN_RECORD_MS = 36e5;
19415
+ var LOOPBACK_GRACE_MS = 1e4;
19416
+ var defaultIo = () => ({
19417
+ stderr: process.stderr,
19418
+ openBrowser,
19419
+ browserAvailable: () => browserAvailable(),
19420
+ listen: listenLoopback,
19421
+ env: process.env
19422
+ });
19080
19423
  var KEY_OPERATION_KINDS = /* @__PURE__ */ new Set(["app.add", "app.key.add"]);
19081
19424
  var OPERATION_RETENTION_MS = 90 * 864e5;
19082
19425
  var pathFor = operationPath;
@@ -19106,14 +19449,16 @@ var Context = class {
19106
19449
  state;
19107
19450
  transport;
19108
19451
  flags;
19452
+ io;
19109
19453
  /** Operations whose result this run has printed, released once stdout has taken it. */
19110
19454
  delivered = /* @__PURE__ */ new Set();
19111
19455
  onboarding;
19112
- constructor(store, state, transport, flags) {
19456
+ constructor(store, state, transport, flags, io = {}) {
19113
19457
  this.store = store;
19114
19458
  this.state = state;
19115
19459
  this.transport = transport;
19116
19460
  this.flags = flags;
19461
+ this.io = { ...defaultIo(), ...io };
19117
19462
  }
19118
19463
  get active() {
19119
19464
  return this.state.active;
@@ -19121,6 +19466,28 @@ var Context = class {
19121
19466
  get url() {
19122
19467
  return this.active?.url || CLOUD;
19123
19468
  }
19469
+ /** `AGW_MANAGEMENT_KEY`, when it is set to anything. */
19470
+ get environmentKey() {
19471
+ const value = this.io.env[MANAGEMENT_KEY_ENV]?.trim();
19472
+ return value ? value : void 0;
19473
+ }
19474
+ /**
19475
+ * The management credential every call is sent with: the environment's, which
19476
+ * always wins, or the one the selected connection stored.
19477
+ */
19478
+ get managementKey() {
19479
+ return this.environmentKey ?? this.active?.credential;
19480
+ }
19481
+ /** Refuses a command that would store a key the environment would then override. */
19482
+ assertNoEnvironmentKey() {
19483
+ if (this.environmentKey)
19484
+ fail(
19485
+ "environment_credential",
19486
+ `${MANAGEMENT_KEY_ENV} is set, so it is the credential for every command and a stored login would never be used.`,
19487
+ `Unset ${MANAGEMENT_KEY_ENV} to log in, or keep using it as is.`,
19488
+ 4
19489
+ );
19490
+ }
19124
19491
  async save() {
19125
19492
  await this.store.write(this.state);
19126
19493
  }
@@ -19158,7 +19525,20 @@ var Context = class {
19158
19525
  }
19159
19526
  /** The same call, with the connection's management credential attached. */
19160
19527
  async call(name, options = {}) {
19161
- return this.publicCall(name, { ...options, key: options.key ?? this.credential(name) });
19528
+ const key = options.key ?? this.credential(name);
19529
+ try {
19530
+ return await this.publicCall(name, { ...options, key });
19531
+ } catch (error51) {
19532
+ if (error51 instanceof CliError && error51.details?.status === 401 && options.key === void 0 && this.environmentKey)
19533
+ throw new CliError(
19534
+ error51.code,
19535
+ error51.message,
19536
+ `Check ${MANAGEMENT_KEY_ENV}: the deployment refused the key it holds.`,
19537
+ error51.exitCode,
19538
+ error51.details
19539
+ );
19540
+ throw error51;
19541
+ }
19162
19542
  }
19163
19543
  /**
19164
19544
  * The current management credential, or the refusal that names the way back.
@@ -19166,12 +19546,12 @@ var Context = class {
19166
19546
  * which is why those get the wider instruction.
19167
19547
  */
19168
19548
  credential(name) {
19169
- const token = this.active?.credential;
19549
+ const token = this.managementKey;
19170
19550
  if (!token)
19171
19551
  fail(
19172
19552
  "login_required",
19173
19553
  "The selected connection is not authenticated.",
19174
- CLI_OPERATIONS.has(name) ? "Run agw account login." : "Run agw account login or agw deployment connect.",
19554
+ CLI_OPERATIONS.has(name) ? `Run agw account login, or set ${MANAGEMENT_KEY_ENV}.` : `Run agw account login or agw deployment connect, or set ${MANAGEMENT_KEY_ENV}.`,
19175
19555
  4
19176
19556
  );
19177
19557
  return token;
@@ -19247,11 +19627,11 @@ var Context = class {
19247
19627
  * to, in which case it answers pending with the URL and the id to resume.
19248
19628
  */
19249
19629
  async operation(kind, payload, { browser: browser2 = false, url: url2 = this.url } = {}) {
19250
- if (!this.active?.credential)
19630
+ if (!this.managementKey)
19251
19631
  fail(
19252
19632
  "login_required",
19253
19633
  "This operation requires account management access.",
19254
- "Run agw account login.",
19634
+ `Run agw account login, or set ${MANAGEMENT_KEY_ENV}.`,
19255
19635
  4
19256
19636
  );
19257
19637
  const target = origin(url2);
@@ -19262,7 +19642,7 @@ var Context = class {
19262
19642
  // which is the correlation the compiler cannot follow through a generic.
19263
19643
  body: { kind, payload, token: record2.token, ...browser2 ? { browser: true } : {} },
19264
19644
  url: target,
19265
- key: this.active?.credential
19645
+ key: this.managementKey
19266
19646
  })).data);
19267
19647
  if (data.state === "expired") {
19268
19648
  await this.forget(id);
@@ -19273,15 +19653,11 @@ var Context = class {
19273
19653
  return data;
19274
19654
  }
19275
19655
  const { data: capabilities } = await this.publicCall("getCliCapabilities", { url: target });
19276
- const browserUrl = new URL(data.url);
19277
- const trustedOrigin = origin(
19278
- capabilities.consoleOrigin ?? capabilities.deployment.consoleOrigin ?? target
19279
- );
19280
- if (browserUrl.origin !== trustedOrigin || browserUrl.username || browserUrl.password)
19281
- fail("invalid_response", "The approval URL is not on this deployment\u2019s trusted console origin.");
19282
- if (!this.flags["no-open"]) await openBrowser(data.url);
19656
+ assertTrustedApprovalUrl(data.url, capabilities, target);
19657
+ if (!this.flags["no-open"] && !await this.io.openBrowser(data.url))
19658
+ this.io.stderr.write("Browser could not open. Use the returned handoff URL.\n");
19283
19659
  if (this.flags["no-open"] || this.flags.json || this.flags["no-input"]) return data;
19284
- process.stderr.write(`Complete the browser step: ${data.url}
19660
+ this.io.stderr.write(`Complete the browser step: ${data.url}
19285
19661
  Operation: ${data.id}
19286
19662
  `);
19287
19663
  return this.wait(data.id, 300);
@@ -19297,8 +19673,13 @@ Operation: ${data.id}
19297
19673
  * Once the file holds the key, the record keeps only its metadata.
19298
19674
  */
19299
19675
  async keyOperation(kind, payload, requestedPath) {
19300
- if (!this.active?.credential)
19301
- fail("login_required", "The selected connection is not authenticated.", "Run agw account login.", 4);
19676
+ if (!this.managementKey)
19677
+ fail(
19678
+ "login_required",
19679
+ "The selected connection is not authenticated.",
19680
+ `Run agw account login, or set ${MANAGEMENT_KEY_ENV}.`,
19681
+ 4
19682
+ );
19302
19683
  const target = origin(this.url);
19303
19684
  const [id, record2] = await this.reserveOperation(kind, payload, target);
19304
19685
  const chosen = requestedPath ? resolve2(requestedPath) : record2.output?.path;
@@ -19331,7 +19712,7 @@ Operation: ${data.id}
19331
19712
  const operation2 = await this.send(id, async () => (await this.publicCall("createCliOperation", {
19332
19713
  body: { kind, payload, token: record2.token },
19333
19714
  url: target,
19334
- key: this.active?.credential
19715
+ key: this.managementKey
19335
19716
  })).data);
19336
19717
  const minted = operation2.result?.api_key;
19337
19718
  const appId = operation2.result?.app?.id ?? payload.app ?? "";
@@ -19375,7 +19756,7 @@ Operation: ${data.id}
19375
19756
  }
19376
19757
  }
19377
19758
  async bootstrap() {
19378
- if (this.active?.credential) return;
19759
+ if (this.managementKey) return;
19379
19760
  if (this.active)
19380
19761
  fail(
19381
19762
  "login_required",
@@ -19421,7 +19802,9 @@ Operation: ${data.id}
19421
19802
  };
19422
19803
  await this.save();
19423
19804
  }
19424
- async login(url2 = this.url) {
19805
+ /** `account login --key-stdin` or `--key-prompt`: a management key a person already holds. */
19806
+ async keyLogin(url2 = this.url) {
19807
+ this.assertNoEnvironmentKey();
19425
19808
  const token = await secret(this.flags, "Management API key");
19426
19809
  if (!token)
19427
19810
  fail("input_required", "A management API key is required.", "Supply it with --key-stdin.");
@@ -19429,6 +19812,239 @@ Operation: ${data.id}
19429
19812
  await this.select(url2, { ...data, credential: { token } });
19430
19813
  return { connected: true, ...data };
19431
19814
  }
19815
+ /**
19816
+ * `deployment connect` while `AGW_MANAGEMENT_KEY` is set: selects the
19817
+ * deployment the key is checked against, and stores no key of its own.
19818
+ */
19819
+ async connectWithEnvironment(url2) {
19820
+ const key = this.environmentKey;
19821
+ const target = origin(url2);
19822
+ const { data } = await this.publicCall("getCliAccount", { key, url: target });
19823
+ this.state.active = {
19824
+ url: target,
19825
+ account: data.account,
19826
+ deployment: data.deployment,
19827
+ authenticated: false
19828
+ };
19829
+ await this.save();
19830
+ this.io.stderr.write(
19831
+ `${MANAGEMENT_KEY_ENV} is set, so it stays the credential for every command; no key was stored.
19832
+ `
19833
+ );
19834
+ return { connected: true, ...data };
19835
+ }
19836
+ /**
19837
+ * `account login`: a person approves this CLI in a browser, and it receives
19838
+ * a management key of its own, stored exactly as a pasted one is.
19839
+ *
19840
+ * The login is recorded like every other operation before it is opened, so
19841
+ * one left waiting — by `--json`, `--no-input`, a timeout — is finished by
19842
+ * `agw operation wait`. When this process is going to wait and a browser on
19843
+ * this machine is in front of the person, it also listens on `127.0.0.1`,
19844
+ * and the approval page hands the browser back to it with a one-time code;
19845
+ * the key is then released only for that code and never to a poll.
19846
+ * Otherwise — `--no-open`, SSH, no display, nobody at the terminal — the
19847
+ * approval may happen on any device, so nothing listens and the key is
19848
+ * polled for. A browser that turns out not to open is the same case, found
19849
+ * late: that login is abandoned for one that is polled.
19850
+ */
19851
+ async browserLogin(url2 = this.url) {
19852
+ this.assertNoEnvironmentKey();
19853
+ const target = origin(url2);
19854
+ const { data: capabilities } = await this.publicCall("getCliCapabilities", { url: target });
19855
+ if (!capabilities.features?.browserLogin)
19856
+ fail(
19857
+ "browser_login_unsupported",
19858
+ "This deployment does not offer browser login.",
19859
+ "Update the deployment, or pipe a management key to agw account login --key-stdin.",
19860
+ 3
19861
+ );
19862
+ const opens = !this.flags["no-open"] && this.io.browserAvailable();
19863
+ const detached = Boolean(this.flags.json || this.flags["no-input"]);
19864
+ if (detached) {
19865
+ const { login } = await this.openLogin(target, capabilities, null);
19866
+ if (opens && !await this.io.openBrowser(login.url))
19867
+ this.io.stderr.write("Browser could not open. Use the returned URL.\n");
19868
+ return login;
19869
+ }
19870
+ let loopback = opens ? await this.io.listen() : null;
19871
+ try {
19872
+ let opened = await this.openLogin(target, capabilities, loopback);
19873
+ if (loopback && !await this.io.openBrowser(opened.login.url)) {
19874
+ await loopback.close();
19875
+ loopback = null;
19876
+ await this.forget(opened.id);
19877
+ this.io.stderr.write(
19878
+ "The browser could not be opened, so the approval will be collected by polling.\n"
19879
+ );
19880
+ opened = await this.openLogin(target, capabilities, null);
19881
+ }
19882
+ const { id, token, login } = opened;
19883
+ const style2 = styleFor(this.io.stderr);
19884
+ this.io.stderr.write(
19885
+ [
19886
+ ...login.userCode ? [
19887
+ `Pairing code: ${style2.headline(login.userCode)}`,
19888
+ "Check that your browser shows this same code before you approve."
19889
+ ] : [],
19890
+ `Approve at: ${login.url}`,
19891
+ ""
19892
+ ].join("\n")
19893
+ );
19894
+ if (loopback) {
19895
+ try {
19896
+ await this.collectLoopbackLogin(id, token, target, capabilities, loopback);
19897
+ } finally {
19898
+ if (this.state.operations[id]) await this.forget(id);
19899
+ }
19900
+ } else await this.wait(id, LOGIN_TIMEOUT_SECONDS);
19901
+ const { data } = await this.call("getCliAccount", { url: target });
19902
+ return { connected: true, ...data };
19903
+ } finally {
19904
+ await loopback?.close();
19905
+ }
19906
+ }
19907
+ /** Records and opens one login, refusing one that is not pending on the trusted console. */
19908
+ async openLogin(target, capabilities, loopback) {
19909
+ const token = randomToken();
19910
+ const id = operationIdFor(token);
19911
+ await this.recordLogin(id, token, target);
19912
+ const { data: login } = await this.send(id, () => this.publicCall("openCliLogin", {
19913
+ body: {
19914
+ token,
19915
+ client: clientDescription(),
19916
+ ...loopback ? { loopbackRedirect: loopback.redirect } : {}
19917
+ },
19918
+ url: target
19919
+ }));
19920
+ if (login.state !== "pending" || !login.url) {
19921
+ await this.forget(id);
19922
+ expired({
19923
+ id,
19924
+ state: "expired",
19925
+ expiresAt: login.expiresAt,
19926
+ ...login.state === "denied" ? { denied: true } : {}
19927
+ });
19928
+ }
19929
+ assertTrustedApprovalUrl(login.url, capabilities, target);
19930
+ return { id, token, login: { ...login, url: login.url } };
19931
+ }
19932
+ /** Writes a login's record before it is opened, and drops logins long since expired. */
19933
+ async recordLogin(id, token, url2) {
19934
+ const now = Date.now();
19935
+ for (const [key, record2] of Object.entries(this.state.operations))
19936
+ if (record2.kind === "login" && now - Date.parse(record2.createdAt) > LOGIN_RECORD_MS)
19937
+ delete this.state.operations[key];
19938
+ this.state.operations[id] = {
19939
+ url: url2,
19940
+ token,
19941
+ kind: "login",
19942
+ // Never matched: every login is a new one, since a listener's port is.
19943
+ requestHash: createHash2("sha256").update(id).digest("hex"),
19944
+ accountId: null,
19945
+ createdAt: new Date(now).toISOString()
19946
+ };
19947
+ await this.save();
19948
+ }
19949
+ /**
19950
+ * Waits for the approval page to send the browser back with the redeem code,
19951
+ * and redeems it. Polls meanwhile, since a declined or expired login never
19952
+ * comes back to the listener.
19953
+ */
19954
+ async collectLoopbackLogin(id, token, url2, capabilities, loopback) {
19955
+ let code;
19956
+ const arrived = loopback.code.then((value) => {
19957
+ code = value;
19958
+ });
19959
+ const end = Date.now() + LOGIN_TIMEOUT_SECONDS * 1e3;
19960
+ let approvedAt;
19961
+ while (code === void 0) {
19962
+ const { data: status2 } = await this.publicCall("pollCliOperation", {
19963
+ params: { id },
19964
+ key: token,
19965
+ url: url2
19966
+ });
19967
+ if (code !== void 0) break;
19968
+ if (status2.state === "expired") {
19969
+ await this.forget(id);
19970
+ expired(status2);
19971
+ }
19972
+ if (status2.state === "completed") {
19973
+ approvedAt ??= Date.now();
19974
+ if (Date.now() - approvedAt > LOOPBACK_GRACE_MS) {
19975
+ await this.forget(id);
19976
+ fail(
19977
+ "login_not_delivered",
19978
+ "The login was approved, but the browser never came back to this terminal.",
19979
+ "Run agw account login --no-open to approve it from another device, or run agw account login again here.",
19980
+ 3,
19981
+ { id }
19982
+ );
19983
+ }
19984
+ }
19985
+ if (Date.now() >= end) {
19986
+ await this.forget(id);
19987
+ fail(
19988
+ "wait_timeout",
19989
+ "The login was not approved in time.",
19990
+ "Run agw account login again.",
19991
+ 5,
19992
+ { id, state: status2.state, expiresAt: status2.expiresAt }
19993
+ );
19994
+ }
19995
+ await Promise.race([delay2(Math.min(1500, Math.max(0, end - Date.now()))), arrived]);
19996
+ }
19997
+ try {
19998
+ const { data } = await this.publicCall("redeemCliLogin", {
19999
+ params: { id },
20000
+ body: { redeemCode: code },
20001
+ key: token,
20002
+ url: url2
20003
+ });
20004
+ await this.select(url2, { ...data, deployment: capabilities.deployment });
20005
+ delete this.state.operations[id];
20006
+ await this.save();
20007
+ loopback.answer(true);
20008
+ } catch (error51) {
20009
+ loopback.answer(false);
20010
+ throw error51;
20011
+ }
20012
+ }
20013
+ /**
20014
+ * `account logout`: the deployment revokes the stored key, then this machine
20015
+ * forgets it. A key the deployment no longer accepts is already as good as
20016
+ * revoked; any other failure still removes it here, and says so, because a
20017
+ * machine must always be able to sign out.
20018
+ */
20019
+ async logout() {
20020
+ const active = this.active;
20021
+ const key = active?.credential;
20022
+ let revoked = false;
20023
+ if (active && key) {
20024
+ try {
20025
+ await this.publicCall("revokeCliCredential", { key, url: active.url });
20026
+ revoked = true;
20027
+ } catch (error51) {
20028
+ const status2 = error51 instanceof CliError ? error51.details?.status : void 0;
20029
+ if (status2 !== 401)
20030
+ this.io.stderr.write(
20031
+ `The key could not be revoked (${error51 instanceof Error ? error51.message : "unknown error"}). It was removed from this machine; revoke it on the console's keys page.
20032
+ `
20033
+ );
20034
+ }
20035
+ }
20036
+ if (active) {
20037
+ delete active.credential;
20038
+ active.authenticated = false;
20039
+ await this.save();
20040
+ }
20041
+ if (this.environmentKey)
20042
+ this.io.stderr.write(
20043
+ `${MANAGEMENT_KEY_ENV} is still set, so it stays the credential for every command; unset it to sign out completely.
20044
+ `
20045
+ );
20046
+ return { loggedOut: true, revoked };
20047
+ }
19432
20048
  async poll(id) {
19433
20049
  const record2 = this.state.operations[id];
19434
20050
  if (!record2)
@@ -19438,7 +20054,7 @@ Operation: ${data.id}
19438
20054
  "Resume it from the machine that initiated the operation.",
19439
20055
  4
19440
20056
  );
19441
- if (this.active && this.active.url !== record2.url)
20057
+ if (this.active && this.active.url !== record2.url && record2.kind !== "login")
19442
20058
  fail(
19443
20059
  "operation_context",
19444
20060
  "This operation belongs to another selected deployment.",
@@ -19458,6 +20074,7 @@ Operation: ${data.id}
19458
20074
  url: record2.url
19459
20075
  });
19460
20076
  if (data.state === "expired") await this.forget(id);
20077
+ if (data.state === "completed" && record2.kind === "login") return this.polledLogin(id, record2, data);
19461
20078
  if (data.state === "completed") {
19462
20079
  if (record2.kind === "claim" && data.account && this.active?.account?.id === record2.accountId && data.account.id === record2.accountId) {
19463
20080
  this.active.account = data.account;
@@ -19467,6 +20084,36 @@ Operation: ${data.id}
19467
20084
  }
19468
20085
  return data;
19469
20086
  }
20087
+ /**
20088
+ * A completed login, as a poll first sees it: its key is stored the way a
20089
+ * pasted one is, and taken out of what the command prints. A login whose key
20090
+ * was already handed to another poll, or is only ever redeemed, cannot be
20091
+ * collected here.
20092
+ */
20093
+ async polledLogin(id, record2, data) {
20094
+ const { credential, ...result } = data.result ?? {};
20095
+ if (credential) {
20096
+ if (!data.account)
20097
+ fail(
20098
+ "invalid_response",
20099
+ "The login did not say which account it was approved into.",
20100
+ "Run agw account login again.",
20101
+ 3
20102
+ );
20103
+ await this.select(record2.url, { credential, account: data.account, deployment: data.deployment });
20104
+ } else if (!(this.active?.credential && this.active.url === record2.url && data.account && this.active.account?.id === data.account.id)) {
20105
+ await this.forget(id);
20106
+ fail(
20107
+ "login_not_delivered",
20108
+ "This login was approved, but its key can no longer be collected here.",
20109
+ "Run agw account login again.",
20110
+ 3,
20111
+ { id }
20112
+ );
20113
+ }
20114
+ this.delivered.add(id);
20115
+ return { ...data, result };
20116
+ }
19470
20117
  async wait(id, timeout) {
19471
20118
  const end = Date.now() + timeout * 1e3;
19472
20119
  for (; ; ) {
@@ -19488,6 +20135,14 @@ Operation: ${data.id}
19488
20135
  }
19489
20136
  };
19490
20137
  function expired(operation2) {
20138
+ if (operation2.denied)
20139
+ fail(
20140
+ "operation_denied",
20141
+ "The browser step was declined.",
20142
+ "Run the same command again to start a new one.",
20143
+ 3,
20144
+ { id: operation2.id, state: "denied", expiresAt: operation2.expiresAt }
20145
+ );
19491
20146
  fail(
19492
20147
  "operation_expired",
19493
20148
  "The browser step expired before it was approved.",
@@ -19517,20 +20172,23 @@ async function storeKey(minted, output, appId) {
19517
20172
  contentHash: createHash2("sha256").update(minted.key + "\n").digest("hex")
19518
20173
  };
19519
20174
  }
20175
+ function assertTrustedApprovalUrl(url2, capabilities, target) {
20176
+ const browserUrl = new URL(url2);
20177
+ const trustedOrigin = origin(
20178
+ capabilities.consoleOrigin ?? capabilities.deployment.consoleOrigin ?? target
20179
+ );
20180
+ if (browserUrl.origin !== trustedOrigin || browserUrl.username || browserUrl.password)
20181
+ fail("invalid_response", "The approval URL is not on this deployment\u2019s trusted console origin.");
20182
+ }
19520
20183
  async function openBrowser(url2) {
19521
20184
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
19522
20185
  const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url2] : [url2];
19523
- await new Promise((resolve3) => {
20186
+ return new Promise((resolve3) => {
19524
20187
  const child = spawn(command, args, { stdio: "ignore", detached: true });
19525
- child.once("error", () => {
19526
- process.stderr.write(
19527
- "Browser could not open. Use the returned handoff URL.\n"
19528
- );
19529
- resolve3();
19530
- });
20188
+ child.once("error", () => resolve3(false));
19531
20189
  child.once("spawn", () => {
19532
20190
  child.unref();
19533
- resolve3();
20191
+ resolve3(true);
19534
20192
  });
19535
20193
  });
19536
20194
  }
@@ -19721,7 +20379,7 @@ async function cachedRelease(version2, integrity, source) {
19721
20379
  }
19722
20380
  return directory;
19723
20381
  }
19724
- async function release(requested = VERSION, base = dirname(fileURLToPath(import.meta.url)), source = {}) {
20382
+ async function release2(requested = VERSION, base = dirname(fileURLToPath(import.meta.url)), source = {}) {
19725
20383
  if (requested !== VERSION)
19726
20384
  fail(
19727
20385
  "unsupported_release",
@@ -19976,14 +20634,14 @@ var Cloudflare = class {
19976
20634
  }
19977
20635
  async all(path) {
19978
20636
  const result = [];
19979
- for (let page = 1; page <= 1e4; page++) {
20637
+ for (let page2 = 1; page2 <= 1e4; page2++) {
19980
20638
  const data = await this.request(
19981
- path + (path.includes("?") ? "&" : "?") + "page=" + page + "&per_page=50"
20639
+ path + (path.includes("?") ? "&" : "?") + "page=" + page2 + "&per_page=50"
19982
20640
  );
19983
20641
  if (!data || !Array.isArray(data.result))
19984
20642
  fail("cloudflare_error", "Expected a Cloudflare resource list.");
19985
20643
  result.push(...data.result);
19986
- if (data.result_info?.total_pages ? page >= data.result_info.total_pages : data.result.length < 50)
20644
+ if (data.result_info?.total_pages ? page2 >= data.result_info.total_pages : data.result.length < 50)
19987
20645
  return result;
19988
20646
  }
19989
20647
  fail("cloudflare_error", "Cloudflare pagination exceeded its safety bound.");
@@ -20304,7 +20962,7 @@ async function matchExisting(ctx, cf, flags, target) {
20304
20962
  }
20305
20963
 
20306
20964
  // src/deployment.ts
20307
- function hostname4(value) {
20965
+ function hostname5(value) {
20308
20966
  const u = origin("https://" + value);
20309
20967
  if (new URL(u).hostname !== value || !value.includes(".") || value.includes(":"))
20310
20968
  fail("invalid_input", "Supply a hostname without protocol, path or port.");
@@ -20317,10 +20975,10 @@ async function authorized(flags, cf, loadRelease) {
20317
20975
  await cf.authenticate(flags);
20318
20976
  return artifact;
20319
20977
  }
20320
- async function deploymentDomain(ctx, flags, cf = new Cloudflare(), loadRelease = release) {
20978
+ async function deploymentDomain(ctx, flags, cf = new Cloudflare(), loadRelease = release2) {
20321
20979
  const artifact = await authorized(flags, cf, loadRelease);
20322
20980
  const journal = await matchExisting(ctx, cf, flags, selectedInstallation(ctx));
20323
- const domain2 = hostname4(flags.hostname ?? await prompt("Custom hostname", flags));
20981
+ const domain2 = hostname5(flags.hostname ?? await prompt("Custom hostname", flags));
20324
20982
  if (!flags["dry-run"])
20325
20983
  await confirm(
20326
20984
  `Attach ${domain2} to ${journal.name} in Cloudflare account ${journal.accountId}?`,
@@ -20328,7 +20986,7 @@ async function deploymentDomain(ctx, flags, cf = new Cloudflare(), loadRelease =
20328
20986
  );
20329
20987
  return attachDomain(ctx, cf, journal, domain2, artifact, Boolean(flags["dry-run"]));
20330
20988
  }
20331
- async function deploymentUpdate(ctx, flags, cf = new Cloudflare(), loadRelease = release) {
20989
+ async function deploymentUpdate(ctx, flags, cf = new Cloudflare(), loadRelease = release2) {
20332
20990
  const artifact = await authorized(flags, cf, loadRelease);
20333
20991
  const journal = await matchExisting(ctx, cf, flags, selectedInstallation(ctx));
20334
20992
  if (!artifact.manifest.upgradeFrom.includes(journal.version))
@@ -20459,12 +21117,12 @@ async function install(ctx, cf, journal, artifact, url2, domain2) {
20459
21117
  await advance(ctx, journal, journal.phase, { pendingDomain: void 0 });
20460
21118
  }
20461
21119
  }
20462
- async function deploymentSetup(ctx, flags, cf = new Cloudflare(), loadRelease = release) {
21120
+ async function deploymentSetup(ctx, flags, cf = new Cloudflare(), loadRelease = release2) {
20463
21121
  const artifact = await authorized(flags, cf, loadRelease);
20464
21122
  const name = flags.name ?? await prompt("Worker name", flags, { defaultValue: "app-ai-gateway" });
20465
21123
  if (!/^[a-z][a-z0-9-]{0,62}$/.test(name))
20466
21124
  fail("invalid_input", "Worker name must be a lowercase slug of at most 63 characters.");
20467
- const domain2 = flags.domain ? hostname4(flags.domain) : void 0;
21125
+ const domain2 = flags.domain ? hostname5(flags.domain) : void 0;
20468
21126
  const accountId = await cf.account(flags);
20469
21127
  const workers = await cf.all("/accounts/" + accountId + "/workers/scripts");
20470
21128
  const installations = ctx.state.installations ??= {};
@@ -20537,7 +21195,7 @@ async function deploymentSetup(ctx, flags, cf = new Cloudflare(), loadRelease =
20537
21195
  throw error51;
20538
21196
  }
20539
21197
  }
20540
- async function deploymentCommand(ctx, command, flags, cf = new Cloudflare(), loadRelease = release) {
21198
+ async function deploymentCommand(ctx, command, flags, cf = new Cloudflare(), loadRelease = release2) {
20541
21199
  if (command === "deployment setup") return deploymentSetup(ctx, flags, cf, loadRelease);
20542
21200
  if (command === "deployment domain") return deploymentDomain(ctx, flags, cf, loadRelease);
20543
21201
  if (command === "deployment update") return deploymentUpdate(ctx, flags, cf, loadRelease);
@@ -20576,64 +21234,6 @@ var CliErrorDetailsSchema = external_exports.object({
20576
21234
  apiErrors: external_exports.string().optional()
20577
21235
  });
20578
21236
 
20579
- // src/style.ts
20580
- import { Writable } from "node:stream";
20581
- import { styleText } from "node:util";
20582
- var STATES = /* @__PURE__ */ new Map([
20583
- ["active", "green"],
20584
- ["ready", "green"],
20585
- ["completed", "green"],
20586
- ["yes", "green"],
20587
- ["pending", "yellow"],
20588
- ["failed", "red"],
20589
- ["expired", "red"],
20590
- ["blocked", "red"],
20591
- ["inactive", "red"],
20592
- ["disabled", "red"],
20593
- ["revoked", "red"],
20594
- ["no", "red"]
20595
- ]);
20596
- function colorEnabled(sink, env = process.env) {
20597
- if (env["NO_COLOR"]) return false;
20598
- if (!(sink instanceof Writable)) return false;
20599
- if (env["FORCE_COLOR"] && env["FORCE_COLOR"] !== "0") return true;
20600
- if (!("isTTY" in sink && sink.isTTY === true)) return false;
20601
- return styleText("dim", "?", { stream: sink, validateStream: true }) !== "?";
20602
- }
20603
- var plain = style(false);
20604
- var styleFor = (sink) => style(colorEnabled(sink));
20605
- function style(enabled) {
20606
- const paint = (format, text2) => enabled ? styleText(format, text2, { validateStream: false }) : text2;
20607
- const self = {
20608
- enabled,
20609
- headline: (text2) => paint("bold", text2),
20610
- dim: (text2) => paint("dim", text2),
20611
- alert: (text2) => paint("red", text2),
20612
- warn: (text2) => paint("yellow", text2),
20613
- state: (value) => {
20614
- const tone = STATES.get(value.toLowerCase());
20615
- return tone ? paint(tone, value) : value;
20616
- },
20617
- code: (text2, language) => {
20618
- const rule = `\u2500\u2500 ${language} \u2500\u2500`;
20619
- return [
20620
- self.dim(rule),
20621
- ...text2.split("\n").map((line) => self.dim(line)),
20622
- self.dim("\u2500".repeat(rule.length))
20623
- ];
20624
- },
20625
- json: (value) => JSON.stringify(value, null, 2).split("\n").map((line) => jsonLine(line, paint)).join("\n")
20626
- };
20627
- return self;
20628
- }
20629
- function jsonLine(line, paint) {
20630
- const match = /^(\s*)(?:("(?:[^"\\]|\\.)*")(: ))?(.*?)(,?)$/.exec(line);
20631
- if (!match) return line;
20632
- const [, indent = "", key, colon, value = "", comma = ""] = match;
20633
- const painted = value === "" ? "" : /^[[\]{}]+$/.test(value) ? paint("dim", value) : value.startsWith('"') ? value : paint("green", value);
20634
- return indent + (key && colon ? paint("bold", key) + paint("dim", colon) : "") + painted + (comma ? paint("dim", comma) : "");
20635
- }
20636
-
20637
21237
  // src/human.ts
20638
21238
  function kv(entries, style2) {
20639
21239
  const rows = [];
@@ -20675,8 +21275,13 @@ function render(command, result, context, style2) {
20675
21275
  return [
20676
21276
  style2.headline(`Waiting for browser approval (${result.id}).`),
20677
21277
  result.url,
21278
+ // A login's page shows the same code; checking it is what tells a person
21279
+ // the request they are approving is this one.
21280
+ ..."userCode" in result && result.userCode ? [`Pairing code: ${result.userCode} (check that the browser shows the same code)`] : [],
20678
21281
  `Resume: agw operation wait ${result.id}`
20679
21282
  ];
21283
+ if ((command === "operation wait" || command === "operation status") && "kind" in result && result.kind === "login" && result.state === "completed")
21284
+ return [style2.headline(`Signed in to ${context.url}.`), ...operation(result, style2)];
20680
21285
  switch (command) {
20681
21286
  case "account status":
20682
21287
  case "account login":
@@ -20858,7 +21463,7 @@ function show(command, result, style2) {
20858
21463
  function mutation(command, result, context, style2) {
20859
21464
  if ("loggedOut" in result)
20860
21465
  return [
20861
- style2.headline(`Signed out of ${context.url}.`) + " The stored credential was removed."
21466
+ style2.headline(`Signed out of ${context.url}.`) + (result.revoked ? " The key was revoked and removed from this machine." : " The stored credential was removed.")
20862
21467
  ];
20863
21468
  if ("deleted" in result) {
20864
21469
  if ("provider_id" in result)
@@ -21315,27 +21920,23 @@ async function execute(parsed, ctx) {
21315
21920
  if (command.startsWith("operation "))
21316
21921
  return command === "operation wait" ? ctx.wait(args[0] ?? "", positive(flags.timeout ?? 300)) : ctx.poll(args[0] ?? "");
21317
21922
  if (command === "account status") return (await ctx.call("getCliAccount")).data;
21318
- if (command === "account login") return ctx.login();
21923
+ if (command === "account login")
21924
+ return flags["key-stdin"] || flags["key-prompt"] ? ctx.keyLogin() : ctx.browserLogin();
21319
21925
  if (command === "account claim") return ctx.operation("claim", {});
21320
- if (command === "account logout") {
21321
- if (ctx.active) {
21322
- delete ctx.active.credential;
21323
- ctx.active.authenticated = false;
21324
- await ctx.save();
21325
- }
21326
- return { loggedOut: true };
21327
- }
21926
+ if (command === "account logout") return ctx.logout();
21328
21927
  if (command === "deployment connect") {
21329
21928
  const url2 = flags.cloud ? CLOUD : origin(
21330
21929
  flags.url ?? await required2(flags, "url", "Deployment HTTPS URL")
21331
21930
  );
21332
- return ctx.login(url2);
21931
+ if (flags["key-stdin"] || flags["key-prompt"]) return ctx.keyLogin(url2);
21932
+ if (ctx.environmentKey) return ctx.connectWithEnvironment(url2);
21933
+ return ctx.browserLogin(url2);
21333
21934
  }
21334
21935
  if (command === "deployment status") {
21335
21936
  const { data } = await ctx.publicCall("getCliCapabilities");
21336
21937
  return {
21337
21938
  url: ctx.url,
21338
- authenticated: Boolean(ctx.active?.credential),
21939
+ authenticated: Boolean(ctx.managementKey),
21339
21940
  ...data,
21340
21941
  connected: true
21341
21942
  };
@@ -21346,7 +21947,8 @@ async function main(argv = process.argv.slice(2), {
21346
21947
  store = new StateStore(),
21347
21948
  transport = new Transport(),
21348
21949
  stdout = process.stdout,
21349
- stderr: stderr2 = process.stderr
21950
+ stderr: stderr2 = process.stderr,
21951
+ io = {}
21350
21952
  } = {}) {
21351
21953
  const json2 = argv.includes("--json");
21352
21954
  try {
@@ -21361,7 +21963,7 @@ async function main(argv = process.argv.slice(2), {
21361
21963
  }
21362
21964
  const run = async () => {
21363
21965
  const state = await store.read((line) => stderr2.write(line + "\n"));
21364
- const ctx = new Context(store, state, transport, parsed.flags);
21966
+ const ctx = new Context(store, state, transport, parsed.flags, { ...io, stderr: stderr2 });
21365
21967
  const result = await execute(parsed, ctx);
21366
21968
  const context = {
21367
21969
  url: ctx.url,