letthemquibble 1.2.0 → 2.0.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 +9 -0
- package/dist/quibble.js +426 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,13 @@ quibble list
|
|
|
50
50
|
quibble summary
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
`quibble login` opens the browser, where you check the code and approve the
|
|
54
|
+
login. It needs no terminal, so a coding agent can start it too. Over SSH or
|
|
55
|
+
in WSL, open the printed URL on any device where you are signed in, or pass
|
|
56
|
+
`--no-browser` to print it alone. `quibble login --password` asks for the
|
|
57
|
+
email, the password and, when two-factor sign-in is on, a code in the
|
|
58
|
+
terminal instead.
|
|
59
|
+
|
|
53
60
|
`quibble init` writes a `.quibblerc` and prints a short section for your
|
|
54
61
|
`CLAUDE.md` or `AGENTS.md`. It tells the agent when to complain and how, for
|
|
55
62
|
example:
|
|
@@ -98,6 +105,7 @@ once.
|
|
|
98
105
|
|
|
99
106
|
For CI, a maintainer runs `quibble token create ci`, and the job sets the two
|
|
100
107
|
lines it prints, `QUIBBLE_ENDPOINT` and `QUIBBLE_TOKEN`, instead of logging in.
|
|
108
|
+
A plain `quibble login` refuses to run when `CI` is set.
|
|
101
109
|
A complaint that cannot reach the API waits in `~/.quibble/unsent.jsonl` and
|
|
102
110
|
goes out with the next remote command, or with `quibble retry`.
|
|
103
111
|
|
|
@@ -105,5 +113,6 @@ Run `quibble --help` for every command.
|
|
|
105
113
|
|
|
106
114
|
## More
|
|
107
115
|
|
|
116
|
+
- <https://letthemquibble.com/help>: install, update, setup and login
|
|
108
117
|
- <https://github.com/repineap/quibble>
|
|
109
118
|
- <https://letthemquibble.com>
|
package/dist/quibble.js
CHANGED
|
@@ -6303,9 +6303,9 @@ var asciiTabOrNewline = /[\t\n\r]/g;
|
|
|
6303
6303
|
function stripTabAndNewline(value) {
|
|
6304
6304
|
return value.replace(asciiTabOrNewline, "");
|
|
6305
6305
|
}
|
|
6306
|
-
function urlHostnameOk(url2,
|
|
6307
|
-
|
|
6308
|
-
return
|
|
6306
|
+
function urlHostnameOk(url2, hostname5) {
|
|
6307
|
+
hostname5.lastIndex = 0;
|
|
6308
|
+
return hostname5.test(url2.hostname);
|
|
6309
6309
|
}
|
|
6310
6310
|
function urlProtocolOk(url2, protocol) {
|
|
6311
6311
|
protocol.lastIndex = 0;
|
|
@@ -23233,6 +23233,14 @@ var ACT_AS_HEADER = "x-quibble-act-as";
|
|
|
23233
23233
|
var TRUNCATED_HEADER = "x-quibble-truncated";
|
|
23234
23234
|
var IDEMPOTENCY_HEADER = "idempotency-key";
|
|
23235
23235
|
var IDEMPOTENCY_KEY_MAX = 128;
|
|
23236
|
+
var CLI_LOGIN_TTL_SECONDS = 600;
|
|
23237
|
+
var CLI_LOGIN_POLL_SECONDS = 3;
|
|
23238
|
+
var CLI_LOGIN_CODE_ALPHABET = "BCDFGHJKMNPQRSTVWXZ";
|
|
23239
|
+
var CLI_LOGIN_CODE_LENGTH = 8;
|
|
23240
|
+
var CLI_LOGIN_HOST_MAX = 64;
|
|
23241
|
+
var CLI_LOGIN_OS_MAX = 32;
|
|
23242
|
+
var CLI_LOGIN_VERSION_MAX = 32;
|
|
23243
|
+
var CLI_LOGIN_PAGE_PATH = "/cli-login";
|
|
23236
23244
|
|
|
23237
23245
|
// ../core/dist/errors.js
|
|
23238
23246
|
var QuibbleError = class extends Error {
|
|
@@ -23287,6 +23295,35 @@ function describeError(error62) {
|
|
|
23287
23295
|
return error62 instanceof Error ? error62.message : String(error62);
|
|
23288
23296
|
}
|
|
23289
23297
|
|
|
23298
|
+
// ../core/dist/cli-login.js
|
|
23299
|
+
var CLI_LOGIN_CONTROL_RE = /[\u0000-\u001F\u007F-\u009F\p{Cf}]/u;
|
|
23300
|
+
var CONTROL_GLOBAL_RE = /[\u0000-\u001F\u007F-\u009F\p{Cf}]/gu;
|
|
23301
|
+
var CLI_LOGIN_KEY_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
23302
|
+
var CLI_LOGIN_CODE_MESSAGE = "code: not a login code";
|
|
23303
|
+
var HALF = CLI_LOGIN_CODE_LENGTH / 2;
|
|
23304
|
+
var CODE_RE = new RegExp(`^([${CLI_LOGIN_CODE_ALPHABET}]{${HALF}})-?([${CLI_LOGIN_CODE_ALPHABET}]{${HALF}})$`);
|
|
23305
|
+
var UNKNOWN_HOST = "unknown";
|
|
23306
|
+
function parseCliLoginCode(value) {
|
|
23307
|
+
const match = CODE_RE.exec(value.trim().toUpperCase());
|
|
23308
|
+
if (match === null)
|
|
23309
|
+
throw new ValidationError([CLI_LOGIN_CODE_MESSAGE]);
|
|
23310
|
+
return `${match[1] ?? ""}-${match[2] ?? ""}`;
|
|
23311
|
+
}
|
|
23312
|
+
function cliLoginUrl(webUrl, code2) {
|
|
23313
|
+
return `${webUrl.replace(/\/+$/, "")}${CLI_LOGIN_PAGE_PATH}?code=${code2}`;
|
|
23314
|
+
}
|
|
23315
|
+
function cut(value, max) {
|
|
23316
|
+
if (value.length <= max)
|
|
23317
|
+
return value;
|
|
23318
|
+
const head = value.slice(0, max);
|
|
23319
|
+
const last = head.charCodeAt(head.length - 1);
|
|
23320
|
+
return last >= 55296 && last <= 56319 ? head.slice(0, -1) : head;
|
|
23321
|
+
}
|
|
23322
|
+
function cliLoginHost(raw) {
|
|
23323
|
+
const host = cut(raw.replace(CONTROL_GLOBAL_RE, "").trim(), CLI_LOGIN_HOST_MAX).trim();
|
|
23324
|
+
return host === "" ? UNKNOWN_HOST : host;
|
|
23325
|
+
}
|
|
23326
|
+
|
|
23290
23327
|
// ../core/dist/fingerprint.js
|
|
23291
23328
|
function headlineOf(message) {
|
|
23292
23329
|
for (const line of message.split("\n")) {
|
|
@@ -23307,7 +23344,7 @@ var PUNCTUATION_RE = /[^\p{L}\p{N}\s<>]/gu;
|
|
|
23307
23344
|
function isHexId(run2) {
|
|
23308
23345
|
return /[a-f]/.test(run2) && /[0-9]/.test(run2);
|
|
23309
23346
|
}
|
|
23310
|
-
function
|
|
23347
|
+
function cut2(text, max) {
|
|
23311
23348
|
if (text.length <= max)
|
|
23312
23349
|
return text;
|
|
23313
23350
|
const head = text.slice(0, max);
|
|
@@ -23325,7 +23362,7 @@ function fingerprintOf(message) {
|
|
|
23325
23362
|
text = text.replace(NUMBER_RE, "<n>");
|
|
23326
23363
|
text = text.replace(PUNCTUATION_RE, " ");
|
|
23327
23364
|
text = text.replace(/\s+/gu, " ").trim();
|
|
23328
|
-
text =
|
|
23365
|
+
text = cut2(text, FINGERPRINT_MAX).trim();
|
|
23329
23366
|
return text === "" ? EMPTY_FINGERPRINT : text;
|
|
23330
23367
|
}
|
|
23331
23368
|
function groupComplaints(complaints) {
|
|
@@ -23906,6 +23943,23 @@ function newTokenSchema(now) {
|
|
|
23906
23943
|
...input2.expiresAt === void 0 ? {} : { expiresAt: input2.expiresAt }
|
|
23907
23944
|
}));
|
|
23908
23945
|
}
|
|
23946
|
+
function reportedField(max) {
|
|
23947
|
+
const message = lengthMessage(1, max);
|
|
23948
|
+
return external_exports.string({ message }).trim().min(1, { message }).max(max, { message }).refine((value) => !CLI_LOGIN_CONTROL_RE.test(value), { message: CONTROL_MESSAGE });
|
|
23949
|
+
}
|
|
23950
|
+
var newCliLoginSchema = external_exports.strictObject({
|
|
23951
|
+
host: reportedField(CLI_LOGIN_HOST_MAX),
|
|
23952
|
+
os: reportedField(CLI_LOGIN_OS_MAX),
|
|
23953
|
+
version: reportedField(CLI_LOGIN_VERSION_MAX)
|
|
23954
|
+
}).transform((input2) => ({
|
|
23955
|
+
host: input2.host,
|
|
23956
|
+
os: input2.os,
|
|
23957
|
+
version: input2.version
|
|
23958
|
+
}));
|
|
23959
|
+
var LOGIN_KEY_MESSAGE = "must be 43 base64url characters";
|
|
23960
|
+
var cliLoginPollSchema = external_exports.strictObject({
|
|
23961
|
+
loginKey: external_exports.string({ message: LOGIN_KEY_MESSAGE }).regex(CLI_LOGIN_KEY_RE, { message: LOGIN_KEY_MESSAGE })
|
|
23962
|
+
}).transform((input2) => ({ loginKey: input2.loginKey }));
|
|
23909
23963
|
var UNKNOWN_KEY_MESSAGE = "unknown key";
|
|
23910
23964
|
var emptyBodySchema = external_exports.strictObject({});
|
|
23911
23965
|
function formatIssues(error62) {
|
|
@@ -24756,6 +24810,7 @@ var TOO_MANY_LINE = "Too many attempts. Try again later.";
|
|
|
24756
24810
|
var WRONG_CODE_LINE = "Wrong code.";
|
|
24757
24811
|
var EXPIRED_CODE_LINE = "That code has expired. Request a new one.";
|
|
24758
24812
|
var PASSWORD_POLICY_LINE = "Use at least 12 characters, with an uppercase letter, a lowercase letter, a number and a symbol.";
|
|
24813
|
+
var BROWSER_REFUSED_LINE = "the browser approval could not be used: run quibble login again";
|
|
24759
24814
|
var UNREACHABLE_TYPE = "Unreachable";
|
|
24760
24815
|
var INCORRECT_CREDENTIALS_MESSAGE = "Incorrect username or password.";
|
|
24761
24816
|
var CognitoError = class extends RemoteError {
|
|
@@ -24772,14 +24827,15 @@ var CognitoError = class extends RemoteError {
|
|
|
24772
24827
|
function endpointUrl(region) {
|
|
24773
24828
|
return `https://cognito-idp.${region}.amazonaws.com/`;
|
|
24774
24829
|
}
|
|
24775
|
-
function errorLineOf(type, detail2,
|
|
24830
|
+
function errorLineOf(type, detail2, step) {
|
|
24776
24831
|
switch (type) {
|
|
24777
24832
|
case UNREACHABLE_TYPE:
|
|
24778
24833
|
return UNREACHABLE_LINE;
|
|
24779
24834
|
case "UserNotFoundException":
|
|
24780
|
-
return
|
|
24835
|
+
return step === "sign-in" ? WRONG_CREDENTIALS_LINE : detail2 ?? type;
|
|
24781
24836
|
case "NotAuthorizedException":
|
|
24782
|
-
|
|
24837
|
+
if (step === "browser") return BROWSER_REFUSED_LINE;
|
|
24838
|
+
return step === "sign-in" && detail2 === INCORRECT_CREDENTIALS_MESSAGE ? WRONG_CREDENTIALS_LINE : detail2 ?? type;
|
|
24783
24839
|
case "LimitExceededException":
|
|
24784
24840
|
case "TooManyRequestsException":
|
|
24785
24841
|
case "TooManyFailedAttemptsException":
|
|
@@ -24800,7 +24856,7 @@ function typeOf(value) {
|
|
|
24800
24856
|
const hash2 = value.indexOf("#");
|
|
24801
24857
|
return hash2 === -1 ? value : value.slice(hash2 + 1);
|
|
24802
24858
|
}
|
|
24803
|
-
async function call(endpoint, operation, body,
|
|
24859
|
+
async function call(endpoint, operation, body, step = "other") {
|
|
24804
24860
|
const url2 = endpointUrl(endpoint.region);
|
|
24805
24861
|
const send2 = endpoint.fetch ?? globalThis.fetch;
|
|
24806
24862
|
const site = {
|
|
@@ -24812,7 +24868,7 @@ async function call(endpoint, operation, body, signIn2 = false) {
|
|
|
24812
24868
|
throw new CognitoError(
|
|
24813
24869
|
type,
|
|
24814
24870
|
detail2,
|
|
24815
|
-
errorLineOf(type, detail2,
|
|
24871
|
+
errorLineOf(type, detail2, step),
|
|
24816
24872
|
detail(site, status)
|
|
24817
24873
|
);
|
|
24818
24874
|
};
|
|
@@ -24895,14 +24951,27 @@ function authAnswerOf(body) {
|
|
|
24895
24951
|
}
|
|
24896
24952
|
};
|
|
24897
24953
|
}
|
|
24954
|
+
function flowOf(input2) {
|
|
24955
|
+
switch (input2.flow) {
|
|
24956
|
+
case "USER_PASSWORD_AUTH":
|
|
24957
|
+
return {
|
|
24958
|
+
parameters: { USERNAME: input2.username, PASSWORD: input2.password },
|
|
24959
|
+
step: "sign-in"
|
|
24960
|
+
};
|
|
24961
|
+
case "REFRESH_TOKEN_AUTH":
|
|
24962
|
+
return { parameters: { REFRESH_TOKEN: input2.refreshToken }, step: "other" };
|
|
24963
|
+
case "CUSTOM_AUTH":
|
|
24964
|
+
return { parameters: { USERNAME: input2.username }, step: "browser" };
|
|
24965
|
+
}
|
|
24966
|
+
}
|
|
24898
24967
|
async function initiateAuth(endpoint, input2) {
|
|
24899
|
-
const
|
|
24968
|
+
const { parameters, step } = flowOf(input2);
|
|
24900
24969
|
return authAnswerOf(
|
|
24901
24970
|
await call(
|
|
24902
24971
|
endpoint,
|
|
24903
24972
|
"InitiateAuth",
|
|
24904
24973
|
{ ClientId: input2.clientId, AuthFlow: input2.flow, AuthParameters: parameters },
|
|
24905
|
-
|
|
24974
|
+
step
|
|
24906
24975
|
)
|
|
24907
24976
|
);
|
|
24908
24977
|
}
|
|
@@ -24917,6 +24986,22 @@ async function respondToAuthChallenge(endpoint, input2) {
|
|
|
24917
24986
|
})
|
|
24918
24987
|
);
|
|
24919
24988
|
}
|
|
24989
|
+
async function respondToCustomChallenge(endpoint, input2) {
|
|
24990
|
+
const username = input2.challenge.challengeParameters["USERNAME"] ?? input2.username;
|
|
24991
|
+
return authAnswerOf(
|
|
24992
|
+
await call(
|
|
24993
|
+
endpoint,
|
|
24994
|
+
"RespondToAuthChallenge",
|
|
24995
|
+
{
|
|
24996
|
+
ClientId: input2.clientId,
|
|
24997
|
+
ChallengeName: input2.challenge.challengeName,
|
|
24998
|
+
Session: input2.challenge.session,
|
|
24999
|
+
ChallengeResponses: { USERNAME: username, ANSWER: input2.answer }
|
|
25000
|
+
},
|
|
25001
|
+
"browser"
|
|
25002
|
+
)
|
|
25003
|
+
);
|
|
25004
|
+
}
|
|
24920
25005
|
async function revokeToken(endpoint, input2) {
|
|
24921
25006
|
await call(endpoint, "RevokeToken", { ClientId: input2.clientId, Token: input2.token });
|
|
24922
25007
|
}
|
|
@@ -27088,12 +27173,12 @@ var REPORTER_WIDTH = 20;
|
|
|
27088
27173
|
var TAGS_WIDTH = 24;
|
|
27089
27174
|
var SUMMARY_MAX = 60;
|
|
27090
27175
|
var RECURRED_MARK = "\u21BB";
|
|
27091
|
-
function
|
|
27176
|
+
function cut3(text, width) {
|
|
27092
27177
|
if (text.length <= width) return text;
|
|
27093
27178
|
return `${text.slice(0, width - 3)}...`;
|
|
27094
27179
|
}
|
|
27095
27180
|
function summaryOf(complaint) {
|
|
27096
|
-
return
|
|
27181
|
+
return cut3(printable(summarize(complaint.message)), SUMMARY_MAX);
|
|
27097
27182
|
}
|
|
27098
27183
|
function limitNote(count, limit, more) {
|
|
27099
27184
|
if (limit >= MAX_LIMIT) {
|
|
@@ -27147,7 +27232,7 @@ async function runList(options, context, io) {
|
|
|
27147
27232
|
return 0;
|
|
27148
27233
|
}
|
|
27149
27234
|
function reporterOf(complaint) {
|
|
27150
|
-
return
|
|
27235
|
+
return cut3(printable(complaint.reporterName ?? complaint.reporter), REPORTER_WIDTH);
|
|
27151
27236
|
}
|
|
27152
27237
|
function groupCell(group) {
|
|
27153
27238
|
const count = group.count > 1 ? `\xD7${group.count}` : "";
|
|
@@ -27164,7 +27249,7 @@ function formatTable(rows2) {
|
|
|
27164
27249
|
status: complaint.status,
|
|
27165
27250
|
project: complaint.project,
|
|
27166
27251
|
reporter: reporterOf(complaint),
|
|
27167
|
-
tags:
|
|
27252
|
+
tags: cut3(printable(complaint.tags.join(",")), TAGS_WIDTH),
|
|
27168
27253
|
group: group === void 0 ? "" : groupCell(group),
|
|
27169
27254
|
summary: summaryOf(complaint)
|
|
27170
27255
|
};
|
|
@@ -27221,6 +27306,81 @@ function formatOrgs(orgs) {
|
|
|
27221
27306
|
return [format(COLUMNS2), ...rows2.map(format)];
|
|
27222
27307
|
}
|
|
27223
27308
|
|
|
27309
|
+
// src/commands/login.ts
|
|
27310
|
+
import { hostname as hostname4 } from "node:os";
|
|
27311
|
+
|
|
27312
|
+
// src/opener.ts
|
|
27313
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
27314
|
+
var OPEN_TIMEOUT_MS = 5e3;
|
|
27315
|
+
var OPENABLE_URL_RE = /^https?:\/\/[A-Za-z0-9.:[\]-]+(?:\/[A-Za-z0-9/?=._~-]*)?$/;
|
|
27316
|
+
function isWsl(platform, env) {
|
|
27317
|
+
if (platform !== "linux") return false;
|
|
27318
|
+
const set2 = (name) => (env[name] ?? "") !== "";
|
|
27319
|
+
return set2("WSL_DISTRO_NAME") || set2("WSL_INTEROP");
|
|
27320
|
+
}
|
|
27321
|
+
function openCommands(url2, platform, env) {
|
|
27322
|
+
if (platform === "darwin") return [{ command: "open", args: [url2] }];
|
|
27323
|
+
if (platform === "win32") {
|
|
27324
|
+
return [{ command: "cmd", args: ["/c", "start", "", url2] }];
|
|
27325
|
+
}
|
|
27326
|
+
if (isWsl(platform, env)) {
|
|
27327
|
+
return [
|
|
27328
|
+
{ command: "wslview", args: [url2] },
|
|
27329
|
+
{ command: "cmd.exe", args: ["/c", "start", "", url2] }
|
|
27330
|
+
];
|
|
27331
|
+
}
|
|
27332
|
+
return [{ command: "xdg-open", args: [url2] }];
|
|
27333
|
+
}
|
|
27334
|
+
function attempt(spawn, entry, timeoutMs) {
|
|
27335
|
+
return new Promise((resolve4) => {
|
|
27336
|
+
let settled = false;
|
|
27337
|
+
const settle2 = (opened) => {
|
|
27338
|
+
if (settled) return;
|
|
27339
|
+
settled = true;
|
|
27340
|
+
clearTimeout(timer);
|
|
27341
|
+
resolve4(opened);
|
|
27342
|
+
};
|
|
27343
|
+
const timer = setTimeout(() => {
|
|
27344
|
+
settle2(false);
|
|
27345
|
+
}, timeoutMs);
|
|
27346
|
+
timer.unref();
|
|
27347
|
+
let child;
|
|
27348
|
+
try {
|
|
27349
|
+
child = spawn(entry.command, entry.args, {
|
|
27350
|
+
shell: false,
|
|
27351
|
+
detached: true,
|
|
27352
|
+
stdio: "ignore",
|
|
27353
|
+
windowsHide: true
|
|
27354
|
+
});
|
|
27355
|
+
} catch {
|
|
27356
|
+
settle2(false);
|
|
27357
|
+
return;
|
|
27358
|
+
}
|
|
27359
|
+
child.on("error", () => {
|
|
27360
|
+
settle2(false);
|
|
27361
|
+
});
|
|
27362
|
+
child.on("exit", (code2) => {
|
|
27363
|
+
settle2(code2 === 0);
|
|
27364
|
+
});
|
|
27365
|
+
child.unref();
|
|
27366
|
+
});
|
|
27367
|
+
}
|
|
27368
|
+
function platformOpener(options = {}) {
|
|
27369
|
+
const platform = options.platform ?? process.platform;
|
|
27370
|
+
const env = options.env ?? process.env;
|
|
27371
|
+
const spawn = options.spawn ?? nodeSpawn;
|
|
27372
|
+
const timeoutMs = options.timeoutMs ?? OPEN_TIMEOUT_MS;
|
|
27373
|
+
return {
|
|
27374
|
+
async open(url2) {
|
|
27375
|
+
if (!OPENABLE_URL_RE.test(url2)) return false;
|
|
27376
|
+
for (const entry of openCommands(url2, platform, env)) {
|
|
27377
|
+
if (await attempt(spawn, entry, timeoutMs)) return true;
|
|
27378
|
+
}
|
|
27379
|
+
return false;
|
|
27380
|
+
}
|
|
27381
|
+
};
|
|
27382
|
+
}
|
|
27383
|
+
|
|
27224
27384
|
// src/terminal.ts
|
|
27225
27385
|
import { createInterface } from "node:readline";
|
|
27226
27386
|
import { Writable } from "node:stream";
|
|
@@ -27342,15 +27502,40 @@ function nodeTerminal(input2, output2) {
|
|
|
27342
27502
|
|
|
27343
27503
|
// src/commands/login.ts
|
|
27344
27504
|
var LOCAL_IDENTITY_MESSAGE = "local mode: no identity.";
|
|
27345
|
-
var NEEDS_TERMINAL_MESSAGE = "quibble login needs a terminal: in CI, set QUIBBLE_TOKEN instead";
|
|
27505
|
+
var NEEDS_TERMINAL_MESSAGE = "quibble login --password needs a terminal: in CI, set QUIBBLE_TOKEN instead";
|
|
27506
|
+
var CI_MESSAGE = "quibble login needs a person to approve it in a browser: in CI, set QUIBBLE_TOKEN instead";
|
|
27507
|
+
var NO_BROWSER_MESSAGE = "--no-browser applies to the browser login only";
|
|
27508
|
+
var DENIED_MESSAGE = "login was denied in the browser";
|
|
27509
|
+
var EXPIRED_MESSAGE = "the login request expired: run quibble login again";
|
|
27510
|
+
var MALFORMED_MESSAGE = "the API answered a malformed login request";
|
|
27511
|
+
var UNKNOWN_STATUS_MESSAGE = "the API answered an unknown login status";
|
|
27512
|
+
var MFA_NEEDS_TERMINAL_MESSAGE = "this account needs a two-factor code: run quibble login in a terminal";
|
|
27346
27513
|
var MFA_CHALLENGE = "SOFTWARE_TOKEN_MFA";
|
|
27514
|
+
var CUSTOM_CHALLENGE = "CUSTOM_CHALLENGE";
|
|
27347
27515
|
var NEW_PASSWORD_CHALLENGE = "NEW_PASSWORD_REQUIRED";
|
|
27516
|
+
var CLI_LOGINS_PATH = "/v1/cli-logins";
|
|
27517
|
+
var CLI_LOGIN_POLL_PATH = "/v1/cli-logins/poll";
|
|
27518
|
+
var MIN_INTERVAL_SECONDS = 1;
|
|
27519
|
+
var MAX_INTERVAL_SECONDS = 30;
|
|
27520
|
+
var PLAIN_HOST_RE = /^(?:[a-z0-9.-]+|\[[0-9a-f:.]+\])(?::[0-9]+)?$/;
|
|
27348
27521
|
var LOGIN_PAGE = "/login";
|
|
27349
27522
|
var ORGS_PAGE2 = "/orgs";
|
|
27350
27523
|
var NO_PROFILE_STATUSES = /* @__PURE__ */ new Set([404, 409]);
|
|
27351
27524
|
function terminalOf(io) {
|
|
27352
27525
|
return io.terminal ?? nodeTerminal(io.stdin, io.stderr);
|
|
27353
27526
|
}
|
|
27527
|
+
function clockOf2(io) {
|
|
27528
|
+
return io.now ?? (() => /* @__PURE__ */ new Date());
|
|
27529
|
+
}
|
|
27530
|
+
function sleepOf(io) {
|
|
27531
|
+
return io.sleep ?? ((ms) => new Promise((resolve4) => {
|
|
27532
|
+
setTimeout(resolve4, ms);
|
|
27533
|
+
}));
|
|
27534
|
+
}
|
|
27535
|
+
function ciIsSet(env) {
|
|
27536
|
+
const value = (env["CI"] ?? "").trim().toLowerCase();
|
|
27537
|
+
return value !== "" && value !== "0" && value !== "false";
|
|
27538
|
+
}
|
|
27354
27539
|
async function answer(terminal, label, hidden = false) {
|
|
27355
27540
|
const text = hidden ? await terminal.promptHidden(label) : await terminal.prompt(label);
|
|
27356
27541
|
const value = hidden ? text : text.trim();
|
|
@@ -27358,6 +27543,23 @@ async function answer(terminal, label, hidden = false) {
|
|
|
27358
27543
|
throw new UsageError(`${label.replace(/:$/, "").toLowerCase()} must not be empty`);
|
|
27359
27544
|
return value;
|
|
27360
27545
|
}
|
|
27546
|
+
function unsupportedStep(name) {
|
|
27547
|
+
return new RemoteError(
|
|
27548
|
+
`this account needs a sign-in step that quibble doesn't support (${name})`
|
|
27549
|
+
);
|
|
27550
|
+
}
|
|
27551
|
+
function pageError(error62, webUrl) {
|
|
27552
|
+
if (error62 instanceof CognitoError) {
|
|
27553
|
+
const page = `${trimSlash2(webUrl)}${LOGIN_PAGE}`;
|
|
27554
|
+
if (error62.type === "PasswordResetRequiredException") {
|
|
27555
|
+
return new ConfigError(`reset your password at ${page}`);
|
|
27556
|
+
}
|
|
27557
|
+
if (error62.type === "UserNotConfirmedException") {
|
|
27558
|
+
return new ConfigError(`finish your sign-up at ${page}`);
|
|
27559
|
+
}
|
|
27560
|
+
}
|
|
27561
|
+
return error62;
|
|
27562
|
+
}
|
|
27361
27563
|
async function signIn(terminal, identity, config2) {
|
|
27362
27564
|
const email3 = await answer(terminal, "Email:");
|
|
27363
27565
|
const password = await answer(terminal, "Password:", true);
|
|
@@ -27385,26 +27587,22 @@ async function signIn(terminal, identity, config2) {
|
|
|
27385
27587
|
`finish your first sign-in at ${trimSlash2(config2.webUrl)}${LOGIN_PAGE}`
|
|
27386
27588
|
);
|
|
27387
27589
|
}
|
|
27388
|
-
throw
|
|
27389
|
-
`this account needs a sign-in step that quibble doesn't support (${name})`
|
|
27390
|
-
);
|
|
27590
|
+
throw unsupportedStep(name);
|
|
27391
27591
|
}
|
|
27392
27592
|
return result.tokens;
|
|
27393
27593
|
} catch (error62) {
|
|
27394
|
-
|
|
27395
|
-
const page = `${trimSlash2(config2.webUrl)}${LOGIN_PAGE}`;
|
|
27396
|
-
if (error62.type === "PasswordResetRequiredException") {
|
|
27397
|
-
throw new ConfigError(`reset your password at ${page}`);
|
|
27398
|
-
}
|
|
27399
|
-
if (error62.type === "UserNotConfirmedException") {
|
|
27400
|
-
throw new ConfigError(`finish your sign-up at ${page}`);
|
|
27401
|
-
}
|
|
27402
|
-
}
|
|
27403
|
-
throw error62;
|
|
27594
|
+
throw pageError(error62, config2.webUrl);
|
|
27404
27595
|
}
|
|
27405
27596
|
}
|
|
27406
27597
|
async function runLogin(options, context, io) {
|
|
27407
27598
|
requireServer("login", context);
|
|
27599
|
+
if (options.password === true) {
|
|
27600
|
+
if (options.browser === false) throw new UsageError(NO_BROWSER_MESSAGE);
|
|
27601
|
+
return passwordLogin(options, context, io);
|
|
27602
|
+
}
|
|
27603
|
+
return browserLogin(options, context, io);
|
|
27604
|
+
}
|
|
27605
|
+
async function passwordLogin(options, context, io) {
|
|
27408
27606
|
const terminal = terminalOf(io);
|
|
27409
27607
|
if (!terminal.isTTY) throw new UsageError(NEEDS_TERMINAL_MESSAGE);
|
|
27410
27608
|
const endpoint = await resolveEndpoint(context, io);
|
|
@@ -27420,15 +27618,203 @@ async function runLogin(options, context, io) {
|
|
|
27420
27618
|
} finally {
|
|
27421
27619
|
terminal.close();
|
|
27422
27620
|
}
|
|
27621
|
+
return finishLogin(tokens, endpoint, config2, options, context, io);
|
|
27622
|
+
}
|
|
27623
|
+
async function browserLogin(options, context, io) {
|
|
27624
|
+
if (ciIsSet(io.env)) throw new UsageError(CI_MESSAGE);
|
|
27625
|
+
const endpoint = await resolveEndpoint(context, io);
|
|
27626
|
+
const http = { debug: context.debug, fetch: io.fetch };
|
|
27627
|
+
const config2 = await fetchAuthConfig(endpoint, http);
|
|
27628
|
+
if (config2.mode === "local") {
|
|
27629
|
+
writeLine(io.stdout, `${endpoint} is in local mode: no login needed.`);
|
|
27630
|
+
return 0;
|
|
27631
|
+
}
|
|
27632
|
+
const clock = clockOf2(io);
|
|
27633
|
+
const started = await startLogin(endpoint, config2.webUrl, http, {
|
|
27634
|
+
host: cliLoginHost((io.hostname ?? hostname4)()),
|
|
27635
|
+
os: process.platform,
|
|
27636
|
+
version: options.version
|
|
27637
|
+
});
|
|
27638
|
+
const deadline = clock().getTime() + (CLI_LOGIN_TTL_SECONDS + started.interval) * 1e3;
|
|
27639
|
+
writeLine(
|
|
27640
|
+
io.stderr,
|
|
27641
|
+
`Approve this login in your browser: ${started.url} (code ${started.code}). Waiting...`
|
|
27642
|
+
);
|
|
27643
|
+
if (options.browser !== false) {
|
|
27644
|
+
const opener = io.opener ?? platformOpener({ env: io.env });
|
|
27645
|
+
void opener.open(started.url).catch(() => false);
|
|
27646
|
+
}
|
|
27647
|
+
const approval = await waitForApproval(endpoint, started, deadline, http, io);
|
|
27648
|
+
const tokens = await browserSignIn(
|
|
27649
|
+
{ region: config2.region, ...http },
|
|
27650
|
+
config2,
|
|
27651
|
+
approval,
|
|
27652
|
+
io
|
|
27653
|
+
);
|
|
27654
|
+
return finishLogin(tokens, endpoint, config2, options, context, io);
|
|
27655
|
+
}
|
|
27656
|
+
function postJson(url2, body, http) {
|
|
27657
|
+
return send(url2, {
|
|
27658
|
+
method: "POST",
|
|
27659
|
+
headers: { "content-type": "application/json" },
|
|
27660
|
+
body: JSON.stringify(body),
|
|
27661
|
+
...http
|
|
27662
|
+
});
|
|
27663
|
+
}
|
|
27664
|
+
async function startLogin(endpoint, webUrl, http, body) {
|
|
27665
|
+
const url2 = `${trimSlash2(endpoint)}${CLI_LOGINS_PATH}`;
|
|
27666
|
+
const site = { method: "POST", url: url2, debug: http.debug };
|
|
27667
|
+
const response = await postJson(url2, body, http);
|
|
27668
|
+
const text = await readText(response, site);
|
|
27669
|
+
if (response.status === 404) {
|
|
27670
|
+
throw new RemoteError(
|
|
27671
|
+
`${endpoint} does not support the browser login: run quibble login --password${detail(site, 404)}`,
|
|
27672
|
+
{ status: 404 }
|
|
27673
|
+
);
|
|
27674
|
+
}
|
|
27675
|
+
if (!response.ok) throw remoteFailure(site, response, text);
|
|
27676
|
+
const started = checkStart(asRecord(parseQuietly(text)), webUrl);
|
|
27677
|
+
if (started === void 0) {
|
|
27678
|
+
throw new RemoteError(`${MALFORMED_MESSAGE}${detail(site, response.status)}`);
|
|
27679
|
+
}
|
|
27680
|
+
return started;
|
|
27681
|
+
}
|
|
27682
|
+
function checkStart(body, webUrl) {
|
|
27683
|
+
const loginKey = body?.["loginKey"];
|
|
27684
|
+
const code2 = body?.["code"];
|
|
27685
|
+
const url2 = body?.["url"];
|
|
27686
|
+
if (typeof loginKey !== "string" || !CLI_LOGIN_KEY_RE.test(loginKey)) return void 0;
|
|
27687
|
+
if (typeof code2 !== "string" || typeof url2 !== "string") return void 0;
|
|
27688
|
+
try {
|
|
27689
|
+
if (parseCliLoginCode(code2) !== code2) return void 0;
|
|
27690
|
+
} catch {
|
|
27691
|
+
return void 0;
|
|
27692
|
+
}
|
|
27693
|
+
let origin;
|
|
27694
|
+
try {
|
|
27695
|
+
const web = new URL(webUrl);
|
|
27696
|
+
const secure = web.protocol === "https:" || web.protocol === "http:" && isLoopbackUrl(web.origin);
|
|
27697
|
+
if (!secure || !PLAIN_HOST_RE.test(web.host)) return void 0;
|
|
27698
|
+
origin = web.origin;
|
|
27699
|
+
} catch {
|
|
27700
|
+
return void 0;
|
|
27701
|
+
}
|
|
27702
|
+
if (url2 !== cliLoginUrl(origin, code2)) return void 0;
|
|
27703
|
+
const raw = body?.["interval"];
|
|
27704
|
+
const interval = typeof raw === "number" && Number.isInteger(raw) ? Math.min(MAX_INTERVAL_SECONDS, Math.max(MIN_INTERVAL_SECONDS, raw)) : CLI_LOGIN_POLL_SECONDS;
|
|
27705
|
+
return { loginKey, code: code2, url: url2, interval };
|
|
27706
|
+
}
|
|
27707
|
+
async function waitForApproval(endpoint, started, deadline, http, io) {
|
|
27708
|
+
const clock = clockOf2(io);
|
|
27709
|
+
const sleep = sleepOf(io);
|
|
27710
|
+
for (; ; ) {
|
|
27711
|
+
await sleep(started.interval * 1e3);
|
|
27712
|
+
if (clock().getTime() > deadline) throw new ConfigError(EXPIRED_MESSAGE);
|
|
27713
|
+
const answer2 = await poll(endpoint, started.loginKey, http);
|
|
27714
|
+
if (answer2 === void 0) continue;
|
|
27715
|
+
const status = answer2["status"];
|
|
27716
|
+
switch (status) {
|
|
27717
|
+
case "pending":
|
|
27718
|
+
continue;
|
|
27719
|
+
case "denied":
|
|
27720
|
+
throw new ConfigError(DENIED_MESSAGE);
|
|
27721
|
+
case "expired":
|
|
27722
|
+
throw new ConfigError(EXPIRED_MESSAGE);
|
|
27723
|
+
case "approved": {
|
|
27724
|
+
const username = answer2["username"];
|
|
27725
|
+
const secret = answer2["secret"];
|
|
27726
|
+
if (typeof username !== "string" || username === "") {
|
|
27727
|
+
throw new RemoteError(MALFORMED_MESSAGE);
|
|
27728
|
+
}
|
|
27729
|
+
if (typeof secret !== "string" || secret === "") {
|
|
27730
|
+
throw new RemoteError(MALFORMED_MESSAGE);
|
|
27731
|
+
}
|
|
27732
|
+
return { username, secret };
|
|
27733
|
+
}
|
|
27734
|
+
default:
|
|
27735
|
+
throw new RemoteError(UNKNOWN_STATUS_MESSAGE);
|
|
27736
|
+
}
|
|
27737
|
+
}
|
|
27738
|
+
}
|
|
27739
|
+
async function poll(endpoint, loginKey, http) {
|
|
27740
|
+
const url2 = `${trimSlash2(endpoint)}${CLI_LOGIN_POLL_PATH}`;
|
|
27741
|
+
const site = { method: "POST", url: url2, debug: http.debug };
|
|
27742
|
+
let response;
|
|
27743
|
+
let text;
|
|
27744
|
+
try {
|
|
27745
|
+
response = await postJson(url2, { loginKey }, http);
|
|
27746
|
+
text = await readText(response, site);
|
|
27747
|
+
} catch (error62) {
|
|
27748
|
+
if (error62 instanceof RemoteError && error62.unanswered) return void 0;
|
|
27749
|
+
throw error62;
|
|
27750
|
+
}
|
|
27751
|
+
if (!response.ok) {
|
|
27752
|
+
if (response.status >= 500 || response.status === 429) return void 0;
|
|
27753
|
+
throw remoteFailure(site, response, text);
|
|
27754
|
+
}
|
|
27755
|
+
return asRecord(parseJson(text, site, response.status)) ?? {};
|
|
27756
|
+
}
|
|
27757
|
+
async function browserSignIn(identity, config2, approval, io) {
|
|
27758
|
+
const clientId = config2.cliClientId;
|
|
27759
|
+
try {
|
|
27760
|
+
let result = await initiateAuth(identity, {
|
|
27761
|
+
clientId,
|
|
27762
|
+
flow: "CUSTOM_AUTH",
|
|
27763
|
+
username: approval.username
|
|
27764
|
+
});
|
|
27765
|
+
if (result.kind === "challenge") {
|
|
27766
|
+
const name = result.challenge.challengeName;
|
|
27767
|
+
if (name !== CUSTOM_CHALLENGE) throw unsupportedStep(name);
|
|
27768
|
+
result = await respondToCustomChallenge(identity, {
|
|
27769
|
+
clientId,
|
|
27770
|
+
challenge: result.challenge,
|
|
27771
|
+
username: approval.username,
|
|
27772
|
+
answer: approval.secret
|
|
27773
|
+
});
|
|
27774
|
+
}
|
|
27775
|
+
if (result.kind === "challenge" && result.challenge.challengeName === MFA_CHALLENGE) {
|
|
27776
|
+
result = await answerCode(
|
|
27777
|
+
identity,
|
|
27778
|
+
clientId,
|
|
27779
|
+
result.challenge,
|
|
27780
|
+
approval.username,
|
|
27781
|
+
io
|
|
27782
|
+
);
|
|
27783
|
+
}
|
|
27784
|
+
if (result.kind === "challenge") {
|
|
27785
|
+
throw unsupportedStep(result.challenge.challengeName);
|
|
27786
|
+
}
|
|
27787
|
+
return result.tokens;
|
|
27788
|
+
} catch (error62) {
|
|
27789
|
+
throw pageError(error62, config2.webUrl);
|
|
27790
|
+
}
|
|
27791
|
+
}
|
|
27792
|
+
async function answerCode(identity, clientId, challenge, username, io) {
|
|
27793
|
+
const terminal = terminalOf(io);
|
|
27794
|
+
try {
|
|
27795
|
+
if (!terminal.isTTY) throw new ConfigError(MFA_NEEDS_TERMINAL_MESSAGE);
|
|
27796
|
+
const code2 = await answer(terminal, "Code:");
|
|
27797
|
+
return await respondToAuthChallenge(identity, {
|
|
27798
|
+
clientId,
|
|
27799
|
+
challenge,
|
|
27800
|
+
email: username,
|
|
27801
|
+
code: code2
|
|
27802
|
+
});
|
|
27803
|
+
} finally {
|
|
27804
|
+
terminal.close();
|
|
27805
|
+
}
|
|
27806
|
+
}
|
|
27807
|
+
async function finishLogin(tokens, endpoint, config2, options, context, io) {
|
|
27423
27808
|
if (tokens.refreshToken === void 0) {
|
|
27424
27809
|
throw new RemoteError("sign-in didn't return a refresh token");
|
|
27425
27810
|
}
|
|
27426
|
-
const now =
|
|
27811
|
+
const now = clockOf2(io)();
|
|
27427
27812
|
const me = await readProfile(
|
|
27428
27813
|
new ApiClient({
|
|
27429
27814
|
endpoint,
|
|
27430
27815
|
authorizer: bearerAuthorizer(tokens.accessToken),
|
|
27431
|
-
|
|
27816
|
+
debug: context.debug,
|
|
27817
|
+
fetch: io.fetch
|
|
27432
27818
|
}),
|
|
27433
27819
|
config2.webUrl
|
|
27434
27820
|
);
|
|
@@ -28487,11 +28873,17 @@ function buildProgram(io, setCode) {
|
|
|
28487
28873
|
setCode(await runOrgs(options, contextFor(options, io), io));
|
|
28488
28874
|
});
|
|
28489
28875
|
withGlobalOptions(
|
|
28490
|
-
program2.command("login").description("log in to the endpoint")
|
|
28876
|
+
program2.command("login").description("log in to the endpoint, approved in a browser").option("--no-browser", "print the approval URL, and open no browser").option("--password", "log in with an email and a password on the terminal")
|
|
28491
28877
|
).action(async function() {
|
|
28492
28878
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
28493
28879
|
rejectEmptyOptions(options);
|
|
28494
|
-
setCode(
|
|
28880
|
+
setCode(
|
|
28881
|
+
await runLogin(
|
|
28882
|
+
{ ...options, version: packageVersion() },
|
|
28883
|
+
contextFor(options, io),
|
|
28884
|
+
io
|
|
28885
|
+
)
|
|
28886
|
+
);
|
|
28495
28887
|
});
|
|
28496
28888
|
withGlobalOptions(
|
|
28497
28889
|
program2.command("logout").description("log out of the endpoint")
|