instar 1.3.400 → 1.3.402
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/dashboard/index.html +48 -18
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +59 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/core/EnrollmentWizard.d.ts +84 -0
- package/dist/core/EnrollmentWizard.d.ts.map +1 -0
- package/dist/core/EnrollmentWizard.js +107 -0
- package/dist/core/EnrollmentWizard.js.map +1 -0
- package/dist/core/FrameworkLoginDriver.d.ts +85 -0
- package/dist/core/FrameworkLoginDriver.d.ts.map +1 -0
- package/dist/core/FrameworkLoginDriver.js +110 -0
- package/dist/core/FrameworkLoginDriver.js.map +1 -0
- package/dist/core/PendingLoginStore.d.ts +118 -0
- package/dist/core/PendingLoginStore.d.ts.map +1 -0
- package/dist/core/PendingLoginStore.js +201 -0
- package/dist/core/PendingLoginStore.js.map +1 -0
- package/dist/core/types.d.ts +8 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +1 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +59 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/src/scaffold/templates.ts +1 -0
- package/upgrades/1.3.401.md +30 -0
- package/upgrades/1.3.402.md +64 -0
- package/upgrades/side-effects/dashboard-stream-phase3-ui.md +58 -0
- package/upgrades/side-effects/subscription-auth-enrollment.md +91 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EnrollmentWizard — mobile-first login orchestration (P2.1 of the Subscription
|
|
3
|
+
* & Auth Standard). Sits on top of PendingLoginStore and turns a raw framework
|
|
4
|
+
* login into a phone-friendly, expiry-resilient flow.
|
|
5
|
+
*
|
|
6
|
+
* The job (from the spec + the pi live-test specimen):
|
|
7
|
+
* - START a login: drive the framework's login flow, capture the PUBLIC
|
|
8
|
+
* device-code / auth URL, and store it with its TTL visible. (Codex →
|
|
9
|
+
* device-code; Claude → URL+paste-back-code.)
|
|
10
|
+
* - On EXPIRY: auto-reissue a fresh code/URL WITHOUT the operator asking — the
|
|
11
|
+
* exact gap the pi live-test exposed (the first code expired before Justin
|
|
12
|
+
* got to it and re-issuing took a manual round-trip).
|
|
13
|
+
* - COMPLETE when the operator approves at the provider + the account enrols.
|
|
14
|
+
*
|
|
15
|
+
* The interactive part — actually driving the framework's login CLI to obtain a
|
|
16
|
+
* code/URL — is INJECTED (`driveLogin`), so this orchestrator is pure +
|
|
17
|
+
* hermetically testable (no spawning, no network, no real OAuth). In production
|
|
18
|
+
* the driver runs the framework's device-code/login flow and scrapes the code +
|
|
19
|
+
* URL + TTL; here tests pass a stub. NEVER handles a credential — only the
|
|
20
|
+
* public code/URL the operator types into the provider's own page.
|
|
21
|
+
*/
|
|
22
|
+
import { PendingLoginStore, type PendingLogin, type LoginFlowKind, type LoginProvider } from './PendingLoginStore.js';
|
|
23
|
+
/** The public artifact a framework login yields (no secret). */
|
|
24
|
+
export interface LoginArtifact {
|
|
25
|
+
verificationUrl: string;
|
|
26
|
+
userCode?: string;
|
|
27
|
+
ttlMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/** Injected: drive the framework's login flow, return its public code/URL. */
|
|
30
|
+
export type LoginDriver = (req: {
|
|
31
|
+
provider: LoginProvider;
|
|
32
|
+
framework: PendingLogin['framework'];
|
|
33
|
+
kind: LoginFlowKind;
|
|
34
|
+
/** The new account's CLAUDE_CONFIG_DIR — isolates this login to its own slot
|
|
35
|
+
* so enrolling a 2nd account never clobbers the 1st. */
|
|
36
|
+
configHome?: string;
|
|
37
|
+
}) => Promise<LoginArtifact>;
|
|
38
|
+
export interface EnrollmentWizardConfig {
|
|
39
|
+
store: PendingLoginStore;
|
|
40
|
+
driveLogin: LoginDriver;
|
|
41
|
+
now?: () => number;
|
|
42
|
+
logger?: {
|
|
43
|
+
log: (m: string) => void;
|
|
44
|
+
warn: (m: string) => void;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export interface StartEnrollmentInput {
|
|
48
|
+
id: string;
|
|
49
|
+
label: string;
|
|
50
|
+
provider: LoginProvider;
|
|
51
|
+
framework: PendingLogin['framework'];
|
|
52
|
+
/** device-code (Codex) or url-code-paste (Claude). Defaults per provider. */
|
|
53
|
+
kind?: LoginFlowKind;
|
|
54
|
+
/** The new account's CLAUDE_CONFIG_DIR — isolates the login to its own slot. */
|
|
55
|
+
configHome?: string;
|
|
56
|
+
}
|
|
57
|
+
export declare class EnrollmentWizard {
|
|
58
|
+
private readonly store;
|
|
59
|
+
private readonly driveLogin;
|
|
60
|
+
private readonly logger;
|
|
61
|
+
constructor(cfg: EnrollmentWizardConfig);
|
|
62
|
+
/** Default flow kind per provider: Codex/OpenAI = device-code (its endorsed
|
|
63
|
+
* flow); everyone else = url-code-paste (the phone-friendly Claude path). */
|
|
64
|
+
static defaultKind(provider: LoginProvider): LoginFlowKind;
|
|
65
|
+
/**
|
|
66
|
+
* Start an enrollment: drive the login, capture the public code/URL, store it
|
|
67
|
+
* as a pending login (TTL visible). Returns the stored PendingLogin — the
|
|
68
|
+
* surface the operator's phone shows.
|
|
69
|
+
*/
|
|
70
|
+
start(input: StartEnrollmentInput): Promise<PendingLogin>;
|
|
71
|
+
/**
|
|
72
|
+
* Auto-reissue every EXPIRED pending login without the operator asking — the
|
|
73
|
+
* pi-live-test gap. For each expired login, re-drive the framework flow and
|
|
74
|
+
* refresh the stored code/URL + TTL. Returns the reissued logins. Driver
|
|
75
|
+
* failures are skipped (logged, left expired) so one bad re-drive doesn't
|
|
76
|
+
* abort the sweep.
|
|
77
|
+
*/
|
|
78
|
+
reissueExpired(): Promise<PendingLogin[]>;
|
|
79
|
+
/** Mark a login completed once the operator approved + the account enrolled. */
|
|
80
|
+
complete(id: string): PendingLogin | null;
|
|
81
|
+
/** The phone surface: still-valid logins awaiting approval. */
|
|
82
|
+
pending(): PendingLogin[];
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=EnrollmentWizard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EnrollmentWizard.d.ts","sourceRoot":"","sources":["../../src/core/EnrollmentWizard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EACL,iBAAiB,EACjB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,wBAAwB,CAAC;AAEhC,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,8EAA8E;AAC9E,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE;IAC9B,QAAQ,EAAE,aAAa,CAAC;IACxB,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;IACrC,IAAI,EAAE,aAAa,CAAC;IACpB;6DACyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;AAE7B,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,iBAAiB,CAAC;IACzB,UAAU,EAAE,WAAW,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE;QAAE,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAClE;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;IACxB,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;IACrC,6EAA6E;IAC7E,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAc;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0D;gBAErE,GAAG,EAAE,sBAAsB;IAMvC;kFAC8E;IAC9E,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,aAAa,GAAG,aAAa;IAI1D;;;;OAIG;IACG,KAAK,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC;IAuB/D;;;;;;OAMG;IACG,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IA4B/C,gFAAgF;IAChF,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAIzC,+DAA+D;IAC/D,OAAO,IAAI,YAAY,EAAE;CAG1B"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EnrollmentWizard — mobile-first login orchestration (P2.1 of the Subscription
|
|
3
|
+
* & Auth Standard). Sits on top of PendingLoginStore and turns a raw framework
|
|
4
|
+
* login into a phone-friendly, expiry-resilient flow.
|
|
5
|
+
*
|
|
6
|
+
* The job (from the spec + the pi live-test specimen):
|
|
7
|
+
* - START a login: drive the framework's login flow, capture the PUBLIC
|
|
8
|
+
* device-code / auth URL, and store it with its TTL visible. (Codex →
|
|
9
|
+
* device-code; Claude → URL+paste-back-code.)
|
|
10
|
+
* - On EXPIRY: auto-reissue a fresh code/URL WITHOUT the operator asking — the
|
|
11
|
+
* exact gap the pi live-test exposed (the first code expired before Justin
|
|
12
|
+
* got to it and re-issuing took a manual round-trip).
|
|
13
|
+
* - COMPLETE when the operator approves at the provider + the account enrols.
|
|
14
|
+
*
|
|
15
|
+
* The interactive part — actually driving the framework's login CLI to obtain a
|
|
16
|
+
* code/URL — is INJECTED (`driveLogin`), so this orchestrator is pure +
|
|
17
|
+
* hermetically testable (no spawning, no network, no real OAuth). In production
|
|
18
|
+
* the driver runs the framework's device-code/login flow and scrapes the code +
|
|
19
|
+
* URL + TTL; here tests pass a stub. NEVER handles a credential — only the
|
|
20
|
+
* public code/URL the operator types into the provider's own page.
|
|
21
|
+
*/
|
|
22
|
+
export class EnrollmentWizard {
|
|
23
|
+
store;
|
|
24
|
+
driveLogin;
|
|
25
|
+
logger;
|
|
26
|
+
constructor(cfg) {
|
|
27
|
+
this.store = cfg.store;
|
|
28
|
+
this.driveLogin = cfg.driveLogin;
|
|
29
|
+
this.logger = cfg.logger ?? { log: () => { }, warn: () => { } };
|
|
30
|
+
}
|
|
31
|
+
/** Default flow kind per provider: Codex/OpenAI = device-code (its endorsed
|
|
32
|
+
* flow); everyone else = url-code-paste (the phone-friendly Claude path). */
|
|
33
|
+
static defaultKind(provider) {
|
|
34
|
+
return provider === 'openai' ? 'device-code' : 'url-code-paste';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Start an enrollment: drive the login, capture the public code/URL, store it
|
|
38
|
+
* as a pending login (TTL visible). Returns the stored PendingLogin — the
|
|
39
|
+
* surface the operator's phone shows.
|
|
40
|
+
*/
|
|
41
|
+
async start(input) {
|
|
42
|
+
const kind = input.kind ?? EnrollmentWizard.defaultKind(input.provider);
|
|
43
|
+
const artifact = await this.driveLogin({
|
|
44
|
+
provider: input.provider,
|
|
45
|
+
framework: input.framework,
|
|
46
|
+
kind,
|
|
47
|
+
configHome: input.configHome,
|
|
48
|
+
});
|
|
49
|
+
const login = this.store.issue({
|
|
50
|
+
id: input.id,
|
|
51
|
+
label: input.label,
|
|
52
|
+
provider: input.provider,
|
|
53
|
+
framework: input.framework,
|
|
54
|
+
kind,
|
|
55
|
+
configHome: input.configHome,
|
|
56
|
+
verificationUrl: artifact.verificationUrl,
|
|
57
|
+
userCode: artifact.userCode,
|
|
58
|
+
ttlMs: artifact.ttlMs,
|
|
59
|
+
});
|
|
60
|
+
this.logger.log(`[EnrollmentWizard] started ${kind} login for ${input.label} (${input.provider})`);
|
|
61
|
+
return login;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Auto-reissue every EXPIRED pending login without the operator asking — the
|
|
65
|
+
* pi-live-test gap. For each expired login, re-drive the framework flow and
|
|
66
|
+
* refresh the stored code/URL + TTL. Returns the reissued logins. Driver
|
|
67
|
+
* failures are skipped (logged, left expired) so one bad re-drive doesn't
|
|
68
|
+
* abort the sweep.
|
|
69
|
+
*/
|
|
70
|
+
async reissueExpired() {
|
|
71
|
+
const reissued = [];
|
|
72
|
+
for (const login of this.store.expired()) {
|
|
73
|
+
try {
|
|
74
|
+
const fresh = await this.driveLogin({
|
|
75
|
+
provider: login.provider,
|
|
76
|
+
framework: login.framework,
|
|
77
|
+
kind: login.kind,
|
|
78
|
+
configHome: login.configHome,
|
|
79
|
+
});
|
|
80
|
+
const updated = this.store.reissue(login.id, {
|
|
81
|
+
verificationUrl: fresh.verificationUrl,
|
|
82
|
+
userCode: fresh.userCode,
|
|
83
|
+
ttlMs: fresh.ttlMs,
|
|
84
|
+
});
|
|
85
|
+
if (updated) {
|
|
86
|
+
reissued.push(updated);
|
|
87
|
+
this.logger.log(`[EnrollmentWizard] auto-reissued expired login ${login.id} (reissue #${updated.reissueCount})`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
// @silent-fallback-ok: one re-drive failing must not abort the sweep;
|
|
92
|
+
// the login stays expired and is retried next sweep.
|
|
93
|
+
this.logger.warn(`[EnrollmentWizard] reissue of ${login.id} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return reissued;
|
|
97
|
+
}
|
|
98
|
+
/** Mark a login completed once the operator approved + the account enrolled. */
|
|
99
|
+
complete(id) {
|
|
100
|
+
return this.store.complete(id);
|
|
101
|
+
}
|
|
102
|
+
/** The phone surface: still-valid logins awaiting approval. */
|
|
103
|
+
pending() {
|
|
104
|
+
return this.store.active();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=EnrollmentWizard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EnrollmentWizard.js","sourceRoot":"","sources":["../../src/core/EnrollmentWizard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FrameworkLoginDriver — the concrete `LoginDriver` for the EnrollmentWizard
|
|
3
|
+
* (P2.1 of the Subscription & Auth Standard). It is the "hands" that actually
|
|
4
|
+
* obtain a PUBLIC login artifact (verification URL + optional device code + TTL)
|
|
5
|
+
* from a framework's login flow, so the wizard can surface it to the operator's
|
|
6
|
+
* phone.
|
|
7
|
+
*
|
|
8
|
+
* Design constraints (why the I/O is injected):
|
|
9
|
+
* - The SCRAPE logic — turning a pane's text into a `{ verificationUrl,
|
|
10
|
+
* userCode, ttlMs }` artifact — is PURE and must be unit-testable against
|
|
11
|
+
* real captured-output fixtures with no tmux, no spawning, no network.
|
|
12
|
+
* - The I/O — spawning the login command under the target account's
|
|
13
|
+
* `CLAUDE_CONFIG_DIR` and capturing the pane — is INJECTED (`spawn`,
|
|
14
|
+
* `capture`, `sleep`, `now`). Production wiring (server.ts) passes the real
|
|
15
|
+
* tmux primitives; tests pass fakes. This also keeps this module decoupled
|
|
16
|
+
* from SessionManager's spawn internals.
|
|
17
|
+
*
|
|
18
|
+
* SECURITY: this driver only ever reads the PUBLIC artifact a provider prints to
|
|
19
|
+
* be typed into its own page — the verification URL and (for device-code flows)
|
|
20
|
+
* the short user code. It NEVER reads, stores, or returns a token. The actual
|
|
21
|
+
* credential is written by the framework's own login client into the account's
|
|
22
|
+
* config home; instar never touches it.
|
|
23
|
+
*/
|
|
24
|
+
import type { LoginArtifact, LoginDriver } from './EnrollmentWizard.js';
|
|
25
|
+
import type { LoginFlowKind, LoginProvider } from './PendingLoginStore.js';
|
|
26
|
+
/** A login flow we know how to launch + scrape. */
|
|
27
|
+
export interface FrameworkLoginRequest {
|
|
28
|
+
provider: LoginProvider;
|
|
29
|
+
framework: 'claude-code' | 'codex-cli' | 'gemini-cli' | 'pi-cli';
|
|
30
|
+
kind: LoginFlowKind;
|
|
31
|
+
/** The account's CLAUDE_CONFIG_DIR — isolates this login to its own slot. */
|
|
32
|
+
configHome?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Injected I/O so the driver is decoupled + hermetically testable. */
|
|
35
|
+
export interface FrameworkLoginDriverDeps {
|
|
36
|
+
/**
|
|
37
|
+
* Spawn the framework's login command in a dedicated tmux pane under the
|
|
38
|
+
* given configHome. Returns a session handle the capture fn reads from.
|
|
39
|
+
*/
|
|
40
|
+
spawn: (req: FrameworkLoginRequest) => Promise<{
|
|
41
|
+
session: string;
|
|
42
|
+
}>;
|
|
43
|
+
/** Capture the current text of a login pane. */
|
|
44
|
+
capture: (session: string) => Promise<string>;
|
|
45
|
+
/** Await ms (injected so tests don't really wait). */
|
|
46
|
+
sleep?: (ms: number) => Promise<void>;
|
|
47
|
+
now?: () => number;
|
|
48
|
+
logger?: {
|
|
49
|
+
log: (m: string) => void;
|
|
50
|
+
warn: (m: string) => void;
|
|
51
|
+
};
|
|
52
|
+
/** How long to poll the pane for the artifact before giving up (default 60s). */
|
|
53
|
+
scrapeTimeoutMs?: number;
|
|
54
|
+
/** Poll cadence while waiting for the artifact to appear (default 1s). */
|
|
55
|
+
pollIntervalMs?: number;
|
|
56
|
+
}
|
|
57
|
+
export declare class FrameworkLoginDriver {
|
|
58
|
+
private readonly deps;
|
|
59
|
+
private readonly sleep;
|
|
60
|
+
private readonly now;
|
|
61
|
+
private readonly logger;
|
|
62
|
+
private readonly scrapeTimeoutMs;
|
|
63
|
+
private readonly pollIntervalMs;
|
|
64
|
+
constructor(deps: FrameworkLoginDriverDeps);
|
|
65
|
+
/**
|
|
66
|
+
* Parse a PUBLIC login artifact out of captured pane text. Pure — no I/O.
|
|
67
|
+
* Returns null until the pane has emitted at least a verification URL (for
|
|
68
|
+
* device-code flows, also a user code). Exported via the static for tests.
|
|
69
|
+
*/
|
|
70
|
+
static parseArtifact(paneText: string, kind: LoginFlowKind): LoginArtifact | null;
|
|
71
|
+
/**
|
|
72
|
+
* Drive a framework login: spawn it under the target config home, poll the
|
|
73
|
+
* pane until the public artifact appears (or timeout), and return it. Throws
|
|
74
|
+
* on timeout so the wizard logs + leaves the login for the next sweep.
|
|
75
|
+
*/
|
|
76
|
+
drive(req: {
|
|
77
|
+
provider: LoginProvider;
|
|
78
|
+
framework: FrameworkLoginRequest['framework'];
|
|
79
|
+
kind: LoginFlowKind;
|
|
80
|
+
configHome?: string;
|
|
81
|
+
}): Promise<LoginArtifact>;
|
|
82
|
+
/** Adapt to the EnrollmentWizard's LoginDriver signature. */
|
|
83
|
+
asLoginDriver(): LoginDriver;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=FrameworkLoginDriver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FrameworkLoginDriver.d.ts","sourceRoot":"","sources":["../../src/core/FrameworkLoginDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE3E,mDAAmD;AACnD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,aAAa,CAAC;IACxB,SAAS,EAAE,aAAa,GAAG,WAAW,GAAG,YAAY,GAAG,QAAQ,CAAC;IACjE,IAAI,EAAE,aAAa,CAAC;IACpB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,uEAAuE;AACvE,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,KAAK,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,gDAAgD;IAChD,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,sDAAsD;IACtD,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE;QAAE,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;IACjE,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AASD,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA2B;IAChD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0D;IACjF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAE5B,IAAI,EAAE,wBAAwB;IAS1C;;;;OAIG;IACH,MAAM,CAAC,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,aAAa,GAAG,IAAI;IAiBjF;;;;OAIG;IACG,KAAK,CAAC,GAAG,EAAE;QACf,QAAQ,EAAE,aAAa,CAAC;QACxB,SAAS,EAAE,qBAAqB,CAAC,WAAW,CAAC,CAAC;QAC9C,IAAI,EAAE,aAAa,CAAC;QACpB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,aAAa,CAAC;IA4B1B,6DAA6D;IAC7D,aAAa,IAAI,WAAW;CAG7B"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FrameworkLoginDriver — the concrete `LoginDriver` for the EnrollmentWizard
|
|
3
|
+
* (P2.1 of the Subscription & Auth Standard). It is the "hands" that actually
|
|
4
|
+
* obtain a PUBLIC login artifact (verification URL + optional device code + TTL)
|
|
5
|
+
* from a framework's login flow, so the wizard can surface it to the operator's
|
|
6
|
+
* phone.
|
|
7
|
+
*
|
|
8
|
+
* Design constraints (why the I/O is injected):
|
|
9
|
+
* - The SCRAPE logic — turning a pane's text into a `{ verificationUrl,
|
|
10
|
+
* userCode, ttlMs }` artifact — is PURE and must be unit-testable against
|
|
11
|
+
* real captured-output fixtures with no tmux, no spawning, no network.
|
|
12
|
+
* - The I/O — spawning the login command under the target account's
|
|
13
|
+
* `CLAUDE_CONFIG_DIR` and capturing the pane — is INJECTED (`spawn`,
|
|
14
|
+
* `capture`, `sleep`, `now`). Production wiring (server.ts) passes the real
|
|
15
|
+
* tmux primitives; tests pass fakes. This also keeps this module decoupled
|
|
16
|
+
* from SessionManager's spawn internals.
|
|
17
|
+
*
|
|
18
|
+
* SECURITY: this driver only ever reads the PUBLIC artifact a provider prints to
|
|
19
|
+
* be typed into its own page — the verification URL and (for device-code flows)
|
|
20
|
+
* the short user code. It NEVER reads, stores, or returns a token. The actual
|
|
21
|
+
* credential is written by the framework's own login client into the account's
|
|
22
|
+
* config home; instar never touches it.
|
|
23
|
+
*/
|
|
24
|
+
const URL_RE = /(https?:\/\/[^\s"'<>)\]]+)/i;
|
|
25
|
+
// Device codes are short, dash-grouped, uppercase-alnum: e.g. 7DAU-W4XJA, ABCD-1234.
|
|
26
|
+
const DEVICE_CODE_RE = /\b([A-Z0-9]{4}-[A-Z0-9]{4,6})\b/;
|
|
27
|
+
// "expires in 15 minutes", "valid for 10 min", "expires in 900 seconds".
|
|
28
|
+
const TTL_MIN_RE = /(?:expire[sd]?|valid)\b[^.\n]*?\b(\d{1,3})\s*(?:minutes?|mins?\b)/i;
|
|
29
|
+
const TTL_SEC_RE = /(?:expire[sd]?|valid)\b[^.\n]*?\b(\d{2,5})\s*(?:seconds?|secs?\b)/i;
|
|
30
|
+
export class FrameworkLoginDriver {
|
|
31
|
+
deps;
|
|
32
|
+
sleep;
|
|
33
|
+
now;
|
|
34
|
+
logger;
|
|
35
|
+
scrapeTimeoutMs;
|
|
36
|
+
pollIntervalMs;
|
|
37
|
+
constructor(deps) {
|
|
38
|
+
this.deps = deps;
|
|
39
|
+
this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
40
|
+
this.now = deps.now ?? (() => Date.now());
|
|
41
|
+
this.logger = deps.logger ?? { log: () => { }, warn: () => { } };
|
|
42
|
+
this.scrapeTimeoutMs = deps.scrapeTimeoutMs ?? 60_000;
|
|
43
|
+
this.pollIntervalMs = deps.pollIntervalMs ?? 1_000;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse a PUBLIC login artifact out of captured pane text. Pure — no I/O.
|
|
47
|
+
* Returns null until the pane has emitted at least a verification URL (for
|
|
48
|
+
* device-code flows, also a user code). Exported via the static for tests.
|
|
49
|
+
*/
|
|
50
|
+
static parseArtifact(paneText, kind) {
|
|
51
|
+
if (!paneText)
|
|
52
|
+
return null;
|
|
53
|
+
const urlMatch = paneText.match(URL_RE);
|
|
54
|
+
if (!urlMatch)
|
|
55
|
+
return null;
|
|
56
|
+
const verificationUrl = stripTrailingPunctuation(urlMatch[1]);
|
|
57
|
+
let userCode;
|
|
58
|
+
if (kind === 'device-code') {
|
|
59
|
+
const codeMatch = paneText.match(DEVICE_CODE_RE);
|
|
60
|
+
if (!codeMatch)
|
|
61
|
+
return null; // device-code flow isn't ready until the code prints
|
|
62
|
+
userCode = codeMatch[1];
|
|
63
|
+
}
|
|
64
|
+
const ttlMs = parseTtlMs(paneText);
|
|
65
|
+
return { verificationUrl, userCode, ttlMs };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Drive a framework login: spawn it under the target config home, poll the
|
|
69
|
+
* pane until the public artifact appears (or timeout), and return it. Throws
|
|
70
|
+
* on timeout so the wizard logs + leaves the login for the next sweep.
|
|
71
|
+
*/
|
|
72
|
+
async drive(req) {
|
|
73
|
+
const { session } = await this.deps.spawn({
|
|
74
|
+
provider: req.provider,
|
|
75
|
+
framework: req.framework,
|
|
76
|
+
kind: req.kind,
|
|
77
|
+
configHome: req.configHome,
|
|
78
|
+
});
|
|
79
|
+
const deadline = this.now() + this.scrapeTimeoutMs;
|
|
80
|
+
let lastText = '';
|
|
81
|
+
while (this.now() < deadline) {
|
|
82
|
+
lastText = await this.deps.capture(session);
|
|
83
|
+
const artifact = FrameworkLoginDriver.parseArtifact(lastText, req.kind);
|
|
84
|
+
if (artifact) {
|
|
85
|
+
this.logger.log(`[FrameworkLoginDriver] captured ${req.kind} artifact for ${req.provider}/${req.framework}`);
|
|
86
|
+
return artifact;
|
|
87
|
+
}
|
|
88
|
+
await this.sleep(this.pollIntervalMs);
|
|
89
|
+
}
|
|
90
|
+
this.logger.warn(`[FrameworkLoginDriver] timed out scraping ${req.kind} login for ${req.provider}/${req.framework}`);
|
|
91
|
+
throw new Error(`login artifact not found for ${req.provider}/${req.framework} within ${this.scrapeTimeoutMs}ms`);
|
|
92
|
+
}
|
|
93
|
+
/** Adapt to the EnrollmentWizard's LoginDriver signature. */
|
|
94
|
+
asLoginDriver() {
|
|
95
|
+
return (req) => this.drive(req);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function stripTrailingPunctuation(url) {
|
|
99
|
+
return url.replace(/[.,;:'")\]]+$/, '');
|
|
100
|
+
}
|
|
101
|
+
function parseTtlMs(text) {
|
|
102
|
+
const min = text.match(TTL_MIN_RE);
|
|
103
|
+
if (min)
|
|
104
|
+
return Number(min[1]) * 60_000;
|
|
105
|
+
const sec = text.match(TTL_SEC_RE);
|
|
106
|
+
if (sec)
|
|
107
|
+
return Number(sec[1]) * 1_000;
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=FrameworkLoginDriver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FrameworkLoginDriver.js","sourceRoot":"","sources":["../../src/core/FrameworkLoginDriver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PendingLoginStore — durable record of in-flight subscription logins (P2.1 of
|
|
3
|
+
* the Subscription & Auth Standard, the mobile-first enrollment wizard).
|
|
4
|
+
*
|
|
5
|
+
* When the operator enrolls an account from their phone, the login is a
|
|
6
|
+
* short-lived artifact: a device-code (Codex: `auth.openai.com/codex/device` +
|
|
7
|
+
* a 9-char code, ~15min TTL) or a URL+paste-back-code flow (Claude). The pi
|
|
8
|
+
* live-test exposed the gap this closes: the first code expired before the
|
|
9
|
+
* operator got to it, the provider's error was opaque, and re-issuing required a
|
|
10
|
+
* manual round-trip. This store makes pending logins durable + TTL-aware so the
|
|
11
|
+
* wizard can:
|
|
12
|
+
* - surface a code/URL IMMEDIATELY with its TTL visible,
|
|
13
|
+
* - detect expiry and re-issue a fresh one WITHOUT the operator asking,
|
|
14
|
+
* - present a "Pending Logins" surface where a fresh code is one tap away.
|
|
15
|
+
*
|
|
16
|
+
* It stores NO credential — only the public device-code / auth URL the provider
|
|
17
|
+
* shows, which the operator types into the provider's own page. The actual login
|
|
18
|
+
* happens against the provider, never through instar (Secret-Drop discipline).
|
|
19
|
+
*
|
|
20
|
+
* File-backed JSON (atomic tmp+rename), mirrors the SubscriptionPool durable-
|
|
21
|
+
* registry pattern. The re-issue mechanism (re-driving the framework login) is
|
|
22
|
+
* the wizard's concern + injected, so this store stays pure + hermetically
|
|
23
|
+
* testable (no spawning, no network).
|
|
24
|
+
*/
|
|
25
|
+
export type LoginFlowKind = 'device-code' | 'url-code-paste';
|
|
26
|
+
export type LoginProvider = 'openai' | 'anthropic' | 'github-copilot' | 'google';
|
|
27
|
+
/**
|
|
28
|
+
* Pending-login lifecycle:
|
|
29
|
+
* pending — code/URL issued, awaiting the operator's approval at the provider
|
|
30
|
+
* expired — TTL elapsed before approval (eligible for auto-reissue)
|
|
31
|
+
* completed — the operator approved + the account enrolled
|
|
32
|
+
* abandoned — operator/admin cancelled, or superseded by a re-issue
|
|
33
|
+
*/
|
|
34
|
+
export type PendingLoginStatus = 'pending' | 'expired' | 'completed' | 'abandoned';
|
|
35
|
+
export interface PendingLogin {
|
|
36
|
+
/** Stable id, charset ^[a-z0-9-]+$. */
|
|
37
|
+
id: string;
|
|
38
|
+
/** Operator-facing label / intended account nickname. */
|
|
39
|
+
label: string;
|
|
40
|
+
provider: LoginProvider;
|
|
41
|
+
framework: 'claude-code' | 'codex-cli' | 'gemini-cli' | 'pi-cli';
|
|
42
|
+
kind: LoginFlowKind;
|
|
43
|
+
/** The new account's CLAUDE_CONFIG_DIR (a filesystem path, never a secret) —
|
|
44
|
+
* recorded so an auto-reissue re-drives the login under the SAME slot. */
|
|
45
|
+
configHome?: string;
|
|
46
|
+
/** The public verification URL the operator opens (never a secret). */
|
|
47
|
+
verificationUrl: string;
|
|
48
|
+
/** Device-code (e.g. "7DAU-W4XJA") for device-code flows; absent for url-code-paste. */
|
|
49
|
+
userCode?: string;
|
|
50
|
+
/** ISO timestamp the code/URL expires. */
|
|
51
|
+
ttlExpiresAt: string;
|
|
52
|
+
status: PendingLoginStatus;
|
|
53
|
+
/** How many times this login has been re-issued after expiry. */
|
|
54
|
+
reissueCount: number;
|
|
55
|
+
createdAt: string;
|
|
56
|
+
updatedAt: string;
|
|
57
|
+
/** Monotonic version for optimistic CAS. */
|
|
58
|
+
version: number;
|
|
59
|
+
}
|
|
60
|
+
export interface PendingLoginStoreConfig {
|
|
61
|
+
stateDir: string;
|
|
62
|
+
/** Injectable clock (ms epoch) for deterministic TTL tests. */
|
|
63
|
+
now?: () => number;
|
|
64
|
+
}
|
|
65
|
+
export interface IssueLoginInput {
|
|
66
|
+
id: string;
|
|
67
|
+
label: string;
|
|
68
|
+
provider: LoginProvider;
|
|
69
|
+
framework: PendingLogin['framework'];
|
|
70
|
+
kind: LoginFlowKind;
|
|
71
|
+
configHome?: string;
|
|
72
|
+
verificationUrl: string;
|
|
73
|
+
userCode?: string;
|
|
74
|
+
/** TTL in ms from now (default 15 min — the observed Codex device-code TTL). */
|
|
75
|
+
ttlMs?: number;
|
|
76
|
+
}
|
|
77
|
+
export declare class ValidationError extends Error {
|
|
78
|
+
constructor(message: string);
|
|
79
|
+
}
|
|
80
|
+
export declare class PendingLoginStore {
|
|
81
|
+
private storePath;
|
|
82
|
+
private store;
|
|
83
|
+
private readonly now;
|
|
84
|
+
constructor(config: PendingLoginStoreConfig);
|
|
85
|
+
/** All pending logins, with live status (expiry computed against the clock). */
|
|
86
|
+
list(): PendingLogin[];
|
|
87
|
+
get(id: string): PendingLogin | null;
|
|
88
|
+
/** Logins that are expired (TTL elapsed) but not completed/abandoned — the
|
|
89
|
+
* auto-reissue work-list. */
|
|
90
|
+
expired(): PendingLogin[];
|
|
91
|
+
/** Active (still-valid, awaiting-approval) logins — the "Pending Logins" surface. */
|
|
92
|
+
active(): PendingLogin[];
|
|
93
|
+
size(): number;
|
|
94
|
+
/** Issue a new pending login. Stores the public URL/code only — never a token. */
|
|
95
|
+
issue(input: IssueLoginInput, rawExtra?: Record<string, unknown>): PendingLogin;
|
|
96
|
+
/**
|
|
97
|
+
* Re-issue an expired (or pending) login with a fresh URL/code + TTL — the
|
|
98
|
+
* auto-reissue path. Bumps reissueCount, resets status to pending. Returns null
|
|
99
|
+
* if not found.
|
|
100
|
+
*/
|
|
101
|
+
reissue(id: string, fresh: {
|
|
102
|
+
verificationUrl: string;
|
|
103
|
+
userCode?: string;
|
|
104
|
+
ttlMs?: number;
|
|
105
|
+
}): PendingLogin | null;
|
|
106
|
+
/** Mark a login completed (the operator approved + the account enrolled). */
|
|
107
|
+
complete(id: string): PendingLogin | null;
|
|
108
|
+
/** Cancel a login (operator/admin). */
|
|
109
|
+
abandon(id: string): PendingLogin | null;
|
|
110
|
+
private transition;
|
|
111
|
+
/** Computes live `expired` status from the TTL without mutating the store
|
|
112
|
+
* (pending → expired when past TTL; completed/abandoned are terminal). */
|
|
113
|
+
private withLiveStatus;
|
|
114
|
+
private assertNoCredentialFields;
|
|
115
|
+
private load;
|
|
116
|
+
private save;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=PendingLoginStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PendingLoginStore.d.ts","sourceRoot":"","sources":["../../src/core/PendingLoginStore.ts"],"names":[],"mappings":"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"}
|