@maxceem/agw 0.4.0 → 0.5.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/README.md +17 -2
- package/dist/agw.mjs +742 -143
- 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.
|
|
9
|
+
var define_AGW_MANIFEST_default = { version: "0.5.0", wrangler: "4.96.0", upgradeFrom: ["0.5.0", "0.4.0"], release: { url: "https://github.com/maxceem/app-ai-gateway/releases/download/v0.5.0/gateway-0.5.0.tar.gz", sha256: "04aae870521afb7cf669fbd0c7692aae0d551d1b492b954b33618a0fc7bea931", manifest: "69ffad488e129f5487a24d48b0aaead9c4f247200b79336c88dc5e4c49c63b27" } };
|
|
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(
|
|
14565
|
-
if (
|
|
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(
|
|
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 (
|
|
14571
|
+
if (hostname6.endsWith(".")) {
|
|
14572
14572
|
return "Base URL host must not end with a dot";
|
|
14573
14573
|
}
|
|
14574
|
-
const labels =
|
|
14574
|
+
const labels = hostname6.split(".");
|
|
14575
14575
|
if (labels.length < 2) {
|
|
14576
|
-
return `Base URL host must be a public domain name; ${
|
|
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)) {
|
|
@@ -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: --
|
|
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.
|
|
16991
|
+
if (!ctx.managementKey)
|
|
16989
16992
|
return {
|
|
16990
16993
|
local: true,
|
|
16991
16994
|
remote: false,
|
|
@@ -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)
|
|
@@ -17940,8 +17950,11 @@ var PricesResponseSchema = external_exports.object({
|
|
|
17940
17950
|
// ../src/contracts/cli.ts
|
|
17941
17951
|
var CliProofSchema = external_exports.string().regex(/^[A-Za-z0-9_-]{32,256}$/);
|
|
17942
17952
|
var CliBootstrapRequestSchema = external_exports.object({ token: CliProofSchema }).strict();
|
|
17953
|
+
var CliUserCodeSchema = external_exports.string().regex(/^[A-Za-z0-9]{4}-?[A-Za-z0-9]{4}$/);
|
|
17954
|
+
var CliSubmissionTokenSchema = external_exports.string().regex(/^(?:[A-Za-z0-9_-]{32,256}|[A-Za-z0-9]{4}-?[A-Za-z0-9]{4})$/);
|
|
17943
17955
|
var CliOperationKindSchema = external_exports.enum([
|
|
17944
17956
|
"bootstrap",
|
|
17957
|
+
"login",
|
|
17945
17958
|
"claim",
|
|
17946
17959
|
"app.add",
|
|
17947
17960
|
"app.key.add",
|
|
@@ -17970,15 +17983,16 @@ var CliOperationRequestSchema = external_exports.discriminatedUnion("kind", [
|
|
|
17970
17983
|
),
|
|
17971
17984
|
operationRequest("provider-gateway.rotate-key", HandoffRotatePayloadSchema, "always")
|
|
17972
17985
|
]);
|
|
17973
|
-
var CliBrowserProofSchema = external_exports.object({ submissionToken:
|
|
17986
|
+
var CliBrowserProofSchema = external_exports.object({ submissionToken: CliSubmissionTokenSchema }).strict();
|
|
17974
17987
|
var CliBrowserSubmitRequestSchema = external_exports.object({
|
|
17975
|
-
submissionToken:
|
|
17988
|
+
submissionToken: CliSubmissionTokenSchema,
|
|
17976
17989
|
approve: external_exports.literal(true),
|
|
17977
17990
|
// 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()
|
|
17991
|
+
secret: external_exports.string().max(16384).regex(/\S/, { error: "must not be blank" }).optional(),
|
|
17992
|
+
organizationId: external_exports.string().min(1).max(256).optional()
|
|
17979
17993
|
}).strict();
|
|
17980
17994
|
var CliBrowserRegisterRequestSchema = external_exports.object({
|
|
17981
|
-
submissionToken:
|
|
17995
|
+
submissionToken: CliSubmissionTokenSchema,
|
|
17982
17996
|
email: external_exports.email(),
|
|
17983
17997
|
password: external_exports.string().min(8).max(256),
|
|
17984
17998
|
name: external_exports.string().min(1).max(100)
|
|
@@ -17991,35 +18005,89 @@ var CliDeploymentSchema = external_exports.object({
|
|
|
17991
18005
|
});
|
|
17992
18006
|
var CliApprovalRefusalSchema = external_exports.enum([
|
|
17993
18007
|
"registration_required",
|
|
17994
|
-
"sign_out_required"
|
|
18008
|
+
"sign_out_required",
|
|
18009
|
+
/**
|
|
18010
|
+
* A login's approver belongs to no account they could log the CLI in to, and
|
|
18011
|
+
* this deployment does not give a person one of their own. Nothing on the
|
|
18012
|
+
* page can fix that; an owner has to add them to an account first.
|
|
18013
|
+
*/
|
|
18014
|
+
"no_eligible_organization"
|
|
17995
18015
|
]);
|
|
17996
18016
|
var CliViewerSchema = external_exports.object({
|
|
17997
18017
|
name: external_exports.string().nullable(),
|
|
17998
18018
|
email: external_exports.string().nullable()
|
|
17999
18019
|
});
|
|
18020
|
+
var CliOperationClientSchema = external_exports.object({
|
|
18021
|
+
/** E.g. `CLI on mac-studio`. */
|
|
18022
|
+
label: external_exports.string().nullable(),
|
|
18023
|
+
os: external_exports.string().nullable(),
|
|
18024
|
+
/** The address the request came from, as the edge saw it. */
|
|
18025
|
+
ip: external_exports.string().nullable(),
|
|
18026
|
+
requestedAt: external_exports.string()
|
|
18027
|
+
});
|
|
18028
|
+
var CliLoginOrganizationSchema = external_exports.object({
|
|
18029
|
+
id: external_exports.string(),
|
|
18030
|
+
name: external_exports.string(),
|
|
18031
|
+
role: external_exports.enum(["owner", "admin", "member"])
|
|
18032
|
+
});
|
|
18000
18033
|
var CliBrowserDetailsResponseSchema = external_exports.object({
|
|
18001
18034
|
kind: CliOperationKindSchema,
|
|
18002
18035
|
payload: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
18003
|
-
account
|
|
18036
|
+
/** Null only for a login, which lands in whichever account its approver picks. */
|
|
18037
|
+
account: OrganizationSummarySchema.nullable(),
|
|
18004
18038
|
viewer: CliViewerSchema.nullable(),
|
|
18005
18039
|
/**
|
|
18006
18040
|
* What stands between this browser and the Approve button, or null when
|
|
18007
|
-
* nothing does — which is every
|
|
18008
|
-
* is holding the browser. One field decides the whole screen: each value
|
|
18041
|
+
* nothing does — which is every kind but a claim and a login, since only
|
|
18042
|
+
* those two ask who is holding the browser. One field decides the whole screen: each value
|
|
18009
18043
|
* names the single thing the page may offer, so the page never has to
|
|
18010
18044
|
* consult the handoff kind to know what to put in front of a person.
|
|
18011
18045
|
*/
|
|
18012
18046
|
blockedBy: CliApprovalRefusalSchema.nullable(),
|
|
18013
18047
|
googleEnabled: external_exports.boolean(),
|
|
18014
|
-
expiresAt: external_exports.string()
|
|
18048
|
+
expiresAt: external_exports.string(),
|
|
18049
|
+
/**
|
|
18050
|
+
* The code the terminal shows, however the page was reached, so a person
|
|
18051
|
+
* can check it against their terminal before approving. Null for a kind
|
|
18052
|
+
* that has none — every kind but a login.
|
|
18053
|
+
*/
|
|
18054
|
+
userCode: external_exports.string().nullable(),
|
|
18055
|
+
/** Who asked, for a login; null for every other kind. */
|
|
18056
|
+
client: CliOperationClientSchema.nullable(),
|
|
18057
|
+
/** Whether approving hands the browser back to the CLI's local listener (`redirectUrl`). */
|
|
18058
|
+
hasLoopbackRedirect: external_exports.boolean(),
|
|
18059
|
+
/**
|
|
18060
|
+
* For a login only: the accounts the signed-in person may log the CLI in to,
|
|
18061
|
+
* so the page can ask which one when there is more than one.
|
|
18062
|
+
*/
|
|
18063
|
+
organizations: external_exports.array(CliLoginOrganizationSchema).optional()
|
|
18015
18064
|
});
|
|
18016
18065
|
var CliHandoffContinuationSchema = external_exports.enum(["cli", "console"]);
|
|
18017
18066
|
var CliBrowserSubmitResponseSchema = external_exports.object({
|
|
18018
18067
|
state: external_exports.literal("completed"),
|
|
18019
18068
|
/** The whole of what the page says once it is approved. */
|
|
18020
18069
|
message: external_exports.string(),
|
|
18021
|
-
continueTo: CliHandoffContinuationSchema
|
|
18070
|
+
continueTo: CliHandoffContinuationSchema,
|
|
18071
|
+
/**
|
|
18072
|
+
* Set when the CLI registered a local listener: the page navigates here, and
|
|
18073
|
+
* the one-time code in it is what the CLI redeems for its credential.
|
|
18074
|
+
*/
|
|
18075
|
+
redirectUrl: external_exports.url().optional()
|
|
18076
|
+
});
|
|
18077
|
+
var CliBrowserDenyResponseSchema = external_exports.object({
|
|
18078
|
+
state: external_exports.literal("denied"),
|
|
18079
|
+
message: external_exports.string()
|
|
18022
18080
|
});
|
|
18081
|
+
var CliBrowserLookupRequestSchema = external_exports.object({ userCode: external_exports.string().min(1).max(32) }).strict();
|
|
18082
|
+
var CliBrowserLookupResponseSchema = external_exports.discriminatedUnion("found", [
|
|
18083
|
+
external_exports.object({ found: external_exports.literal(false) }),
|
|
18084
|
+
external_exports.object({
|
|
18085
|
+
found: external_exports.literal(true),
|
|
18086
|
+
id: external_exports.string(),
|
|
18087
|
+
kind: CliOperationKindSchema,
|
|
18088
|
+
expiresAt: external_exports.string()
|
|
18089
|
+
})
|
|
18090
|
+
]);
|
|
18023
18091
|
var CliBrowserRegisterResponseSchema = external_exports.object({
|
|
18024
18092
|
token: external_exports.string().nullable().optional(),
|
|
18025
18093
|
redirect: external_exports.boolean().optional()
|
|
@@ -18031,16 +18099,43 @@ var CliBrowserGoogleResponseSchema = external_exports.object({
|
|
|
18031
18099
|
var CliCredentialSchema = external_exports.object({
|
|
18032
18100
|
token: external_exports.string()
|
|
18033
18101
|
});
|
|
18102
|
+
var CliLoginRequestSchema = external_exports.object({
|
|
18103
|
+
token: CliProofSchema,
|
|
18104
|
+
client: external_exports.object({
|
|
18105
|
+
label: external_exports.string().trim().min(1).max(200),
|
|
18106
|
+
os: external_exports.string().trim().min(1).max(64).optional()
|
|
18107
|
+
}).strict(),
|
|
18108
|
+
// Ports 1–65535, spelled out because the source is published as an
|
|
18109
|
+
// OpenAPI `pattern` and has no range syntax.
|
|
18110
|
+
loopbackRedirect: external_exports.string().regex(
|
|
18111
|
+
/^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$/
|
|
18112
|
+
).optional()
|
|
18113
|
+
}).strict();
|
|
18114
|
+
var CliLoginSchema = external_exports.object({
|
|
18115
|
+
id: external_exports.string(),
|
|
18116
|
+
kind: external_exports.literal("login"),
|
|
18117
|
+
/** `completed` when a login already approved is sent again: poll it, or redeem it, for the key. */
|
|
18118
|
+
state: external_exports.enum(["pending", "completed", "denied", "expired"]),
|
|
18119
|
+
url: external_exports.url().nullable(),
|
|
18120
|
+
userCode: external_exports.string().nullable(),
|
|
18121
|
+
expiresAt: external_exports.string()
|
|
18122
|
+
});
|
|
18123
|
+
var CliLoginRedeemRequestSchema = external_exports.object({ redeemCode: CliProofSchema }).strict();
|
|
18124
|
+
var CliLoginRedeemResponseSchema = external_exports.object({
|
|
18125
|
+
credential: CliCredentialSchema,
|
|
18126
|
+
account: OrganizationSummarySchema
|
|
18127
|
+
});
|
|
18128
|
+
var CliCredentialRevokeResponseSchema = external_exports.object({ revoked: external_exports.literal(true) });
|
|
18034
18129
|
var UnclaimedAccessSchema = external_exports.object({ endsAt: external_exports.string(), limit: external_exports.number().int().optional() }).nullable();
|
|
18035
18130
|
var CliOperationResultSchema = external_exports.object({
|
|
18036
|
-
/** A bootstrap's management key. Never print or log it. */
|
|
18131
|
+
/** A bootstrap's or a login's management key. Never print or log it. */
|
|
18037
18132
|
credential: CliCredentialSchema.optional(),
|
|
18038
18133
|
/**
|
|
18039
18134
|
* On a hosted deployment, when a bootstrapped account's free access ends and
|
|
18040
18135
|
* the request allowance it has until then; null on a self-host.
|
|
18041
18136
|
*/
|
|
18042
18137
|
unclaimedAccess: UnclaimedAccessSchema.optional(),
|
|
18043
|
-
/** The account a claim acted on. */
|
|
18138
|
+
/** The account a claim acted on, or a login landed in. */
|
|
18044
18139
|
accountId: external_exports.string().optional(),
|
|
18045
18140
|
app: AppResponseSchema.shape.app.optional(),
|
|
18046
18141
|
/**
|
|
@@ -18055,6 +18150,8 @@ var CliOperationSchema = external_exports.object({
|
|
|
18055
18150
|
id: external_exports.string(),
|
|
18056
18151
|
kind: CliOperationKindSchema,
|
|
18057
18152
|
state: external_exports.enum(["pending", "completed", "expired"]),
|
|
18153
|
+
/** Set beside `state: "expired"` when a person declined the browser step rather than letting it lapse. */
|
|
18154
|
+
denied: external_exports.literal(true).optional(),
|
|
18058
18155
|
expiresAt: external_exports.string(),
|
|
18059
18156
|
url: external_exports.url().optional(),
|
|
18060
18157
|
deployment: CliDeploymentSchema,
|
|
@@ -18092,7 +18189,12 @@ var CliCapabilitiesResponseSchema = external_exports.object({
|
|
|
18092
18189
|
endpointStyles: external_exports.array(external_exports.string())
|
|
18093
18190
|
})
|
|
18094
18191
|
),
|
|
18095
|
-
providerGateways: external_exports.array(external_exports.object({ type: external_exports.string(), name: external_exports.string() }))
|
|
18192
|
+
providerGateways: external_exports.array(external_exports.object({ type: external_exports.string(), name: external_exports.string() })),
|
|
18193
|
+
/** What this deployment supports beyond protocol 1's baseline. Absent on older deployments. */
|
|
18194
|
+
features: external_exports.object({
|
|
18195
|
+
/** `POST /v1/cli/login`: a person approves a CLI in a browser and it receives a key of its own. */
|
|
18196
|
+
browserLogin: external_exports.boolean()
|
|
18197
|
+
}).optional()
|
|
18096
18198
|
});
|
|
18097
18199
|
var [SelfHostedAccessSchema, UnavailableAccessSchema, BilledAccessSchema] = GatewayBillingAccessSchema.options;
|
|
18098
18200
|
var CliBillingAccessSchema = external_exports.discriminatedUnion("state", [
|
|
@@ -18131,7 +18233,7 @@ var CLI_ERRORS = {
|
|
|
18131
18233
|
410: "The protected credential exchange has expired; no replacement is minted.",
|
|
18132
18234
|
429: "The durable initiation or submission rate limit was reached."
|
|
18133
18235
|
};
|
|
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.";
|
|
18236
|
+
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
18237
|
var SignUpRequestSchema = external_exports.object({
|
|
18136
18238
|
name: external_exports.string(),
|
|
18137
18239
|
email: external_exports.email(),
|
|
@@ -18954,12 +19056,51 @@ var CATALOG = {
|
|
|
18954
19056
|
responseDescription: "Where the operation stands: completed with its result, or pending with the approval URL.",
|
|
18955
19057
|
errors: CLI_ERRORS
|
|
18956
19058
|
},
|
|
19059
|
+
openCliLogin: {
|
|
19060
|
+
method: "POST",
|
|
19061
|
+
path: "/v1/cli/login",
|
|
19062
|
+
tags: ["CLI"],
|
|
19063
|
+
summary: "Ask a person to log this CLI in",
|
|
19064
|
+
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.",
|
|
19065
|
+
security: "public",
|
|
19066
|
+
request: CliLoginRequestSchema,
|
|
19067
|
+
response: CliLoginSchema,
|
|
19068
|
+
responseDescription: "The pending login, its approval URL and its user code.",
|
|
19069
|
+
errors: CLI_ERRORS
|
|
19070
|
+
},
|
|
19071
|
+
redeemCliLogin: {
|
|
19072
|
+
method: "POST",
|
|
19073
|
+
path: "/v1/cli/login/{id}/redeem",
|
|
19074
|
+
tags: ["CLI"],
|
|
19075
|
+
summary: "Exchange a login's redeem code for its credential",
|
|
19076
|
+
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.",
|
|
19077
|
+
security: "cliPoll",
|
|
19078
|
+
params: { id: {} },
|
|
19079
|
+
request: CliLoginRedeemRequestSchema,
|
|
19080
|
+
response: CliLoginRedeemResponseSchema,
|
|
19081
|
+
responseDescription: "The management key the login issued, and the account it belongs to.",
|
|
19082
|
+
errors: CLI_ERRORS
|
|
19083
|
+
},
|
|
19084
|
+
revokeCliCredential: {
|
|
19085
|
+
method: "DELETE",
|
|
19086
|
+
path: "/v1/cli/credential",
|
|
19087
|
+
tags: ["CLI"],
|
|
19088
|
+
summary: "Revoke the management key this request is sent with",
|
|
19089
|
+
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.",
|
|
19090
|
+
security: "management",
|
|
19091
|
+
// Holding a key is authority enough to end it: a member may, and an
|
|
19092
|
+
// account past its free window may.
|
|
19093
|
+
policy: { role: "member", access: "read" },
|
|
19094
|
+
response: CliCredentialRevokeResponseSchema,
|
|
19095
|
+
responseDescription: "The key is revoked; the next request made with it is refused.",
|
|
19096
|
+
errors: CLI_ERRORS
|
|
19097
|
+
},
|
|
18957
19098
|
pollCliOperation: {
|
|
18958
19099
|
method: "GET",
|
|
18959
19100
|
path: "/v1/cli/operations/{id}",
|
|
18960
19101
|
tags: ["CLI"],
|
|
18961
19102
|
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.",
|
|
19103
|
+
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
19104
|
security: "cliPoll",
|
|
18964
19105
|
params: { id: {} },
|
|
18965
19106
|
response: CliOperationSchema,
|
|
@@ -19020,7 +19161,32 @@ var CATALOG = {
|
|
|
19020
19161
|
params: { id: {} },
|
|
19021
19162
|
request: CliBrowserSubmitRequestSchema,
|
|
19022
19163
|
response: CliBrowserSubmitResponseSchema,
|
|
19023
|
-
responseDescription: "The handoff is approved and consumed. No submitted secret is ever echoed.",
|
|
19164
|
+
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.",
|
|
19165
|
+
errors: CLI_ERRORS
|
|
19166
|
+
},
|
|
19167
|
+
cliBrowserDeny: {
|
|
19168
|
+
method: "POST",
|
|
19169
|
+
path: "/v1/cli/browser/{id}/deny",
|
|
19170
|
+
tags: ["CLI"],
|
|
19171
|
+
summary: "Browser handoff: deny",
|
|
19172
|
+
description: `${BROWSER_HANDOFF_DESCRIPTION} Anyone holding the link or the code may decline, signed in or not.`,
|
|
19173
|
+
security: "public",
|
|
19174
|
+
params: { id: {} },
|
|
19175
|
+
request: CliBrowserProofSchema,
|
|
19176
|
+
response: CliBrowserDenyResponseSchema,
|
|
19177
|
+
responseDescription: "The handoff is declined; the CLI's next poll reports it. Repeating a denial answers the same.",
|
|
19178
|
+
errors: CLI_ERRORS
|
|
19179
|
+
},
|
|
19180
|
+
cliBrowserLookup: {
|
|
19181
|
+
method: "POST",
|
|
19182
|
+
path: "/v1/cli/browser/lookup",
|
|
19183
|
+
tags: ["CLI"],
|
|
19184
|
+
summary: "Browser handoff: find a step by its user code",
|
|
19185
|
+
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.",
|
|
19186
|
+
security: "public",
|
|
19187
|
+
request: CliBrowserLookupRequestSchema,
|
|
19188
|
+
response: CliBrowserLookupResponseSchema,
|
|
19189
|
+
responseDescription: "Whether a pending step answers to the code, and which one.",
|
|
19024
19190
|
errors: CLI_ERRORS
|
|
19025
19191
|
},
|
|
19026
19192
|
cliBrowserRegister: {
|
|
@@ -19033,7 +19199,7 @@ var CATALOG = {
|
|
|
19033
19199
|
params: { id: {} },
|
|
19034
19200
|
request: CliBrowserRegisterRequestSchema,
|
|
19035
19201
|
response: CliBrowserRegisterResponseSchema,
|
|
19036
|
-
responseDescription: "A new human identity for a pending claim, with its session set as a cookie.",
|
|
19202
|
+
responseDescription: "A new human identity for a pending claim or login, with its session set as a cookie.",
|
|
19037
19203
|
errors: CLI_ERRORS
|
|
19038
19204
|
},
|
|
19039
19205
|
cliBrowserGoogle: {
|
|
@@ -19046,7 +19212,7 @@ var CATALOG = {
|
|
|
19046
19212
|
params: { id: {} },
|
|
19047
19213
|
request: CliBrowserProofSchema,
|
|
19048
19214
|
response: CliBrowserGoogleResponseSchema,
|
|
19049
|
-
responseDescription: "Where to send the browser to start Google consent for a pending claim.",
|
|
19215
|
+
responseDescription: "Where to send the browser to start Google consent for a pending claim or login.",
|
|
19050
19216
|
errors: CLI_ERRORS
|
|
19051
19217
|
}
|
|
19052
19218
|
};
|
|
@@ -19068,6 +19234,167 @@ function operationPath(name, ...rest) {
|
|
|
19068
19234
|
return query === void 0 ? filled : filled + searchSuffix(query);
|
|
19069
19235
|
}
|
|
19070
19236
|
|
|
19237
|
+
// src/login.ts
|
|
19238
|
+
import { createServer } from "node:http";
|
|
19239
|
+
import { hostname as hostname4, platform as platform2, release } from "node:os";
|
|
19240
|
+
var REDEEM_CODE_PARAMETER = "code";
|
|
19241
|
+
var PAGE_HEADERS = {
|
|
19242
|
+
"content-type": "text/html; charset=utf-8",
|
|
19243
|
+
"cache-control": "no-store",
|
|
19244
|
+
"referrer-policy": "no-referrer",
|
|
19245
|
+
"x-content-type-options": "nosniff",
|
|
19246
|
+
connection: "close"
|
|
19247
|
+
};
|
|
19248
|
+
function page(title, text2) {
|
|
19249
|
+
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>`;
|
|
19250
|
+
}
|
|
19251
|
+
var CONNECTED_PAGE = page(
|
|
19252
|
+
"Your terminal is connected",
|
|
19253
|
+
"The CLI has its key. You can close this tab."
|
|
19254
|
+
);
|
|
19255
|
+
var FAILED_PAGE = page(
|
|
19256
|
+
"The login did not complete",
|
|
19257
|
+
"Return to your terminal to see what happened."
|
|
19258
|
+
);
|
|
19259
|
+
async function listenLoopback() {
|
|
19260
|
+
let deliver;
|
|
19261
|
+
const code = new Promise((resolve3) => {
|
|
19262
|
+
deliver = resolve3;
|
|
19263
|
+
});
|
|
19264
|
+
let waiting = null;
|
|
19265
|
+
let delivered = false;
|
|
19266
|
+
const server = createServer((request, response) => {
|
|
19267
|
+
const url2 = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
19268
|
+
if (request.method !== "GET" || url2.pathname !== "/callback") {
|
|
19269
|
+
response.writeHead(404, PAGE_HEADERS).end(page("Not found", "Nothing is served here."));
|
|
19270
|
+
return;
|
|
19271
|
+
}
|
|
19272
|
+
const value = url2.searchParams.get(REDEEM_CODE_PARAMETER) ?? "";
|
|
19273
|
+
if (delivered || !CliProofSchema.safeParse(value).success) {
|
|
19274
|
+
response.writeHead(400, PAGE_HEADERS).end(FAILED_PAGE);
|
|
19275
|
+
return;
|
|
19276
|
+
}
|
|
19277
|
+
delivered = true;
|
|
19278
|
+
waiting = response;
|
|
19279
|
+
deliver(value);
|
|
19280
|
+
});
|
|
19281
|
+
await new Promise((resolve3, reject2) => {
|
|
19282
|
+
server.once("error", reject2);
|
|
19283
|
+
server.listen(0, "127.0.0.1", () => {
|
|
19284
|
+
server.off("error", reject2);
|
|
19285
|
+
resolve3();
|
|
19286
|
+
});
|
|
19287
|
+
});
|
|
19288
|
+
const { port } = server.address();
|
|
19289
|
+
let answered = Promise.resolve();
|
|
19290
|
+
const loopback = {
|
|
19291
|
+
redirect: `http://127.0.0.1:${port}/callback`,
|
|
19292
|
+
code,
|
|
19293
|
+
answer(connected) {
|
|
19294
|
+
const response = waiting;
|
|
19295
|
+
if (!response) return;
|
|
19296
|
+
waiting = null;
|
|
19297
|
+
answered = new Promise((resolve3) => {
|
|
19298
|
+
response.once("finish", resolve3);
|
|
19299
|
+
response.once("close", resolve3);
|
|
19300
|
+
});
|
|
19301
|
+
response.writeHead(200, PAGE_HEADERS).end(connected ? CONNECTED_PAGE : FAILED_PAGE);
|
|
19302
|
+
},
|
|
19303
|
+
async close() {
|
|
19304
|
+
loopback.answer(false);
|
|
19305
|
+
await answered;
|
|
19306
|
+
await new Promise((resolve3) => {
|
|
19307
|
+
const force = setTimeout(() => server.closeAllConnections(), 1e3);
|
|
19308
|
+
force.unref();
|
|
19309
|
+
server.close(() => {
|
|
19310
|
+
clearTimeout(force);
|
|
19311
|
+
resolve3();
|
|
19312
|
+
});
|
|
19313
|
+
server.closeIdleConnections();
|
|
19314
|
+
});
|
|
19315
|
+
}
|
|
19316
|
+
};
|
|
19317
|
+
return loopback;
|
|
19318
|
+
}
|
|
19319
|
+
function clientDescription() {
|
|
19320
|
+
return {
|
|
19321
|
+
label: `CLI on ${hostname4() || "this computer"}`.slice(0, 200),
|
|
19322
|
+
os: `${platform2()} ${release()}`.trim().slice(0, 64)
|
|
19323
|
+
};
|
|
19324
|
+
}
|
|
19325
|
+
function browserAvailable({
|
|
19326
|
+
env,
|
|
19327
|
+
platform: system,
|
|
19328
|
+
terminal
|
|
19329
|
+
} = {
|
|
19330
|
+
env: process.env,
|
|
19331
|
+
platform: platform2(),
|
|
19332
|
+
terminal: Boolean(process.stdin.isTTY && process.stderr.isTTY)
|
|
19333
|
+
}) {
|
|
19334
|
+
if (!terminal) return false;
|
|
19335
|
+
if (env["SSH_CONNECTION"] || env["SSH_TTY"]) return false;
|
|
19336
|
+
if (system === "linux" && !env["DISPLAY"] && !env["WAYLAND_DISPLAY"]) return false;
|
|
19337
|
+
return true;
|
|
19338
|
+
}
|
|
19339
|
+
|
|
19340
|
+
// src/style.ts
|
|
19341
|
+
import { Writable } from "node:stream";
|
|
19342
|
+
import { styleText } from "node:util";
|
|
19343
|
+
var STATES = /* @__PURE__ */ new Map([
|
|
19344
|
+
["active", "green"],
|
|
19345
|
+
["ready", "green"],
|
|
19346
|
+
["completed", "green"],
|
|
19347
|
+
["yes", "green"],
|
|
19348
|
+
["pending", "yellow"],
|
|
19349
|
+
["failed", "red"],
|
|
19350
|
+
["expired", "red"],
|
|
19351
|
+
["blocked", "red"],
|
|
19352
|
+
["inactive", "red"],
|
|
19353
|
+
["disabled", "red"],
|
|
19354
|
+
["revoked", "red"],
|
|
19355
|
+
["no", "red"]
|
|
19356
|
+
]);
|
|
19357
|
+
function colorEnabled(sink, env = process.env) {
|
|
19358
|
+
if (env["NO_COLOR"]) return false;
|
|
19359
|
+
if (!(sink instanceof Writable)) return false;
|
|
19360
|
+
if (env["FORCE_COLOR"] && env["FORCE_COLOR"] !== "0") return true;
|
|
19361
|
+
if (!("isTTY" in sink && sink.isTTY === true)) return false;
|
|
19362
|
+
return styleText("dim", "?", { stream: sink, validateStream: true }) !== "?";
|
|
19363
|
+
}
|
|
19364
|
+
var plain = style(false);
|
|
19365
|
+
var styleFor = (sink) => style(colorEnabled(sink));
|
|
19366
|
+
function style(enabled) {
|
|
19367
|
+
const paint = (format, text2) => enabled ? styleText(format, text2, { validateStream: false }) : text2;
|
|
19368
|
+
const self = {
|
|
19369
|
+
enabled,
|
|
19370
|
+
headline: (text2) => paint("bold", text2),
|
|
19371
|
+
dim: (text2) => paint("dim", text2),
|
|
19372
|
+
alert: (text2) => paint("red", text2),
|
|
19373
|
+
warn: (text2) => paint("yellow", text2),
|
|
19374
|
+
state: (value) => {
|
|
19375
|
+
const tone = STATES.get(value.toLowerCase());
|
|
19376
|
+
return tone ? paint(tone, value) : value;
|
|
19377
|
+
},
|
|
19378
|
+
code: (text2, language) => {
|
|
19379
|
+
const rule = `\u2500\u2500 ${language} \u2500\u2500`;
|
|
19380
|
+
return [
|
|
19381
|
+
self.dim(rule),
|
|
19382
|
+
...text2.split("\n").map((line) => self.dim(line)),
|
|
19383
|
+
self.dim("\u2500".repeat(rule.length))
|
|
19384
|
+
];
|
|
19385
|
+
},
|
|
19386
|
+
json: (value) => JSON.stringify(value, null, 2).split("\n").map((line) => jsonLine(line, paint)).join("\n")
|
|
19387
|
+
};
|
|
19388
|
+
return self;
|
|
19389
|
+
}
|
|
19390
|
+
function jsonLine(line, paint) {
|
|
19391
|
+
const match = /^(\s*)(?:("(?:[^"\\]|\\.)*")(: ))?(.*?)(,?)$/.exec(line);
|
|
19392
|
+
if (!match) return line;
|
|
19393
|
+
const [, indent = "", key, colon, value = "", comma = ""] = match;
|
|
19394
|
+
const painted = value === "" ? "" : /^[[\]{}]+$/.test(value) ? paint("dim", value) : value.startsWith('"') ? value : paint("green", value);
|
|
19395
|
+
return indent + (key && colon ? paint("bold", key) + paint("dim", colon) : "") + painted + (comma ? paint("dim", comma) : "");
|
|
19396
|
+
}
|
|
19397
|
+
|
|
19071
19398
|
// src/context.ts
|
|
19072
19399
|
var CLI_OPERATIONS = /* @__PURE__ */ new Set([
|
|
19073
19400
|
"getCliCapabilities",
|
|
@@ -19075,8 +19402,21 @@ var CLI_OPERATIONS = /* @__PURE__ */ new Set([
|
|
|
19075
19402
|
"createCliOperation",
|
|
19076
19403
|
"pollCliOperation",
|
|
19077
19404
|
"getCliAccount",
|
|
19078
|
-
"getCliUsage"
|
|
19405
|
+
"getCliUsage",
|
|
19406
|
+
"openCliLogin",
|
|
19407
|
+
"redeemCliLogin",
|
|
19408
|
+
"revokeCliCredential"
|
|
19079
19409
|
]);
|
|
19410
|
+
var LOGIN_TIMEOUT_SECONDS = 300;
|
|
19411
|
+
var LOGIN_RECORD_MS = 36e5;
|
|
19412
|
+
var LOOPBACK_GRACE_MS = 1e4;
|
|
19413
|
+
var defaultIo = () => ({
|
|
19414
|
+
stderr: process.stderr,
|
|
19415
|
+
openBrowser,
|
|
19416
|
+
browserAvailable: () => browserAvailable(),
|
|
19417
|
+
listen: listenLoopback,
|
|
19418
|
+
env: process.env
|
|
19419
|
+
});
|
|
19080
19420
|
var KEY_OPERATION_KINDS = /* @__PURE__ */ new Set(["app.add", "app.key.add"]);
|
|
19081
19421
|
var OPERATION_RETENTION_MS = 90 * 864e5;
|
|
19082
19422
|
var pathFor = operationPath;
|
|
@@ -19106,14 +19446,16 @@ var Context = class {
|
|
|
19106
19446
|
state;
|
|
19107
19447
|
transport;
|
|
19108
19448
|
flags;
|
|
19449
|
+
io;
|
|
19109
19450
|
/** Operations whose result this run has printed, released once stdout has taken it. */
|
|
19110
19451
|
delivered = /* @__PURE__ */ new Set();
|
|
19111
19452
|
onboarding;
|
|
19112
|
-
constructor(store, state, transport, flags) {
|
|
19453
|
+
constructor(store, state, transport, flags, io = {}) {
|
|
19113
19454
|
this.store = store;
|
|
19114
19455
|
this.state = state;
|
|
19115
19456
|
this.transport = transport;
|
|
19116
19457
|
this.flags = flags;
|
|
19458
|
+
this.io = { ...defaultIo(), ...io };
|
|
19117
19459
|
}
|
|
19118
19460
|
get active() {
|
|
19119
19461
|
return this.state.active;
|
|
@@ -19121,6 +19463,28 @@ var Context = class {
|
|
|
19121
19463
|
get url() {
|
|
19122
19464
|
return this.active?.url || CLOUD;
|
|
19123
19465
|
}
|
|
19466
|
+
/** `AGW_MANAGEMENT_KEY`, when it is set to anything. */
|
|
19467
|
+
get environmentKey() {
|
|
19468
|
+
const value = this.io.env[MANAGEMENT_KEY_ENV]?.trim();
|
|
19469
|
+
return value ? value : void 0;
|
|
19470
|
+
}
|
|
19471
|
+
/**
|
|
19472
|
+
* The management credential every call is sent with: the environment's, which
|
|
19473
|
+
* always wins, or the one the selected connection stored.
|
|
19474
|
+
*/
|
|
19475
|
+
get managementKey() {
|
|
19476
|
+
return this.environmentKey ?? this.active?.credential;
|
|
19477
|
+
}
|
|
19478
|
+
/** Refuses a command that would store a key the environment would then override. */
|
|
19479
|
+
assertNoEnvironmentKey() {
|
|
19480
|
+
if (this.environmentKey)
|
|
19481
|
+
fail(
|
|
19482
|
+
"environment_credential",
|
|
19483
|
+
`${MANAGEMENT_KEY_ENV} is set, so it is the credential for every command and a stored login would never be used.`,
|
|
19484
|
+
`Unset ${MANAGEMENT_KEY_ENV} to log in, or keep using it as is.`,
|
|
19485
|
+
4
|
|
19486
|
+
);
|
|
19487
|
+
}
|
|
19124
19488
|
async save() {
|
|
19125
19489
|
await this.store.write(this.state);
|
|
19126
19490
|
}
|
|
@@ -19158,7 +19522,20 @@ var Context = class {
|
|
|
19158
19522
|
}
|
|
19159
19523
|
/** The same call, with the connection's management credential attached. */
|
|
19160
19524
|
async call(name, options = {}) {
|
|
19161
|
-
|
|
19525
|
+
const key = options.key ?? this.credential(name);
|
|
19526
|
+
try {
|
|
19527
|
+
return await this.publicCall(name, { ...options, key });
|
|
19528
|
+
} catch (error51) {
|
|
19529
|
+
if (error51 instanceof CliError && error51.details?.status === 401 && options.key === void 0 && this.environmentKey)
|
|
19530
|
+
throw new CliError(
|
|
19531
|
+
error51.code,
|
|
19532
|
+
error51.message,
|
|
19533
|
+
`Check ${MANAGEMENT_KEY_ENV}: the deployment refused the key it holds.`,
|
|
19534
|
+
error51.exitCode,
|
|
19535
|
+
error51.details
|
|
19536
|
+
);
|
|
19537
|
+
throw error51;
|
|
19538
|
+
}
|
|
19162
19539
|
}
|
|
19163
19540
|
/**
|
|
19164
19541
|
* The current management credential, or the refusal that names the way back.
|
|
@@ -19166,12 +19543,12 @@ var Context = class {
|
|
|
19166
19543
|
* which is why those get the wider instruction.
|
|
19167
19544
|
*/
|
|
19168
19545
|
credential(name) {
|
|
19169
|
-
const token = this.
|
|
19546
|
+
const token = this.managementKey;
|
|
19170
19547
|
if (!token)
|
|
19171
19548
|
fail(
|
|
19172
19549
|
"login_required",
|
|
19173
19550
|
"The selected connection is not authenticated.",
|
|
19174
|
-
CLI_OPERATIONS.has(name) ?
|
|
19551
|
+
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
19552
|
4
|
|
19176
19553
|
);
|
|
19177
19554
|
return token;
|
|
@@ -19247,11 +19624,11 @@ var Context = class {
|
|
|
19247
19624
|
* to, in which case it answers pending with the URL and the id to resume.
|
|
19248
19625
|
*/
|
|
19249
19626
|
async operation(kind, payload, { browser: browser2 = false, url: url2 = this.url } = {}) {
|
|
19250
|
-
if (!this.
|
|
19627
|
+
if (!this.managementKey)
|
|
19251
19628
|
fail(
|
|
19252
19629
|
"login_required",
|
|
19253
19630
|
"This operation requires account management access.",
|
|
19254
|
-
|
|
19631
|
+
`Run agw account login, or set ${MANAGEMENT_KEY_ENV}.`,
|
|
19255
19632
|
4
|
|
19256
19633
|
);
|
|
19257
19634
|
const target = origin(url2);
|
|
@@ -19262,7 +19639,7 @@ var Context = class {
|
|
|
19262
19639
|
// which is the correlation the compiler cannot follow through a generic.
|
|
19263
19640
|
body: { kind, payload, token: record2.token, ...browser2 ? { browser: true } : {} },
|
|
19264
19641
|
url: target,
|
|
19265
|
-
key: this.
|
|
19642
|
+
key: this.managementKey
|
|
19266
19643
|
})).data);
|
|
19267
19644
|
if (data.state === "expired") {
|
|
19268
19645
|
await this.forget(id);
|
|
@@ -19273,15 +19650,11 @@ var Context = class {
|
|
|
19273
19650
|
return data;
|
|
19274
19651
|
}
|
|
19275
19652
|
const { data: capabilities } = await this.publicCall("getCliCapabilities", { url: target });
|
|
19276
|
-
|
|
19277
|
-
|
|
19278
|
-
|
|
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);
|
|
19653
|
+
assertTrustedApprovalUrl(data.url, capabilities, target);
|
|
19654
|
+
if (!this.flags["no-open"] && !await this.io.openBrowser(data.url))
|
|
19655
|
+
this.io.stderr.write("Browser could not open. Use the returned handoff URL.\n");
|
|
19283
19656
|
if (this.flags["no-open"] || this.flags.json || this.flags["no-input"]) return data;
|
|
19284
|
-
|
|
19657
|
+
this.io.stderr.write(`Complete the browser step: ${data.url}
|
|
19285
19658
|
Operation: ${data.id}
|
|
19286
19659
|
`);
|
|
19287
19660
|
return this.wait(data.id, 300);
|
|
@@ -19297,8 +19670,13 @@ Operation: ${data.id}
|
|
|
19297
19670
|
* Once the file holds the key, the record keeps only its metadata.
|
|
19298
19671
|
*/
|
|
19299
19672
|
async keyOperation(kind, payload, requestedPath) {
|
|
19300
|
-
if (!this.
|
|
19301
|
-
fail(
|
|
19673
|
+
if (!this.managementKey)
|
|
19674
|
+
fail(
|
|
19675
|
+
"login_required",
|
|
19676
|
+
"The selected connection is not authenticated.",
|
|
19677
|
+
`Run agw account login, or set ${MANAGEMENT_KEY_ENV}.`,
|
|
19678
|
+
4
|
|
19679
|
+
);
|
|
19302
19680
|
const target = origin(this.url);
|
|
19303
19681
|
const [id, record2] = await this.reserveOperation(kind, payload, target);
|
|
19304
19682
|
const chosen = requestedPath ? resolve2(requestedPath) : record2.output?.path;
|
|
@@ -19331,7 +19709,7 @@ Operation: ${data.id}
|
|
|
19331
19709
|
const operation2 = await this.send(id, async () => (await this.publicCall("createCliOperation", {
|
|
19332
19710
|
body: { kind, payload, token: record2.token },
|
|
19333
19711
|
url: target,
|
|
19334
|
-
key: this.
|
|
19712
|
+
key: this.managementKey
|
|
19335
19713
|
})).data);
|
|
19336
19714
|
const minted = operation2.result?.api_key;
|
|
19337
19715
|
const appId = operation2.result?.app?.id ?? payload.app ?? "";
|
|
@@ -19375,7 +19753,7 @@ Operation: ${data.id}
|
|
|
19375
19753
|
}
|
|
19376
19754
|
}
|
|
19377
19755
|
async bootstrap() {
|
|
19378
|
-
if (this.
|
|
19756
|
+
if (this.managementKey) return;
|
|
19379
19757
|
if (this.active)
|
|
19380
19758
|
fail(
|
|
19381
19759
|
"login_required",
|
|
@@ -19421,7 +19799,9 @@ Operation: ${data.id}
|
|
|
19421
19799
|
};
|
|
19422
19800
|
await this.save();
|
|
19423
19801
|
}
|
|
19424
|
-
|
|
19802
|
+
/** `account login --key-stdin` or `--key-prompt`: a management key a person already holds. */
|
|
19803
|
+
async keyLogin(url2 = this.url) {
|
|
19804
|
+
this.assertNoEnvironmentKey();
|
|
19425
19805
|
const token = await secret(this.flags, "Management API key");
|
|
19426
19806
|
if (!token)
|
|
19427
19807
|
fail("input_required", "A management API key is required.", "Supply it with --key-stdin.");
|
|
@@ -19429,6 +19809,239 @@ Operation: ${data.id}
|
|
|
19429
19809
|
await this.select(url2, { ...data, credential: { token } });
|
|
19430
19810
|
return { connected: true, ...data };
|
|
19431
19811
|
}
|
|
19812
|
+
/**
|
|
19813
|
+
* `deployment connect` while `AGW_MANAGEMENT_KEY` is set: selects the
|
|
19814
|
+
* deployment the key is checked against, and stores no key of its own.
|
|
19815
|
+
*/
|
|
19816
|
+
async connectWithEnvironment(url2) {
|
|
19817
|
+
const key = this.environmentKey;
|
|
19818
|
+
const target = origin(url2);
|
|
19819
|
+
const { data } = await this.publicCall("getCliAccount", { key, url: target });
|
|
19820
|
+
this.state.active = {
|
|
19821
|
+
url: target,
|
|
19822
|
+
account: data.account,
|
|
19823
|
+
deployment: data.deployment,
|
|
19824
|
+
authenticated: false
|
|
19825
|
+
};
|
|
19826
|
+
await this.save();
|
|
19827
|
+
this.io.stderr.write(
|
|
19828
|
+
`${MANAGEMENT_KEY_ENV} is set, so it stays the credential for every command; no key was stored.
|
|
19829
|
+
`
|
|
19830
|
+
);
|
|
19831
|
+
return { connected: true, ...data };
|
|
19832
|
+
}
|
|
19833
|
+
/**
|
|
19834
|
+
* `account login`: a person approves this CLI in a browser, and it receives
|
|
19835
|
+
* a management key of its own, stored exactly as a pasted one is.
|
|
19836
|
+
*
|
|
19837
|
+
* The login is recorded like every other operation before it is opened, so
|
|
19838
|
+
* one left waiting — by `--json`, `--no-input`, a timeout — is finished by
|
|
19839
|
+
* `agw operation wait`. When this process is going to wait and a browser on
|
|
19840
|
+
* this machine is in front of the person, it also listens on `127.0.0.1`,
|
|
19841
|
+
* and the approval page hands the browser back to it with a one-time code;
|
|
19842
|
+
* the key is then released only for that code and never to a poll.
|
|
19843
|
+
* Otherwise — `--no-open`, SSH, no display, nobody at the terminal — the
|
|
19844
|
+
* approval may happen on any device, so nothing listens and the key is
|
|
19845
|
+
* polled for. A browser that turns out not to open is the same case, found
|
|
19846
|
+
* late: that login is abandoned for one that is polled.
|
|
19847
|
+
*/
|
|
19848
|
+
async browserLogin(url2 = this.url) {
|
|
19849
|
+
this.assertNoEnvironmentKey();
|
|
19850
|
+
const target = origin(url2);
|
|
19851
|
+
const { data: capabilities } = await this.publicCall("getCliCapabilities", { url: target });
|
|
19852
|
+
if (!capabilities.features?.browserLogin)
|
|
19853
|
+
fail(
|
|
19854
|
+
"browser_login_unsupported",
|
|
19855
|
+
"This deployment does not offer browser login.",
|
|
19856
|
+
"Update the deployment, or pipe a management key to agw account login --key-stdin.",
|
|
19857
|
+
3
|
|
19858
|
+
);
|
|
19859
|
+
const opens = !this.flags["no-open"] && this.io.browserAvailable();
|
|
19860
|
+
const detached = Boolean(this.flags.json || this.flags["no-input"]);
|
|
19861
|
+
if (detached) {
|
|
19862
|
+
const { login } = await this.openLogin(target, capabilities, null);
|
|
19863
|
+
if (opens && !await this.io.openBrowser(login.url))
|
|
19864
|
+
this.io.stderr.write("Browser could not open. Use the returned URL.\n");
|
|
19865
|
+
return login;
|
|
19866
|
+
}
|
|
19867
|
+
let loopback = opens ? await this.io.listen() : null;
|
|
19868
|
+
try {
|
|
19869
|
+
let opened = await this.openLogin(target, capabilities, loopback);
|
|
19870
|
+
if (loopback && !await this.io.openBrowser(opened.login.url)) {
|
|
19871
|
+
await loopback.close();
|
|
19872
|
+
loopback = null;
|
|
19873
|
+
await this.forget(opened.id);
|
|
19874
|
+
this.io.stderr.write(
|
|
19875
|
+
"The browser could not be opened, so the approval will be collected by polling.\n"
|
|
19876
|
+
);
|
|
19877
|
+
opened = await this.openLogin(target, capabilities, null);
|
|
19878
|
+
}
|
|
19879
|
+
const { id, token, login } = opened;
|
|
19880
|
+
const style2 = styleFor(this.io.stderr);
|
|
19881
|
+
this.io.stderr.write(
|
|
19882
|
+
[
|
|
19883
|
+
...login.userCode ? [
|
|
19884
|
+
`Pairing code: ${style2.headline(login.userCode)}`,
|
|
19885
|
+
"Check that your browser shows this same code before you approve."
|
|
19886
|
+
] : [],
|
|
19887
|
+
`Approve at: ${login.url}`,
|
|
19888
|
+
""
|
|
19889
|
+
].join("\n")
|
|
19890
|
+
);
|
|
19891
|
+
if (loopback) {
|
|
19892
|
+
try {
|
|
19893
|
+
await this.collectLoopbackLogin(id, token, target, capabilities, loopback);
|
|
19894
|
+
} finally {
|
|
19895
|
+
if (this.state.operations[id]) await this.forget(id);
|
|
19896
|
+
}
|
|
19897
|
+
} else await this.wait(id, LOGIN_TIMEOUT_SECONDS);
|
|
19898
|
+
const { data } = await this.call("getCliAccount", { url: target });
|
|
19899
|
+
return { connected: true, ...data };
|
|
19900
|
+
} finally {
|
|
19901
|
+
await loopback?.close();
|
|
19902
|
+
}
|
|
19903
|
+
}
|
|
19904
|
+
/** Records and opens one login, refusing one that is not pending on the trusted console. */
|
|
19905
|
+
async openLogin(target, capabilities, loopback) {
|
|
19906
|
+
const token = randomToken();
|
|
19907
|
+
const id = operationIdFor(token);
|
|
19908
|
+
await this.recordLogin(id, token, target);
|
|
19909
|
+
const { data: login } = await this.send(id, () => this.publicCall("openCliLogin", {
|
|
19910
|
+
body: {
|
|
19911
|
+
token,
|
|
19912
|
+
client: clientDescription(),
|
|
19913
|
+
...loopback ? { loopbackRedirect: loopback.redirect } : {}
|
|
19914
|
+
},
|
|
19915
|
+
url: target
|
|
19916
|
+
}));
|
|
19917
|
+
if (login.state !== "pending" || !login.url) {
|
|
19918
|
+
await this.forget(id);
|
|
19919
|
+
expired({
|
|
19920
|
+
id,
|
|
19921
|
+
state: "expired",
|
|
19922
|
+
expiresAt: login.expiresAt,
|
|
19923
|
+
...login.state === "denied" ? { denied: true } : {}
|
|
19924
|
+
});
|
|
19925
|
+
}
|
|
19926
|
+
assertTrustedApprovalUrl(login.url, capabilities, target);
|
|
19927
|
+
return { id, token, login: { ...login, url: login.url } };
|
|
19928
|
+
}
|
|
19929
|
+
/** Writes a login's record before it is opened, and drops logins long since expired. */
|
|
19930
|
+
async recordLogin(id, token, url2) {
|
|
19931
|
+
const now = Date.now();
|
|
19932
|
+
for (const [key, record2] of Object.entries(this.state.operations))
|
|
19933
|
+
if (record2.kind === "login" && now - Date.parse(record2.createdAt) > LOGIN_RECORD_MS)
|
|
19934
|
+
delete this.state.operations[key];
|
|
19935
|
+
this.state.operations[id] = {
|
|
19936
|
+
url: url2,
|
|
19937
|
+
token,
|
|
19938
|
+
kind: "login",
|
|
19939
|
+
// Never matched: every login is a new one, since a listener's port is.
|
|
19940
|
+
requestHash: createHash2("sha256").update(id).digest("hex"),
|
|
19941
|
+
accountId: null,
|
|
19942
|
+
createdAt: new Date(now).toISOString()
|
|
19943
|
+
};
|
|
19944
|
+
await this.save();
|
|
19945
|
+
}
|
|
19946
|
+
/**
|
|
19947
|
+
* Waits for the approval page to send the browser back with the redeem code,
|
|
19948
|
+
* and redeems it. Polls meanwhile, since a declined or expired login never
|
|
19949
|
+
* comes back to the listener.
|
|
19950
|
+
*/
|
|
19951
|
+
async collectLoopbackLogin(id, token, url2, capabilities, loopback) {
|
|
19952
|
+
let code;
|
|
19953
|
+
const arrived = loopback.code.then((value) => {
|
|
19954
|
+
code = value;
|
|
19955
|
+
});
|
|
19956
|
+
const end = Date.now() + LOGIN_TIMEOUT_SECONDS * 1e3;
|
|
19957
|
+
let approvedAt;
|
|
19958
|
+
while (code === void 0) {
|
|
19959
|
+
const { data: status2 } = await this.publicCall("pollCliOperation", {
|
|
19960
|
+
params: { id },
|
|
19961
|
+
key: token,
|
|
19962
|
+
url: url2
|
|
19963
|
+
});
|
|
19964
|
+
if (code !== void 0) break;
|
|
19965
|
+
if (status2.state === "expired") {
|
|
19966
|
+
await this.forget(id);
|
|
19967
|
+
expired(status2);
|
|
19968
|
+
}
|
|
19969
|
+
if (status2.state === "completed") {
|
|
19970
|
+
approvedAt ??= Date.now();
|
|
19971
|
+
if (Date.now() - approvedAt > LOOPBACK_GRACE_MS) {
|
|
19972
|
+
await this.forget(id);
|
|
19973
|
+
fail(
|
|
19974
|
+
"login_not_delivered",
|
|
19975
|
+
"The login was approved, but the browser never came back to this terminal.",
|
|
19976
|
+
"Run agw account login --no-open to approve it from another device, or run agw account login again here.",
|
|
19977
|
+
3,
|
|
19978
|
+
{ id }
|
|
19979
|
+
);
|
|
19980
|
+
}
|
|
19981
|
+
}
|
|
19982
|
+
if (Date.now() >= end) {
|
|
19983
|
+
await this.forget(id);
|
|
19984
|
+
fail(
|
|
19985
|
+
"wait_timeout",
|
|
19986
|
+
"The login was not approved in time.",
|
|
19987
|
+
"Run agw account login again.",
|
|
19988
|
+
5,
|
|
19989
|
+
{ id, state: status2.state, expiresAt: status2.expiresAt }
|
|
19990
|
+
);
|
|
19991
|
+
}
|
|
19992
|
+
await Promise.race([delay2(Math.min(1500, Math.max(0, end - Date.now()))), arrived]);
|
|
19993
|
+
}
|
|
19994
|
+
try {
|
|
19995
|
+
const { data } = await this.publicCall("redeemCliLogin", {
|
|
19996
|
+
params: { id },
|
|
19997
|
+
body: { redeemCode: code },
|
|
19998
|
+
key: token,
|
|
19999
|
+
url: url2
|
|
20000
|
+
});
|
|
20001
|
+
await this.select(url2, { ...data, deployment: capabilities.deployment });
|
|
20002
|
+
delete this.state.operations[id];
|
|
20003
|
+
await this.save();
|
|
20004
|
+
loopback.answer(true);
|
|
20005
|
+
} catch (error51) {
|
|
20006
|
+
loopback.answer(false);
|
|
20007
|
+
throw error51;
|
|
20008
|
+
}
|
|
20009
|
+
}
|
|
20010
|
+
/**
|
|
20011
|
+
* `account logout`: the deployment revokes the stored key, then this machine
|
|
20012
|
+
* forgets it. A key the deployment no longer accepts is already as good as
|
|
20013
|
+
* revoked; any other failure still removes it here, and says so, because a
|
|
20014
|
+
* machine must always be able to sign out.
|
|
20015
|
+
*/
|
|
20016
|
+
async logout() {
|
|
20017
|
+
const active = this.active;
|
|
20018
|
+
const key = active?.credential;
|
|
20019
|
+
let revoked = false;
|
|
20020
|
+
if (active && key) {
|
|
20021
|
+
try {
|
|
20022
|
+
await this.publicCall("revokeCliCredential", { key, url: active.url });
|
|
20023
|
+
revoked = true;
|
|
20024
|
+
} catch (error51) {
|
|
20025
|
+
const status2 = error51 instanceof CliError ? error51.details?.status : void 0;
|
|
20026
|
+
if (status2 !== 401)
|
|
20027
|
+
this.io.stderr.write(
|
|
20028
|
+
`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.
|
|
20029
|
+
`
|
|
20030
|
+
);
|
|
20031
|
+
}
|
|
20032
|
+
}
|
|
20033
|
+
if (active) {
|
|
20034
|
+
delete active.credential;
|
|
20035
|
+
active.authenticated = false;
|
|
20036
|
+
await this.save();
|
|
20037
|
+
}
|
|
20038
|
+
if (this.environmentKey)
|
|
20039
|
+
this.io.stderr.write(
|
|
20040
|
+
`${MANAGEMENT_KEY_ENV} is still set, so it stays the credential for every command; unset it to sign out completely.
|
|
20041
|
+
`
|
|
20042
|
+
);
|
|
20043
|
+
return { loggedOut: true, revoked };
|
|
20044
|
+
}
|
|
19432
20045
|
async poll(id) {
|
|
19433
20046
|
const record2 = this.state.operations[id];
|
|
19434
20047
|
if (!record2)
|
|
@@ -19438,7 +20051,7 @@ Operation: ${data.id}
|
|
|
19438
20051
|
"Resume it from the machine that initiated the operation.",
|
|
19439
20052
|
4
|
|
19440
20053
|
);
|
|
19441
|
-
if (this.active && this.active.url !== record2.url)
|
|
20054
|
+
if (this.active && this.active.url !== record2.url && record2.kind !== "login")
|
|
19442
20055
|
fail(
|
|
19443
20056
|
"operation_context",
|
|
19444
20057
|
"This operation belongs to another selected deployment.",
|
|
@@ -19458,6 +20071,7 @@ Operation: ${data.id}
|
|
|
19458
20071
|
url: record2.url
|
|
19459
20072
|
});
|
|
19460
20073
|
if (data.state === "expired") await this.forget(id);
|
|
20074
|
+
if (data.state === "completed" && record2.kind === "login") return this.polledLogin(id, record2, data);
|
|
19461
20075
|
if (data.state === "completed") {
|
|
19462
20076
|
if (record2.kind === "claim" && data.account && this.active?.account?.id === record2.accountId && data.account.id === record2.accountId) {
|
|
19463
20077
|
this.active.account = data.account;
|
|
@@ -19467,6 +20081,36 @@ Operation: ${data.id}
|
|
|
19467
20081
|
}
|
|
19468
20082
|
return data;
|
|
19469
20083
|
}
|
|
20084
|
+
/**
|
|
20085
|
+
* A completed login, as a poll first sees it: its key is stored the way a
|
|
20086
|
+
* pasted one is, and taken out of what the command prints. A login whose key
|
|
20087
|
+
* was already handed to another poll, or is only ever redeemed, cannot be
|
|
20088
|
+
* collected here.
|
|
20089
|
+
*/
|
|
20090
|
+
async polledLogin(id, record2, data) {
|
|
20091
|
+
const { credential, ...result } = data.result ?? {};
|
|
20092
|
+
if (credential) {
|
|
20093
|
+
if (!data.account)
|
|
20094
|
+
fail(
|
|
20095
|
+
"invalid_response",
|
|
20096
|
+
"The login did not say which account it was approved into.",
|
|
20097
|
+
"Run agw account login again.",
|
|
20098
|
+
3
|
|
20099
|
+
);
|
|
20100
|
+
await this.select(record2.url, { credential, account: data.account, deployment: data.deployment });
|
|
20101
|
+
} else if (!(this.active?.credential && this.active.url === record2.url && data.account && this.active.account?.id === data.account.id)) {
|
|
20102
|
+
await this.forget(id);
|
|
20103
|
+
fail(
|
|
20104
|
+
"login_not_delivered",
|
|
20105
|
+
"This login was approved, but its key can no longer be collected here.",
|
|
20106
|
+
"Run agw account login again.",
|
|
20107
|
+
3,
|
|
20108
|
+
{ id }
|
|
20109
|
+
);
|
|
20110
|
+
}
|
|
20111
|
+
this.delivered.add(id);
|
|
20112
|
+
return { ...data, result };
|
|
20113
|
+
}
|
|
19470
20114
|
async wait(id, timeout) {
|
|
19471
20115
|
const end = Date.now() + timeout * 1e3;
|
|
19472
20116
|
for (; ; ) {
|
|
@@ -19488,6 +20132,14 @@ Operation: ${data.id}
|
|
|
19488
20132
|
}
|
|
19489
20133
|
};
|
|
19490
20134
|
function expired(operation2) {
|
|
20135
|
+
if (operation2.denied)
|
|
20136
|
+
fail(
|
|
20137
|
+
"operation_denied",
|
|
20138
|
+
"The browser step was declined.",
|
|
20139
|
+
"Run the same command again to start a new one.",
|
|
20140
|
+
3,
|
|
20141
|
+
{ id: operation2.id, state: "denied", expiresAt: operation2.expiresAt }
|
|
20142
|
+
);
|
|
19491
20143
|
fail(
|
|
19492
20144
|
"operation_expired",
|
|
19493
20145
|
"The browser step expired before it was approved.",
|
|
@@ -19517,20 +20169,23 @@ async function storeKey(minted, output, appId) {
|
|
|
19517
20169
|
contentHash: createHash2("sha256").update(minted.key + "\n").digest("hex")
|
|
19518
20170
|
};
|
|
19519
20171
|
}
|
|
20172
|
+
function assertTrustedApprovalUrl(url2, capabilities, target) {
|
|
20173
|
+
const browserUrl = new URL(url2);
|
|
20174
|
+
const trustedOrigin = origin(
|
|
20175
|
+
capabilities.consoleOrigin ?? capabilities.deployment.consoleOrigin ?? target
|
|
20176
|
+
);
|
|
20177
|
+
if (browserUrl.origin !== trustedOrigin || browserUrl.username || browserUrl.password)
|
|
20178
|
+
fail("invalid_response", "The approval URL is not on this deployment\u2019s trusted console origin.");
|
|
20179
|
+
}
|
|
19520
20180
|
async function openBrowser(url2) {
|
|
19521
20181
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
19522
20182
|
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url2] : [url2];
|
|
19523
|
-
|
|
20183
|
+
return new Promise((resolve3) => {
|
|
19524
20184
|
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
|
-
});
|
|
20185
|
+
child.once("error", () => resolve3(false));
|
|
19531
20186
|
child.once("spawn", () => {
|
|
19532
20187
|
child.unref();
|
|
19533
|
-
resolve3();
|
|
20188
|
+
resolve3(true);
|
|
19534
20189
|
});
|
|
19535
20190
|
});
|
|
19536
20191
|
}
|
|
@@ -19721,7 +20376,7 @@ async function cachedRelease(version2, integrity, source) {
|
|
|
19721
20376
|
}
|
|
19722
20377
|
return directory;
|
|
19723
20378
|
}
|
|
19724
|
-
async function
|
|
20379
|
+
async function release2(requested = VERSION, base = dirname(fileURLToPath(import.meta.url)), source = {}) {
|
|
19725
20380
|
if (requested !== VERSION)
|
|
19726
20381
|
fail(
|
|
19727
20382
|
"unsupported_release",
|
|
@@ -19976,14 +20631,14 @@ var Cloudflare = class {
|
|
|
19976
20631
|
}
|
|
19977
20632
|
async all(path) {
|
|
19978
20633
|
const result = [];
|
|
19979
|
-
for (let
|
|
20634
|
+
for (let page2 = 1; page2 <= 1e4; page2++) {
|
|
19980
20635
|
const data = await this.request(
|
|
19981
|
-
path + (path.includes("?") ? "&" : "?") + "page=" +
|
|
20636
|
+
path + (path.includes("?") ? "&" : "?") + "page=" + page2 + "&per_page=50"
|
|
19982
20637
|
);
|
|
19983
20638
|
if (!data || !Array.isArray(data.result))
|
|
19984
20639
|
fail("cloudflare_error", "Expected a Cloudflare resource list.");
|
|
19985
20640
|
result.push(...data.result);
|
|
19986
|
-
if (data.result_info?.total_pages ?
|
|
20641
|
+
if (data.result_info?.total_pages ? page2 >= data.result_info.total_pages : data.result.length < 50)
|
|
19987
20642
|
return result;
|
|
19988
20643
|
}
|
|
19989
20644
|
fail("cloudflare_error", "Cloudflare pagination exceeded its safety bound.");
|
|
@@ -20304,7 +20959,7 @@ async function matchExisting(ctx, cf, flags, target) {
|
|
|
20304
20959
|
}
|
|
20305
20960
|
|
|
20306
20961
|
// src/deployment.ts
|
|
20307
|
-
function
|
|
20962
|
+
function hostname5(value) {
|
|
20308
20963
|
const u = origin("https://" + value);
|
|
20309
20964
|
if (new URL(u).hostname !== value || !value.includes(".") || value.includes(":"))
|
|
20310
20965
|
fail("invalid_input", "Supply a hostname without protocol, path or port.");
|
|
@@ -20317,10 +20972,10 @@ async function authorized(flags, cf, loadRelease) {
|
|
|
20317
20972
|
await cf.authenticate(flags);
|
|
20318
20973
|
return artifact;
|
|
20319
20974
|
}
|
|
20320
|
-
async function deploymentDomain(ctx, flags, cf = new Cloudflare(), loadRelease =
|
|
20975
|
+
async function deploymentDomain(ctx, flags, cf = new Cloudflare(), loadRelease = release2) {
|
|
20321
20976
|
const artifact = await authorized(flags, cf, loadRelease);
|
|
20322
20977
|
const journal = await matchExisting(ctx, cf, flags, selectedInstallation(ctx));
|
|
20323
|
-
const domain2 =
|
|
20978
|
+
const domain2 = hostname5(flags.hostname ?? await prompt("Custom hostname", flags));
|
|
20324
20979
|
if (!flags["dry-run"])
|
|
20325
20980
|
await confirm(
|
|
20326
20981
|
`Attach ${domain2} to ${journal.name} in Cloudflare account ${journal.accountId}?`,
|
|
@@ -20328,7 +20983,7 @@ async function deploymentDomain(ctx, flags, cf = new Cloudflare(), loadRelease =
|
|
|
20328
20983
|
);
|
|
20329
20984
|
return attachDomain(ctx, cf, journal, domain2, artifact, Boolean(flags["dry-run"]));
|
|
20330
20985
|
}
|
|
20331
|
-
async function deploymentUpdate(ctx, flags, cf = new Cloudflare(), loadRelease =
|
|
20986
|
+
async function deploymentUpdate(ctx, flags, cf = new Cloudflare(), loadRelease = release2) {
|
|
20332
20987
|
const artifact = await authorized(flags, cf, loadRelease);
|
|
20333
20988
|
const journal = await matchExisting(ctx, cf, flags, selectedInstallation(ctx));
|
|
20334
20989
|
if (!artifact.manifest.upgradeFrom.includes(journal.version))
|
|
@@ -20459,12 +21114,12 @@ async function install(ctx, cf, journal, artifact, url2, domain2) {
|
|
|
20459
21114
|
await advance(ctx, journal, journal.phase, { pendingDomain: void 0 });
|
|
20460
21115
|
}
|
|
20461
21116
|
}
|
|
20462
|
-
async function deploymentSetup(ctx, flags, cf = new Cloudflare(), loadRelease =
|
|
21117
|
+
async function deploymentSetup(ctx, flags, cf = new Cloudflare(), loadRelease = release2) {
|
|
20463
21118
|
const artifact = await authorized(flags, cf, loadRelease);
|
|
20464
21119
|
const name = flags.name ?? await prompt("Worker name", flags, { defaultValue: "app-ai-gateway" });
|
|
20465
21120
|
if (!/^[a-z][a-z0-9-]{0,62}$/.test(name))
|
|
20466
21121
|
fail("invalid_input", "Worker name must be a lowercase slug of at most 63 characters.");
|
|
20467
|
-
const domain2 = flags.domain ?
|
|
21122
|
+
const domain2 = flags.domain ? hostname5(flags.domain) : void 0;
|
|
20468
21123
|
const accountId = await cf.account(flags);
|
|
20469
21124
|
const workers = await cf.all("/accounts/" + accountId + "/workers/scripts");
|
|
20470
21125
|
const installations = ctx.state.installations ??= {};
|
|
@@ -20537,7 +21192,7 @@ async function deploymentSetup(ctx, flags, cf = new Cloudflare(), loadRelease =
|
|
|
20537
21192
|
throw error51;
|
|
20538
21193
|
}
|
|
20539
21194
|
}
|
|
20540
|
-
async function deploymentCommand(ctx, command, flags, cf = new Cloudflare(), loadRelease =
|
|
21195
|
+
async function deploymentCommand(ctx, command, flags, cf = new Cloudflare(), loadRelease = release2) {
|
|
20541
21196
|
if (command === "deployment setup") return deploymentSetup(ctx, flags, cf, loadRelease);
|
|
20542
21197
|
if (command === "deployment domain") return deploymentDomain(ctx, flags, cf, loadRelease);
|
|
20543
21198
|
if (command === "deployment update") return deploymentUpdate(ctx, flags, cf, loadRelease);
|
|
@@ -20576,64 +21231,6 @@ var CliErrorDetailsSchema = external_exports.object({
|
|
|
20576
21231
|
apiErrors: external_exports.string().optional()
|
|
20577
21232
|
});
|
|
20578
21233
|
|
|
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
21234
|
// src/human.ts
|
|
20638
21235
|
function kv(entries, style2) {
|
|
20639
21236
|
const rows = [];
|
|
@@ -20675,8 +21272,13 @@ function render(command, result, context, style2) {
|
|
|
20675
21272
|
return [
|
|
20676
21273
|
style2.headline(`Waiting for browser approval (${result.id}).`),
|
|
20677
21274
|
result.url,
|
|
21275
|
+
// A login's page shows the same code; checking it is what tells a person
|
|
21276
|
+
// the request they are approving is this one.
|
|
21277
|
+
..."userCode" in result && result.userCode ? [`Pairing code: ${result.userCode} (check that the browser shows the same code)`] : [],
|
|
20678
21278
|
`Resume: agw operation wait ${result.id}`
|
|
20679
21279
|
];
|
|
21280
|
+
if ((command === "operation wait" || command === "operation status") && "kind" in result && result.kind === "login" && result.state === "completed")
|
|
21281
|
+
return [style2.headline(`Signed in to ${context.url}.`), ...operation(result, style2)];
|
|
20680
21282
|
switch (command) {
|
|
20681
21283
|
case "account status":
|
|
20682
21284
|
case "account login":
|
|
@@ -20858,7 +21460,7 @@ function show(command, result, style2) {
|
|
|
20858
21460
|
function mutation(command, result, context, style2) {
|
|
20859
21461
|
if ("loggedOut" in result)
|
|
20860
21462
|
return [
|
|
20861
|
-
style2.headline(`Signed out of ${context.url}.`) + " The stored credential was removed."
|
|
21463
|
+
style2.headline(`Signed out of ${context.url}.`) + (result.revoked ? " The key was revoked and removed from this machine." : " The stored credential was removed.")
|
|
20862
21464
|
];
|
|
20863
21465
|
if ("deleted" in result) {
|
|
20864
21466
|
if ("provider_id" in result)
|
|
@@ -21315,27 +21917,23 @@ async function execute(parsed, ctx) {
|
|
|
21315
21917
|
if (command.startsWith("operation "))
|
|
21316
21918
|
return command === "operation wait" ? ctx.wait(args[0] ?? "", positive(flags.timeout ?? 300)) : ctx.poll(args[0] ?? "");
|
|
21317
21919
|
if (command === "account status") return (await ctx.call("getCliAccount")).data;
|
|
21318
|
-
if (command === "account login")
|
|
21920
|
+
if (command === "account login")
|
|
21921
|
+
return flags["key-stdin"] || flags["key-prompt"] ? ctx.keyLogin() : ctx.browserLogin();
|
|
21319
21922
|
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
|
-
}
|
|
21923
|
+
if (command === "account logout") return ctx.logout();
|
|
21328
21924
|
if (command === "deployment connect") {
|
|
21329
21925
|
const url2 = flags.cloud ? CLOUD : origin(
|
|
21330
21926
|
flags.url ?? await required2(flags, "url", "Deployment HTTPS URL")
|
|
21331
21927
|
);
|
|
21332
|
-
return ctx.
|
|
21928
|
+
if (flags["key-stdin"] || flags["key-prompt"]) return ctx.keyLogin(url2);
|
|
21929
|
+
if (ctx.environmentKey) return ctx.connectWithEnvironment(url2);
|
|
21930
|
+
return ctx.browserLogin(url2);
|
|
21333
21931
|
}
|
|
21334
21932
|
if (command === "deployment status") {
|
|
21335
21933
|
const { data } = await ctx.publicCall("getCliCapabilities");
|
|
21336
21934
|
return {
|
|
21337
21935
|
url: ctx.url,
|
|
21338
|
-
authenticated: Boolean(ctx.
|
|
21936
|
+
authenticated: Boolean(ctx.managementKey),
|
|
21339
21937
|
...data,
|
|
21340
21938
|
connected: true
|
|
21341
21939
|
};
|
|
@@ -21346,7 +21944,8 @@ async function main(argv = process.argv.slice(2), {
|
|
|
21346
21944
|
store = new StateStore(),
|
|
21347
21945
|
transport = new Transport(),
|
|
21348
21946
|
stdout = process.stdout,
|
|
21349
|
-
stderr: stderr2 = process.stderr
|
|
21947
|
+
stderr: stderr2 = process.stderr,
|
|
21948
|
+
io = {}
|
|
21350
21949
|
} = {}) {
|
|
21351
21950
|
const json2 = argv.includes("--json");
|
|
21352
21951
|
try {
|
|
@@ -21361,7 +21960,7 @@ async function main(argv = process.argv.slice(2), {
|
|
|
21361
21960
|
}
|
|
21362
21961
|
const run = async () => {
|
|
21363
21962
|
const state = await store.read((line) => stderr2.write(line + "\n"));
|
|
21364
|
-
const ctx = new Context(store, state, transport, parsed.flags);
|
|
21963
|
+
const ctx = new Context(store, state, transport, parsed.flags, { ...io, stderr: stderr2 });
|
|
21365
21964
|
const result = await execute(parsed, ctx);
|
|
21366
21965
|
const context = {
|
|
21367
21966
|
url: ctx.url,
|