@proagentstore/cli 0.4.133 → 0.4.135
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/dist/browser-runner/local-apply/bridge.js +26 -5
- package/dist/browser-runner/local-apply/contract.js +0 -24
- package/dist/browser-runner/local-apply/handoff-contract.js +34 -0
- package/dist/browser-runner/local-apply/sign-in-runtime.js +218 -0
- package/dist/browser-runner/runner.js +11 -1
- package/dist/browser-runner/server.js +4 -0
- package/package.json +1 -1
|
@@ -94,6 +94,7 @@ export const INSPECT_PAGE = `() => {
|
|
|
94
94
|
unavailable: new RegExp(UNAVAILABLE_TEXT_SOURCE, "i").test(text) ? (new RegExp(EXPIRED_TEXT_SOURCE, "i").test(text) ? "expired" : "unavailable") : null,
|
|
95
95
|
};
|
|
96
96
|
}`;
|
|
97
|
+
const signInQuestion = (site) => `Sign in to ${site} on this machine's application browser, then retry. Nothing was submitted.`;
|
|
97
98
|
const text = (t, isError = false) => ({ content: [{ type: "text", text: t }], ...(isError ? { isError: true } : {}) });
|
|
98
99
|
const textOf = (r) => (r.content ?? []).map((c) => c.text ?? "").join("\n");
|
|
99
100
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -283,7 +284,9 @@ export class ApplyBridge {
|
|
|
283
284
|
if (!v || typeof v.url !== "string")
|
|
284
285
|
return null;
|
|
285
286
|
const unavailable = v.unavailable === "expired" || v.unavailable === "unavailable" ? v.unavailable : null;
|
|
286
|
-
|
|
287
|
+
// A sign-in page by its ADDRESS too (#1100): SEEK's `login.seek.com/login` has no password field, so an
|
|
288
|
+
// "Apply" that bounced there ran on to `awaiting_review` with nothing filled. Same rule as the submit path.
|
|
289
|
+
return { url: v.url, title: String(v.title ?? ""), captcha: !!v.captcha, login: looksLikeSignIn(v.url, !!v.login), duplicate: !!v.duplicate, antiBot: !!v.antiBot, confirmed: !!v.confirmed, unavailable };
|
|
287
290
|
}
|
|
288
291
|
blockerOf(s) {
|
|
289
292
|
return s.captcha ? "captcha" : s.antiBot ? "anti_bot" : s.duplicate ? "duplicate_application" : s.login ? "login_required" : null;
|
|
@@ -314,15 +317,19 @@ export class ApplyBridge {
|
|
|
314
317
|
const blocker = this.blockerOf(state);
|
|
315
318
|
if (!blocker)
|
|
316
319
|
return text(toolText);
|
|
320
|
+
return this.holdFor(blocker, state);
|
|
321
|
+
}
|
|
322
|
+
/** A page that needs a person: pause for the owner, and remember a sign-in they complete (#1094). */
|
|
323
|
+
async holdFor(blocker, state) {
|
|
324
|
+
const host = hostOf(state.url) ?? "";
|
|
317
325
|
this.host.emit({ type: "browser.blocked", url: state.url, domain: host, detail: { reason: blocker } });
|
|
318
326
|
const site = blocker === "login_required" && host ? siteKeyOf(host) : null;
|
|
319
327
|
if (site)
|
|
320
328
|
await this.host.sessions?.beginSignIn(site).catch(() => undefined);
|
|
321
|
-
const ok = await this.pauseFor({ reason: blocker, url: state.url, domain: host }, async () => {
|
|
329
|
+
const ok = await this.pauseFor({ reason: blocker, url: state.url, domain: host, ...(site ? { question: signInQuestion(site) } : {}) }, async () => {
|
|
322
330
|
const after = await this.inspect();
|
|
323
331
|
return !!after && this.blockerOf(after) === null;
|
|
324
332
|
});
|
|
325
|
-
// A sign-in the owner completed here is remembered for this browser profile (#1094).
|
|
326
333
|
if (ok && site)
|
|
327
334
|
await this.recordSignIn(site);
|
|
328
335
|
if (!ok)
|
|
@@ -533,6 +540,12 @@ export class ApplyBridge {
|
|
|
533
540
|
if (!phase)
|
|
534
541
|
return text("supervisor_checkpoint needs phase initial, post_navigation, before_submit or uncertain.", true);
|
|
535
542
|
const state = await this.inspect();
|
|
543
|
+
// A sign-in reached after the last landing — a late redirect (#1100, run 804bed49): hold for the owner as
|
|
544
|
+
// on landing. Reported to the cloud as a blocker it was stopped, and ended `incomplete`.
|
|
545
|
+
if (state?.login && !this.blocked) {
|
|
546
|
+
const held = await this.holdFor("login_required", state);
|
|
547
|
+
return held.isError ? held : text("The owner signed in. Take a browser_snapshot to read the page, then call supervisor_checkpoint again.");
|
|
548
|
+
}
|
|
536
549
|
const blockers = new Set();
|
|
537
550
|
if (state?.captcha)
|
|
538
551
|
blockers.add("captcha");
|
|
@@ -588,8 +601,16 @@ export class ApplyBridge {
|
|
|
588
601
|
this.host.emit({ type: "job.unavailable", url, domain, detail: { reason, source: "page_notice" } });
|
|
589
602
|
return text(`Recorded the runner-verified ${reason} listing notice. Stop now.`);
|
|
590
603
|
}
|
|
591
|
-
review(summary) {
|
|
604
|
+
async review(summary) {
|
|
592
605
|
this.summary = summary; // bounded where the result is built (runtime.ts `end`)
|
|
606
|
+
// Nothing entered, on a sign-in page (#1100; runs abb37433, 7fb1e4f5): the outcome is the owner's sign-in,
|
|
607
|
+
// not an empty form "awaiting review" — a card that is not `login_required` offers them no sign-in at all.
|
|
608
|
+
const state = !this.filled && !this.uploaded.size && !this.blocked ? await this.inspect() : null;
|
|
609
|
+
const host = state?.login ? hostOf(state.url) : null;
|
|
610
|
+
if (state && host) {
|
|
611
|
+
this.host.emit({ type: "browser.blocked", url: state.url, domain: host, detail: { reason: "login_required" } });
|
|
612
|
+
this.blocked = { reason: "login_required", questions: [signInQuestion(siteKeyOf(host))] };
|
|
613
|
+
}
|
|
593
614
|
// #989: "ready for review" with nothing entered is not a review state.
|
|
594
615
|
//
|
|
595
616
|
// The live run ended `awaiting_review` with `filled: 0, uploaded: 0` seconds after the cloud
|
|
@@ -655,7 +676,7 @@ export class ApplyBridge {
|
|
|
655
676
|
],
|
|
656
677
|
};
|
|
657
678
|
}
|
|
658
|
-
this.review(oneClick ? "Stopped at a one-click apply control: it can send the application outright, and this run fills and waits for review." : "Stopped at the final submit: this run fills and waits for review.");
|
|
679
|
+
await this.review(oneClick ? "Stopped at a one-click apply control: it can send the application outright, and this run fills and waits for review." : "Stopped at the final submit: this run fills and waits for review.");
|
|
659
680
|
return text(oneClick
|
|
660
681
|
? 'That control ("' + String(forward.element ?? "") + '") can send the application in one click, and this run is fill-and-review: it was NOT pressed. Approve this application if you want it sent. Stop now.'
|
|
661
682
|
: "That control submits the application, and this run is fill-and-review: it was NOT pressed. The application waits for the owner's review. Stop now.");
|
|
@@ -37,29 +37,6 @@ export const LOCAL_APPLY_CANCEL_PATH = "/local-apply/cancel";
|
|
|
37
37
|
export const LOCAL_APPLY_PREFLIGHT_PATH = "/local-apply/preflight";
|
|
38
38
|
/** Delivers a cloud-persisted supervisory decision to one paused checkpoint. */
|
|
39
39
|
export const LOCAL_APPLY_DIRECTIVE_PATH = "/local-apply/directive";
|
|
40
|
-
/**
|
|
41
|
-
* A short-lived, owner-authorized view of the *existing* browser page for one local-apply run.
|
|
42
|
-
* The id is opaque and carries no browser/profile material. It deliberately is not a task id:
|
|
43
|
-
* local-apply runs are not generic runner tasks.
|
|
44
|
-
*/
|
|
45
|
-
export const LOCAL_APPLY_HANDOFF_PATH = "/local-apply/handoff";
|
|
46
|
-
export const LOCAL_APPLY_HANDOFF_STATUS_PATH = "/local-apply/handoff-status";
|
|
47
|
-
export const LOCAL_APPLY_HANDOFF_FRAME_PATH = "/local-apply/handoff/frame";
|
|
48
|
-
export const LOCAL_APPLY_HANDOFF_INPUT_PATH = "/local-apply/handoff/input";
|
|
49
|
-
export const LOCAL_APPLY_HANDOFF_RESUME_PATH = "/local-apply/handoff/resume";
|
|
50
|
-
export const LOCAL_APPLY_HANDOFF_END_PATH = "/local-apply/handoff/end";
|
|
51
|
-
/**
|
|
52
|
-
* A separate, browser-only receipt/history check for an already-ended uncertain attempt. This is
|
|
53
|
-
* deliberately not an application run: it receives no engine, sources, artifacts or submit gate.
|
|
54
|
-
*/
|
|
55
|
-
export const LOCAL_APPLY_RECONCILIATION_RUN_PATH = "/local-apply/reconciliation/run";
|
|
56
|
-
export const LOCAL_APPLY_RECONCILIATION_STATUS_PATH = "/local-apply/reconciliation/status";
|
|
57
|
-
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_PATH = "/local-apply/reconciliation/handoff";
|
|
58
|
-
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_STATUS_PATH = "/local-apply/reconciliation/handoff-status";
|
|
59
|
-
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_FRAME_PATH = "/local-apply/reconciliation/handoff/frame";
|
|
60
|
-
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_INPUT_PATH = "/local-apply/reconciliation/handoff/input";
|
|
61
|
-
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_RESUME_PATH = "/local-apply/reconciliation/handoff/resume";
|
|
62
|
-
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_END_PATH = "/local-apply/reconciliation/handoff/end";
|
|
63
40
|
export const LOCAL_APPLY_ENGINES = ["claude", "codex"];
|
|
64
41
|
export const LOCAL_APPLY_AUTH_MODES = ["machine", "subscription"];
|
|
65
42
|
export const LOCAL_APPLY_MODES = ["fill_and_review", "auto_submit"];
|
|
@@ -166,7 +143,6 @@ export const LOCAL_APPLY_EVENT_TYPES = [
|
|
|
166
143
|
export const LOCAL_APPLY_ENGINE_AUTHS = ["machine-login", "subscription", "api-key", "missing_login", "unknown"];
|
|
167
144
|
export const LOCAL_APPLY_SUPERVISOR_DIRECTIVES = ["continue", "request_review", "stop"];
|
|
168
145
|
export const LOCAL_APPLY_SUPERVISOR_PHASES = ["initial", "before_submit", "post_navigation", "uncertain"];
|
|
169
|
-
export const LOCAL_APPLY_HANDOFF_TERMINAL_REASONS = ["unavailable", "profile_unavailable", "profile_mismatch", "page_lost", "expired", "run_ended", "runner_restarted"];
|
|
170
146
|
export const LOCAL_APPLY_CAPS = { questions: 20, questionChars: 300, answers: 20, answerChars: 2000, summary: 2000 };
|
|
171
147
|
const str = (v, max) => (typeof v === "string" && v.trim() ? v.trim().slice(0, max) : null);
|
|
172
148
|
const oneOf = (list, v) => (list.includes(v) ? v : null);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A short-lived, owner-authorized view of the *existing* browser page for one local-apply run.
|
|
3
|
+
* The id is opaque and carries no browser/profile material. It deliberately is not a task id:
|
|
4
|
+
* local-apply runs are not generic runner tasks.
|
|
5
|
+
*/
|
|
6
|
+
export const LOCAL_APPLY_HANDOFF_PATH = "/local-apply/handoff";
|
|
7
|
+
export const LOCAL_APPLY_HANDOFF_STATUS_PATH = "/local-apply/handoff-status";
|
|
8
|
+
export const LOCAL_APPLY_HANDOFF_FRAME_PATH = "/local-apply/handoff/frame";
|
|
9
|
+
export const LOCAL_APPLY_HANDOFF_INPUT_PATH = "/local-apply/handoff/input";
|
|
10
|
+
export const LOCAL_APPLY_HANDOFF_RESUME_PATH = "/local-apply/handoff/resume";
|
|
11
|
+
export const LOCAL_APPLY_HANDOFF_END_PATH = "/local-apply/handoff/end";
|
|
12
|
+
/**
|
|
13
|
+
* A separate, browser-only receipt/history check for an already-ended uncertain attempt. This is
|
|
14
|
+
* deliberately not an application run: it receives no engine, sources, artifacts or submit gate.
|
|
15
|
+
*/
|
|
16
|
+
export const LOCAL_APPLY_RECONCILIATION_RUN_PATH = "/local-apply/reconciliation/run";
|
|
17
|
+
export const LOCAL_APPLY_RECONCILIATION_STATUS_PATH = "/local-apply/reconciliation/status";
|
|
18
|
+
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_PATH = "/local-apply/reconciliation/handoff";
|
|
19
|
+
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_STATUS_PATH = "/local-apply/reconciliation/handoff-status";
|
|
20
|
+
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_FRAME_PATH = "/local-apply/reconciliation/handoff/frame";
|
|
21
|
+
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_INPUT_PATH = "/local-apply/reconciliation/handoff/input";
|
|
22
|
+
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_RESUME_PATH = "/local-apply/reconciliation/handoff/resume";
|
|
23
|
+
export const LOCAL_APPLY_RECONCILIATION_HANDOFF_END_PATH = "/local-apply/reconciliation/handoff/end";
|
|
24
|
+
/**
|
|
25
|
+
* A sign-in to the job's site in this machine's application browser profile, AFTER a fill ended
|
|
26
|
+
* `blocked` on `login_required` (#1100). Not a run and not a run's handoff: there is no engine, no
|
|
27
|
+
* source, no artifact and no submit gate — only the profile's page at the job's site, for the owner
|
|
28
|
+
* to sign in, and the profile's sign-in ledger (#1094) to record that they did.
|
|
29
|
+
*/
|
|
30
|
+
export const LOCAL_APPLY_SIGN_IN_PATH = "/local-apply/sign-in";
|
|
31
|
+
export const LOCAL_APPLY_SIGN_IN_FRAME_PATH = "/local-apply/sign-in/frame";
|
|
32
|
+
export const LOCAL_APPLY_SIGN_IN_INPUT_PATH = "/local-apply/sign-in/input";
|
|
33
|
+
export const LOCAL_APPLY_SIGN_IN_END_PATH = "/local-apply/sign-in/end";
|
|
34
|
+
export const LOCAL_APPLY_HANDOFF_TERMINAL_REASONS = ["unavailable", "profile_unavailable", "profile_mismatch", "page_lost", "expired", "run_ended", "runner_restarted"];
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A post-run site sign-in in this machine's application browser profile (#1100).
|
|
3
|
+
*
|
|
4
|
+
* #1094 made the profile persistent and pauses a fill `login_required` before an uncertain press, so
|
|
5
|
+
* the owner signs in once through the fill's handoff. But that handoff exists only while the fill is
|
|
6
|
+
* paused. The reported run (9a7d9e85) paused at 11:32, nobody reached it before the run's 20-minute
|
|
7
|
+
* limit, and it ended `blocked` at 11:53; the owner asked to take over at 11:57 — when there was no
|
|
8
|
+
* paused fill, no retained page, and so nothing to stream. Every later fill would bounce off the same
|
|
9
|
+
* sign-in, because the only way to sign in had closed with the run.
|
|
10
|
+
*
|
|
11
|
+
* This opens the profile itself at the job's site, with no run: the owner signs in there, and `end`
|
|
12
|
+
* with `complete` records the sign-in in the profile's ledger (names and expiry only) so the next
|
|
13
|
+
* fill's session gate passes. It has no engine, no bridge, no source and no submit gate — the page is
|
|
14
|
+
* for a person, and nothing here fills or presses anything.
|
|
15
|
+
*/
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { RunnerInputError } from "../errors.js";
|
|
18
|
+
import { siteKeyOf } from "./site-session.js";
|
|
19
|
+
const SAFE_ID = /^[A-Za-z0-9_.:-]{1,300}$/;
|
|
20
|
+
/** As long as the durable takeover it serves (`TAKEOVER_TTL_MS` in the Worker). */
|
|
21
|
+
const TTL_MS = 60 * 60_000;
|
|
22
|
+
export class LocalApplySignInRuntime {
|
|
23
|
+
deps;
|
|
24
|
+
sessions = new Map();
|
|
25
|
+
now;
|
|
26
|
+
schedule;
|
|
27
|
+
constructor(deps) {
|
|
28
|
+
this.deps = deps;
|
|
29
|
+
this.now = deps.now ?? (() => Date.now());
|
|
30
|
+
this.schedule = deps.schedule ?? ((task, delayMs) => {
|
|
31
|
+
const timer = setTimeout(task, delayMs);
|
|
32
|
+
return () => clearTimeout(timer);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Open the profile at the job's site, or read back the one already open under this id. Idempotent,
|
|
37
|
+
* so the cloud may call it whenever a frame finds no live page: a page the owner closed is reported
|
|
38
|
+
* closed, and a runner restart (nothing in memory) simply opens it again.
|
|
39
|
+
*/
|
|
40
|
+
async open(raw) {
|
|
41
|
+
const request = parseRequest(raw);
|
|
42
|
+
const existing = this.sessions.get(request.signInId);
|
|
43
|
+
if (existing) {
|
|
44
|
+
if (existing.request.browserProfile !== request.browserProfile || existing.request.url !== request.url)
|
|
45
|
+
throw new RunnerInputError("The sign-in id is already bound to another profile or site", 409);
|
|
46
|
+
if (existing.state === "ready" && !(await this.pageLive(existing)))
|
|
47
|
+
await this.close(existing, "page_lost");
|
|
48
|
+
// A finished sign-in is an answer and is read back; one that closed without one (the owner
|
|
49
|
+
// closed the tab, the deadline passed) opens again — the cloud asks only under a live takeover.
|
|
50
|
+
if (existing.state === "ready" || existing.signedIn)
|
|
51
|
+
return this.view(existing);
|
|
52
|
+
this.sessions.delete(request.signInId);
|
|
53
|
+
}
|
|
54
|
+
for (const [id, s] of this.sessions)
|
|
55
|
+
if (s.state === "closed" && s.expiresAt <= this.now())
|
|
56
|
+
this.sessions.delete(id);
|
|
57
|
+
if ([...this.sessions.values()].some((s) => s.state === "ready" && s.request.browserProfile === request.browserProfile))
|
|
58
|
+
throw new RunnerInputError("Another sign-in is already open in this application browser; finish it first.", 409);
|
|
59
|
+
// The `default` profile's page is the runner's own browser page; never navigate it from under a run.
|
|
60
|
+
if (this.deps.busy())
|
|
61
|
+
throw new RunnerInputError("An application run is using this machine's application browser; sign in when it ends.", 409);
|
|
62
|
+
const site = siteKeyOf(new URL(request.url).hostname);
|
|
63
|
+
const takeover = this.deps.takeover;
|
|
64
|
+
let browser;
|
|
65
|
+
try {
|
|
66
|
+
if (!takeover)
|
|
67
|
+
throw new Error("no takeover");
|
|
68
|
+
browser = await this.deps.browserFor(request.browserProfile, join(this.deps.dataDir, "sign-in", request.signInId));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return this.closedView(request, site, "profile_unavailable");
|
|
72
|
+
}
|
|
73
|
+
const session = { request, site, browser, state: "ready", expiresAt: this.now() + TTL_MS };
|
|
74
|
+
this.sessions.set(request.signInId, session);
|
|
75
|
+
session.cancelDeadline = this.schedule(() => void this.close(session, "expired"), TTL_MS);
|
|
76
|
+
const page = this.page(session);
|
|
77
|
+
if (!page) {
|
|
78
|
+
await this.close(session, "profile_unavailable");
|
|
79
|
+
return this.view(session);
|
|
80
|
+
}
|
|
81
|
+
await browser.sessions?.beginSignIn(site).catch(() => undefined);
|
|
82
|
+
// A slow or redirecting site still leaves a page the owner can use; only a lost page is terminal.
|
|
83
|
+
await page.goto(request.url, { waitUntil: "domcontentloaded", timeout: 20_000 }).catch(() => undefined);
|
|
84
|
+
try {
|
|
85
|
+
await takeover.open({ handoffId: request.signInId }, page);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
await this.close(session, "page_lost");
|
|
89
|
+
}
|
|
90
|
+
return this.view(session);
|
|
91
|
+
}
|
|
92
|
+
async frame(raw) {
|
|
93
|
+
const session = await this.live(raw);
|
|
94
|
+
try {
|
|
95
|
+
return await this.deps.takeover.frame(session.request.signInId);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
await this.close(session, "page_lost");
|
|
99
|
+
throw new RunnerInputError("The sign-in page is no longer available", 409);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async input(raw) {
|
|
103
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
104
|
+
const session = await this.live(o);
|
|
105
|
+
if (!o.input || typeof o.input !== "object")
|
|
106
|
+
throw new RunnerInputError("sign-in input is required");
|
|
107
|
+
try {
|
|
108
|
+
await this.deps.takeover.input(session.request.signInId, o.input);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
await this.close(session, "page_lost");
|
|
112
|
+
throw new RunnerInputError("The sign-in page is no longer available", 409);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Close it. `complete: true` is the owner saying they signed in: the ledger records it first. */
|
|
116
|
+
async end(raw) {
|
|
117
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
118
|
+
const session = this.require(o);
|
|
119
|
+
if (session.state === "ready" && o.complete === true) {
|
|
120
|
+
const recorded = await session.browser.sessions?.completeSignIn(session.site).catch(() => null);
|
|
121
|
+
if (recorded)
|
|
122
|
+
session.signedIn = recorded;
|
|
123
|
+
}
|
|
124
|
+
await this.close(session, session.signedIn ? undefined : "unavailable");
|
|
125
|
+
return this.view(session);
|
|
126
|
+
}
|
|
127
|
+
liveWork() {
|
|
128
|
+
return [...this.sessions.values()].filter((s) => s.state === "ready").map((s) => `sign-in:${s.request.signInId}`);
|
|
129
|
+
}
|
|
130
|
+
closeAll() {
|
|
131
|
+
for (const session of this.sessions.values())
|
|
132
|
+
void this.close(session, "runner_restarted");
|
|
133
|
+
}
|
|
134
|
+
async close(session, reason) {
|
|
135
|
+
if (session.state === "closed")
|
|
136
|
+
return;
|
|
137
|
+
// Terminal synchronously, so a deadline racing an `end` cannot tear down twice.
|
|
138
|
+
session.state = "closed";
|
|
139
|
+
session.terminalReason = reason;
|
|
140
|
+
session.cancelDeadline?.();
|
|
141
|
+
session.cancelDeadline = undefined;
|
|
142
|
+
await this.deps.takeover?.end(session.request.signInId).catch(() => undefined);
|
|
143
|
+
await session.browser.stop().catch(() => undefined);
|
|
144
|
+
}
|
|
145
|
+
async live(raw) {
|
|
146
|
+
const session = this.require(raw);
|
|
147
|
+
if (session.state === "ready" && session.expiresAt <= this.now())
|
|
148
|
+
await this.close(session, "expired");
|
|
149
|
+
if (session.state === "ready" && !(await this.pageLive(session)))
|
|
150
|
+
await this.close(session, "page_lost");
|
|
151
|
+
if (session.state !== "ready")
|
|
152
|
+
throw new RunnerInputError(`The sign-in is closed (${session.terminalReason ?? "finished"})`, 409);
|
|
153
|
+
return session;
|
|
154
|
+
}
|
|
155
|
+
async pageLive(session) {
|
|
156
|
+
return !!this.page(session) && (await this.deps.takeover?.state(session.request.signInId)) === "ready";
|
|
157
|
+
}
|
|
158
|
+
page(session) {
|
|
159
|
+
const page = session.browser.handoffPage?.();
|
|
160
|
+
return page && !page.isClosed() ? page : null;
|
|
161
|
+
}
|
|
162
|
+
require(raw) {
|
|
163
|
+
const id = raw?.signInId;
|
|
164
|
+
if (typeof id !== "string" || !SAFE_ID.test(id))
|
|
165
|
+
throw new RunnerInputError("signInId is required");
|
|
166
|
+
const session = this.sessions.get(id);
|
|
167
|
+
if (!session)
|
|
168
|
+
throw new RunnerInputError("Sign-in not found", 404);
|
|
169
|
+
return session;
|
|
170
|
+
}
|
|
171
|
+
view(session) {
|
|
172
|
+
return {
|
|
173
|
+
signInId: session.request.signInId,
|
|
174
|
+
browserProfile: session.request.browserProfile,
|
|
175
|
+
site: session.site,
|
|
176
|
+
state: session.state,
|
|
177
|
+
expiresAt: new Date(session.expiresAt).toISOString(),
|
|
178
|
+
...(session.terminalReason ? { terminalReason: session.terminalReason } : {}),
|
|
179
|
+
...(session.signedIn ? { signedIn: session.signedIn } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
closedView(request, site, terminalReason) {
|
|
183
|
+
return { signInId: request.signInId, browserProfile: request.browserProfile, site, state: "closed", expiresAt: new Date(this.now()).toISOString(), terminalReason };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Separate from the fill's handoff and from reconciliation: this surface has no fill or submit operation. */
|
|
187
|
+
export async function routeLocalApplySignIn(runtime, method, path, read) {
|
|
188
|
+
if (method !== "POST")
|
|
189
|
+
return undefined;
|
|
190
|
+
if (path === "/local-apply/sign-in")
|
|
191
|
+
return runtime.open(await read());
|
|
192
|
+
if (path === "/local-apply/sign-in/frame")
|
|
193
|
+
return runtime.frame(await read());
|
|
194
|
+
if (path === "/local-apply/sign-in/input") {
|
|
195
|
+
await runtime.input(await read());
|
|
196
|
+
return { ok: true };
|
|
197
|
+
}
|
|
198
|
+
if (path === "/local-apply/sign-in/end")
|
|
199
|
+
return runtime.end(await read());
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
function parseRequest(raw) {
|
|
203
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
204
|
+
const signInId = typeof o.signInId === "string" ? o.signInId : "";
|
|
205
|
+
const browserProfile = o.browserProfile === "default" || o.browserProfile === "isolated" ? o.browserProfile : null;
|
|
206
|
+
const url = typeof o.url === "string" ? o.url : "";
|
|
207
|
+
let ok = false;
|
|
208
|
+
try {
|
|
209
|
+
const u = new URL(url);
|
|
210
|
+
ok = u.protocol === "https:" || u.protocol === "http:";
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
ok = false;
|
|
214
|
+
}
|
|
215
|
+
if (!SAFE_ID.test(signInId) || !browserProfile || !ok)
|
|
216
|
+
throw new RunnerInputError("sign-in needs an opaque signInId, a browserProfile and an http(s) url");
|
|
217
|
+
return { signInId, browserProfile, url };
|
|
218
|
+
}
|
|
@@ -19,6 +19,7 @@ import { LocalArtifactRuntime } from "./local-artifact/runtime.js";
|
|
|
19
19
|
import { LOCAL_ARTIFACT_TASK_TYPE } from "./local-artifact/contract.js";
|
|
20
20
|
import { LocalApplyRuntime } from "./local-apply/runtime.js";
|
|
21
21
|
import { LocalApplyReconciliationRuntime } from "./local-apply/reconciliation-runtime.js";
|
|
22
|
+
import { LocalApplySignInRuntime } from "./local-apply/sign-in-runtime.js";
|
|
22
23
|
import { LOCAL_APPLY_TASK_TYPE } from "./local-apply/contract.js";
|
|
23
24
|
import { createRunnerBrowserFactories } from "./local-apply/live-run-browser.js";
|
|
24
25
|
import { createLocalApplyTakeoverAdapter, isLocalApplyTakeover } from "./local-apply/takeover-adapter.js";
|
|
@@ -95,6 +96,8 @@ export class LocalRunner {
|
|
|
95
96
|
localApply;
|
|
96
97
|
/** Browser-only, no-bridge reconciliation for an ended uncertain apply attempt. */
|
|
97
98
|
localApplyReconciliation;
|
|
99
|
+
/** A site sign-in in the application profile after a fill ended blocked on it (#1100): no run, no bridge. */
|
|
100
|
+
localApplySignIn;
|
|
98
101
|
/** The Website Builder's local author (#841): the signed-in CLI drafting through the PAGS broker. */
|
|
99
102
|
siteBuilder;
|
|
100
103
|
/** This runner's own local URL, once the server listens — the bridge forwarder calls back to it. */
|
|
@@ -118,6 +121,12 @@ export class LocalRunner {
|
|
|
118
121
|
takeover: localApplyTakeover,
|
|
119
122
|
});
|
|
120
123
|
this.localApplyReconciliation = new LocalApplyReconciliationRuntime({ dataDir: config.dataDir, browserFor: applyBrowserFor, takeover: localApplyTakeover });
|
|
124
|
+
this.localApplySignIn = new LocalApplySignInRuntime({
|
|
125
|
+
dataDir: config.dataDir,
|
|
126
|
+
browserFor: applyBrowserFor,
|
|
127
|
+
takeover: localApplyTakeover,
|
|
128
|
+
busy: () => this.localApply.liveWork().length + this.localApplyReconciliation.liveWork().length + this.localBrowser.liveWork().length > 0,
|
|
129
|
+
});
|
|
121
130
|
this.siteBuilder = new SiteBuilderRuntime({ dataDir: config.dataDir, selfUrl: () => this.selfUrl, onEvent: (id, type, message, data) => this.addTaskEvent(this.requireTask(id), type, message, data) });
|
|
122
131
|
// Tasks paused/running on a previous process are orphaned now — their
|
|
123
132
|
// pages and takeover sessions are gone. Fail them so the board is clean.
|
|
@@ -136,7 +145,7 @@ export class LocalRunner {
|
|
|
136
145
|
*/
|
|
137
146
|
liveWork() {
|
|
138
147
|
const coding = this.coding.liveWork();
|
|
139
|
-
const local = [...this.localBrowser.liveWork(), ...this.localArtifact.liveWork(), ...this.localApply.liveWork(), ...this.localApplyReconciliation.liveWork(), ...this.siteBuilder.liveWork()];
|
|
148
|
+
const local = [...this.localBrowser.liveWork(), ...this.localArtifact.liveWork(), ...this.localApply.liveWork(), ...this.localApplyReconciliation.liveWork(), ...this.localApplySignIn.liveWork(), ...this.siteBuilder.liveWork()];
|
|
140
149
|
return { codingTurns: coding.length, localRuns: local.length, detail: [...coding, ...local].slice(0, 10) };
|
|
141
150
|
}
|
|
142
151
|
capabilities() {
|
|
@@ -320,6 +329,7 @@ export class LocalRunner {
|
|
|
320
329
|
this.localArtifact.closeAll();
|
|
321
330
|
this.localApply.closeAll();
|
|
322
331
|
this.localApplyReconciliation.closeAll();
|
|
332
|
+
this.localApplySignIn.closeAll();
|
|
323
333
|
try {
|
|
324
334
|
this.coding.closeAll();
|
|
325
335
|
}
|
|
@@ -8,6 +8,7 @@ import { listGithubOrgs, listGithubRepos, searchGithubRepos, getGithubRepoDetail
|
|
|
8
8
|
import { routeLocalApplyHandoff } from "./local-apply/handoff-routes.js";
|
|
9
9
|
import { runNativePreflight } from "./native/preflight.js";
|
|
10
10
|
import { routeLocalApplyReconciliation } from "./local-apply/reconciliation-routes.js";
|
|
11
|
+
import { routeLocalApplySignIn } from "./local-apply/sign-in-runtime.js";
|
|
11
12
|
export function createRunnerServer(runner) {
|
|
12
13
|
return createServer(async (req, res) => {
|
|
13
14
|
try {
|
|
@@ -273,6 +274,9 @@ async function route(runner, req, res) {
|
|
|
273
274
|
const reconciliation = await routeLocalApplyReconciliation(runner.localApplyReconciliation, req.method, path, () => readJson(req));
|
|
274
275
|
if (reconciliation !== undefined)
|
|
275
276
|
return json(res, 200, reconciliation);
|
|
277
|
+
const signIn = await routeLocalApplySignIn(runner.localApplySignIn, req.method, path, () => readJson(req));
|
|
278
|
+
if (signIn !== undefined)
|
|
279
|
+
return json(res, 200, signIn);
|
|
276
280
|
if (req.method === "POST" && path === "/local-apply/resume") {
|
|
277
281
|
return json(res, 200, runner.localApply.resume(await readJson(req)));
|
|
278
282
|
}
|