ework-aio 0.2.1 → 0.2.2
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/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/install.ts +75 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ework-aio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "All-in-one installer for ework (issue tracker) + ework-daemon (AI bridge) + opencode-ework (plugin). One command: npm i -g ework-aio && ework-aio install.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli.ts
CHANGED
package/src/commands/install.ts
CHANGED
|
@@ -505,23 +505,30 @@ interface BootstrapBotOpts {
|
|
|
505
505
|
fetchImpl?: FetchLike;
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
-
// bootstrapBot: faithful port of install.sh's bot user + PAT flow
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
508
|
+
// bootstrapBot: faithful port of install.sh's bot user + PAT flow, with
|
|
509
|
+
// one critical correctness fix the bash version got wrong too.
|
|
510
|
+
//
|
|
511
|
+
// ework-web's /admin/users/create and /admin/users/<login>/reset-password
|
|
512
|
+
// both follow the PRG (Post/Redirect/Get) pattern: they return HTTP 303
|
|
513
|
+
// for BOTH success and failure, with the outcome encoded in the Location
|
|
514
|
+
// URL's query string (?ok=1 vs ?err=<msg>). Status-code checks are
|
|
515
|
+
// useless; we MUST inspect Location.
|
|
516
|
+
//
|
|
517
|
+
// Idempotence contract:
|
|
518
|
+
// 1. POST /admin/users/create — best-effort. May fail with
|
|
519
|
+
// "已存在" / "exists" if bot user was left over from a prior install.
|
|
520
|
+
// Any OTHER error (invalid login, weak password, etc) → abort.
|
|
521
|
+
// 2. POST /admin/users/<login>/reset-password — always. If the user
|
|
522
|
+
// was just created, this is a redundant no-op. If the user already
|
|
523
|
+
// existed, this overwrites the OLD random password (which we don't
|
|
524
|
+
// know) with our fresh one. Either way, /login below is guaranteed.
|
|
525
|
+
// 3. POST /login — uses the password we just set.
|
|
526
|
+
// 4. POST /me/tokens/create — scrape clear-text PAT from HTML.
|
|
520
527
|
async function bootstrapBot(opts: BootstrapBotOpts): Promise<string> {
|
|
521
528
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
522
529
|
const botPassword = randomBytes(24).toString("hex");
|
|
523
530
|
|
|
524
|
-
// 1. Create bot user
|
|
531
|
+
// 1. Create bot user (best-effort).
|
|
525
532
|
const createRes = await fetchImpl(`${opts.baseUrl}/admin/users/create`, {
|
|
526
533
|
method: "POST",
|
|
527
534
|
headers: { Cookie: opts.adminCookie, "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -533,30 +540,35 @@ async function bootstrapBot(opts: BootstrapBotOpts): Promise<string> {
|
|
|
533
540
|
}).toString(),
|
|
534
541
|
redirect: "manual",
|
|
535
542
|
});
|
|
536
|
-
if (createRes.status
|
|
537
|
-
// Bot user already exists from a previous install attempt. The
|
|
538
|
-
// password we just sent was IGNORED by ework-web (create only writes
|
|
539
|
-
// it on first creation). Without resetting, /login below uses the
|
|
540
|
-
// fresh botPassword which doesn't match the stored hash → 401.
|
|
541
|
-
// Force-reset via admin endpoint so the password we hold is real.
|
|
542
|
-
const resetRes = await fetchImpl(
|
|
543
|
-
`${opts.baseUrl}/admin/users/${encodeURIComponent(opts.botLogin)}/reset-password`,
|
|
544
|
-
{
|
|
545
|
-
method: "POST",
|
|
546
|
-
headers: { Cookie: opts.adminCookie, "Content-Type": "application/x-www-form-urlencoded" },
|
|
547
|
-
body: new URLSearchParams({ password: botPassword }).toString(),
|
|
548
|
-
redirect: "manual",
|
|
549
|
-
},
|
|
550
|
-
);
|
|
551
|
-
if (resetRes.status !== 303) {
|
|
552
|
-
const body = await resetRes.text();
|
|
553
|
-
throw new InstallError(
|
|
554
|
-
`bot user exists but password reset failed (HTTP ${resetRes.status}): ${body.slice(0, 200)}`,
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
} else if (createRes.status !== 303) {
|
|
543
|
+
if (createRes.status !== 303) {
|
|
558
544
|
const body = await createRes.text();
|
|
559
|
-
throw new InstallError(`create bot user
|
|
545
|
+
throw new InstallError(`create bot user HTTP ${createRes.status}: ${body.slice(0, 200)}`);
|
|
546
|
+
}
|
|
547
|
+
const createErr = locationErrParam(createRes.headers.get("location"));
|
|
548
|
+
if (createErr && !isAlreadyExistsError(createErr)) {
|
|
549
|
+
// Real create failure (invalid login, weak password, etc). Don't try
|
|
550
|
+
// reset — the same problem would surface there too.
|
|
551
|
+
throw new InstallError(`create bot user rejected by ework-web: ${createErr}`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// 2. Force-reset the password. Idempotent: sets the password to our
|
|
555
|
+
// fresh value whether the user was just created or already existed.
|
|
556
|
+
const resetRes = await fetchImpl(
|
|
557
|
+
`${opts.baseUrl}/admin/users/${encodeURIComponent(opts.botLogin)}/reset-password`,
|
|
558
|
+
{
|
|
559
|
+
method: "POST",
|
|
560
|
+
headers: { Cookie: opts.adminCookie, "Content-Type": "application/x-www-form-urlencoded" },
|
|
561
|
+
body: new URLSearchParams({ password: botPassword }).toString(),
|
|
562
|
+
redirect: "manual",
|
|
563
|
+
},
|
|
564
|
+
);
|
|
565
|
+
if (resetRes.status !== 303) {
|
|
566
|
+
const body = await resetRes.text();
|
|
567
|
+
throw new InstallError(`bot password reset HTTP ${resetRes.status}: ${body.slice(0, 200)}`);
|
|
568
|
+
}
|
|
569
|
+
const resetErr = locationErrParam(resetRes.headers.get("location"));
|
|
570
|
+
if (resetErr) {
|
|
571
|
+
throw new InstallError(`bot password reset rejected by ework-web: ${resetErr}`);
|
|
560
572
|
}
|
|
561
573
|
|
|
562
574
|
// 2. Login as bot to get its session cookie.
|
|
@@ -635,6 +647,33 @@ function parseFirstCookie(headers: Headers): string | null {
|
|
|
635
647
|
return null;
|
|
636
648
|
}
|
|
637
649
|
|
|
650
|
+
// locationErrParam: ework-web's admin POST endpoints use the PRG pattern,
|
|
651
|
+
// returning HTTP 303 with the outcome in the Location URL's query string
|
|
652
|
+
// (?ok=1 on success, ?err=<encoded msg> on failure). Extracts the err
|
|
653
|
+
// value (decoded) or null if absent. Used by bootstrapBot to tell
|
|
654
|
+
// "duplicate user" (recoverable via reset) from real failures (abort).
|
|
655
|
+
function locationErrParam(location: string | null): string | null {
|
|
656
|
+
if (!location) return null;
|
|
657
|
+
try {
|
|
658
|
+
const url = new URL(location, "http://placeholder.invalid");
|
|
659
|
+
const err = url.searchParams.get("err");
|
|
660
|
+
return err ?? null;
|
|
661
|
+
} catch {
|
|
662
|
+
// Location wasn't a parseable URL — defensive fallback via regex.
|
|
663
|
+
const m = location.match(/[?&]err=([^&]+)/);
|
|
664
|
+
return m ? decodeURIComponent(m[1]!) : null;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// isAlreadyExistsError: ework-web's createUser throws StoreError(409,
|
|
669
|
+
// "用户 X 已存在"). The Location redirect will carry this exact string
|
|
670
|
+
// (URL-encoded). We accept both Chinese and English variants in case
|
|
671
|
+
// ework-web changes its message later.
|
|
672
|
+
function isAlreadyExistsError(err: string): boolean {
|
|
673
|
+
const lower = err.toLowerCase();
|
|
674
|
+
return lower.includes("已存在") || lower.includes("already exists") || lower.includes("duplicate");
|
|
675
|
+
}
|
|
676
|
+
|
|
638
677
|
function printSummary(logger: Logger, o: InstallOutcome): void {
|
|
639
678
|
logger.hr();
|
|
640
679
|
logger.ok(`install complete (${o.mode} mode)`);
|