@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.
- package/README.md +17 -2
- package/dist/agw.mjs +751 -149
- 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.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(
|
|
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)) {
|
|
@@ -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
|
|
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: --
|
|
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,
|
|
@@ -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
|
|
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
|
|
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:
|
|
17989
|
+
var CliBrowserProofSchema = external_exports.object({ submissionToken: CliSubmissionTokenSchema }).strict();
|
|
17974
17990
|
var CliBrowserSubmitRequestSchema = external_exports.object({
|
|
17975
|
-
submissionToken:
|
|
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:
|
|
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
|
|
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
|
|
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: ["
|
|
18327
|
-
summary: "Call a server-configured
|
|
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
|
-
|
|
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.
|
|
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) ?
|
|
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.
|
|
19630
|
+
if (!this.managementKey)
|
|
19251
19631
|
fail(
|
|
19252
19632
|
"login_required",
|
|
19253
19633
|
"This operation requires account management access.",
|
|
19254
|
-
|
|
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.
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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.
|
|
19301
|
-
fail(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
20637
|
+
for (let page2 = 1; page2 <= 1e4; page2++) {
|
|
19980
20638
|
const data = await this.request(
|
|
19981
|
-
path + (path.includes("?") ? "&" : "?") + "page=" +
|
|
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 ?
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
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")
|
|
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.
|
|
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.
|
|
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,
|