@rubytech/create-maxy-code 0.1.354 → 0.1.356
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/__tests__/preflight-port-classifier.test.js +52 -0
- package/dist/preflight-port-classifier.js +31 -2
- package/package.json +1 -1
- package/payload/platform/lib/admin-access-password/__tests__/index.test.ts +25 -0
- package/payload/platform/lib/admin-access-password/dist/index.d.ts +7 -1
- package/payload/platform/lib/admin-access-password/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admin-access-password/dist/index.js +12 -1
- package/payload/platform/lib/admin-access-password/dist/index.js.map +1 -1
- package/payload/platform/lib/admin-access-password/src/index.ts +12 -1
- package/payload/platform/plugins/docs/references/admin-session.md +1 -1
- package/payload/platform/scripts/check-no-esm-require.mjs +4 -0
- package/payload/server/{chunk-7GNXBVGY.js → chunk-25UVZNFA.js} +3 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/AdminLoginScreens-4I5CdIWl.js +1 -0
- package/payload/server/public/assets/{AdminShell-GGnPXCOe.js → AdminShell-Bc9F7X_N.js} +1 -1
- package/payload/server/public/assets/{Checkbox-D3XFVUMT.js → Checkbox-DoQV18Ow.js} +1 -1
- package/payload/server/public/assets/OperatorConversations-BJY9FZbr.js +9 -0
- package/payload/server/public/assets/OperatorConversations-C1I78hON.css +1 -0
- package/payload/server/public/assets/{admin-B2yNhVvH.js → admin-DLsMA09F.js} +1 -1
- package/payload/server/public/assets/{browser-ByaQ0ItQ.js → browser-w6rY_KAO.js} +1 -1
- package/payload/server/public/assets/{calendar-BjqCT8cW.js → calendar-CvEdDFYd.js} +1 -1
- package/payload/server/public/assets/chat-BlOWMUVI.js +1 -0
- package/payload/server/public/assets/{data-p9p-181v.js → data-p3T_EZFq.js} +1 -1
- package/payload/server/public/assets/{graph-BuunC8eP.js → graph-BrwfYOr3.js} +1 -1
- package/payload/server/public/assets/{graph-labels-BF3Ak8q0.js → graph-labels-BNZ8u8TE.js} +1 -1
- package/payload/server/public/assets/{operator-D7c8mN4J.js → operator-BNY1P_mH.js} +1 -1
- package/payload/server/public/assets/{page-C6uMZdT1.js → page-CtygoIXt.js} +1 -1
- package/payload/server/public/assets/{public-B9_UhjEa.js → public-CDzw6yjH.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +4 -4
- package/payload/server/public/chat.html +5 -5
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +7 -7
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +105 -10
- package/payload/server/public/assets/AdminLoginScreens-6WgO-Z3m.js +0 -1
- package/payload/server/public/assets/OperatorConversations-CFWXdnjB.css +0 -1
- package/payload/server/public/assets/OperatorConversations-DeZ2wDEK.js +0 -9
- package/payload/server/public/assets/chat-kl-Ips-o.js +0 -1
|
@@ -201,6 +201,58 @@ test("chromium variant basenames: google-chrome-stable detected as chromium", ()
|
|
|
201
201
|
assert.equal(r.kind, "OWN_BRAND");
|
|
202
202
|
assert.equal(r.holderType, "chromium");
|
|
203
203
|
});
|
|
204
|
+
test("chromium argv-rewrite: self-flattened space-joined cmdline (no NULs) still OWN_BRAND", () => {
|
|
205
|
+
// Chromium rewrites its own /proc/<pid>/cmdline into a single space-joined
|
|
206
|
+
// blob shortly after launch (the kernel's NUL-separated argv only survives a
|
|
207
|
+
// brief window). A steady-state chromium therefore has NO NUL separators, so
|
|
208
|
+
// `split("\0")` yields one token and argv[0] basename is not a chromium name.
|
|
209
|
+
// Reproduces the muvin cdpPort=9223 install abort: the holder must still be
|
|
210
|
+
// recognised as OWN_BRAND so the reclaim path runs.
|
|
211
|
+
const flat = "/usr/lib/chromium/chromium --js-flags=--no-decommit-pooled-pages " +
|
|
212
|
+
"--enable-remote-extensions --load-extension --use-angle=gles " +
|
|
213
|
+
"--user-data-dir=/home/admin/.maxy/chromium-profile --ozone-platform=x11 " +
|
|
214
|
+
"--no-sandbox --remote-debugging-port=9222 about:blank";
|
|
215
|
+
const r = classifyPortHolder({
|
|
216
|
+
ssOutput: SS_PID_42,
|
|
217
|
+
ownBrand: MAXY,
|
|
218
|
+
peerBrands: peers(MAXY),
|
|
219
|
+
getCmdline: () => flat, // no NUL bytes — the rewritten form
|
|
220
|
+
});
|
|
221
|
+
assert.equal(r.kind, "OWN_BRAND");
|
|
222
|
+
assert.equal(r.holderType, "chromium");
|
|
223
|
+
assert.equal(r.profilePath, "/home/admin/.maxy/chromium-profile");
|
|
224
|
+
});
|
|
225
|
+
test("chromium argv-rewrite: flattened cmdline with a foreign profile stays UNRELATED", () => {
|
|
226
|
+
// The space-joined fallback must not widen ownership: a rewritten chromium
|
|
227
|
+
// whose --user-data-dir is not the brand's profile is still UNRELATED and
|
|
228
|
+
// must hit the operator-override abort, exactly as the NUL-separated form does.
|
|
229
|
+
const flat = "/usr/lib/chromium/chromium --enable-remote-extensions " +
|
|
230
|
+
"--user-data-dir=/tmp/scratch --remote-debugging-port=9222 about:blank";
|
|
231
|
+
const r = classifyPortHolder({
|
|
232
|
+
ssOutput: SS_PID_42,
|
|
233
|
+
ownBrand: MAXY,
|
|
234
|
+
peerBrands: peers(MAXY),
|
|
235
|
+
getCmdline: () => flat,
|
|
236
|
+
});
|
|
237
|
+
assert.equal(r.kind, "UNRELATED");
|
|
238
|
+
assert.equal(r.holderType, "chromium");
|
|
239
|
+
});
|
|
240
|
+
test("argv-rewrite fallback is chromium-gated: non-chromium single-token blob stays unknown", () => {
|
|
241
|
+
// The whitespace fallback must fire only for chromium (the one holder that
|
|
242
|
+
// rewrites its own cmdline). A non-chromium process whose cmdline is a single
|
|
243
|
+
// token containing whitespace must NOT be whitespace-split — otherwise a stray
|
|
244
|
+
// `websockify` basename token could fabricate a holder. Here the blob contains
|
|
245
|
+
// the word "websockify"; without the chromium gate it would misclassify.
|
|
246
|
+
const flat = "/usr/local/bin/my websockify wrapper [::]:6080";
|
|
247
|
+
const r = classifyPortHolder({
|
|
248
|
+
ssOutput: SS_PID_555,
|
|
249
|
+
ownBrand: MAXY,
|
|
250
|
+
peerBrands: peers(MAXY),
|
|
251
|
+
getCmdline: () => flat, // single token, no NULs, argv[0] basename "my"
|
|
252
|
+
});
|
|
253
|
+
assert.equal(r.kind, "UNRELATED");
|
|
254
|
+
assert.equal(r.holderType, "unknown");
|
|
255
|
+
});
|
|
204
256
|
// ---------------------------------------------------------------------------
|
|
205
257
|
// Xtigervnc (display-number anchor)
|
|
206
258
|
// ---------------------------------------------------------------------------
|
|
@@ -38,7 +38,10 @@
|
|
|
38
38
|
* `cmdline` should be the raw `/proc/<pid>/cmdline` contents — NUL-separated
|
|
39
39
|
* argv as the kernel emits it. Do NOT replace NUL with space before passing:
|
|
40
40
|
* the argv boundaries are what disambiguate `--user-data-dir=PATH` from a
|
|
41
|
-
* different flag whose value happens to contain `--user-data-dir=`.
|
|
41
|
+
* different flag whose value happens to contain `--user-data-dir=`. The one
|
|
42
|
+
* exception is handled internally by `splitCmdline`: chromium rewrites its own
|
|
43
|
+
* cmdline into a space-joined blob with no NULs, so a chromium-gated whitespace
|
|
44
|
+
* fallback recovers argv for that case only.
|
|
42
45
|
*/
|
|
43
46
|
export function classifyPortHolder(args) {
|
|
44
47
|
const { ssOutput, ownBrand, peerBrands, getCmdline } = args;
|
|
@@ -56,7 +59,7 @@ export function classifyPortHolder(args) {
|
|
|
56
59
|
return { kind: "UNRELATED", pid, cmdlineReadFailed: true };
|
|
57
60
|
}
|
|
58
61
|
const prettyCmdline = cmdline.replace(/\0/g, " ").trim();
|
|
59
|
-
const argv = cmdline
|
|
62
|
+
const argv = splitCmdline(cmdline);
|
|
60
63
|
const holderType = detectHolderType(argv);
|
|
61
64
|
switch (holderType) {
|
|
62
65
|
case "chromium":
|
|
@@ -83,6 +86,32 @@ function basename(p) {
|
|
|
83
86
|
const i = p.lastIndexOf("/");
|
|
84
87
|
return i === -1 ? p : p.slice(i + 1);
|
|
85
88
|
}
|
|
89
|
+
// Recover argv from raw /proc/<pid>/cmdline. The kernel emits NUL-separated
|
|
90
|
+
// argv, which is the disambiguating form (a flag value can contain spaces, so
|
|
91
|
+
// only the NUL boundaries reliably separate one argv element from the next).
|
|
92
|
+
// Chromium, however, rewrites its OWN /proc/self/cmdline shortly after launch
|
|
93
|
+
// into a single space-joined blob with no NUL separators — so a steady-state
|
|
94
|
+
// chromium holding the cdpPort splits into one token whose basename is not a
|
|
95
|
+
// chromium name, defeating holder detection.
|
|
96
|
+
//
|
|
97
|
+
// The whitespace fallback is scoped to exactly that proven case: it fires only
|
|
98
|
+
// when NUL-splitting yields a single token AND that token's leading word is a
|
|
99
|
+
// chromium binary. Xtigervnc and websockify do not rewrite their argv (they
|
|
100
|
+
// stay NUL-separated, confirmed in production), so they never reach the
|
|
101
|
+
// fallback. Gating on the chromium basename is what keeps an unrelated
|
|
102
|
+
// argv-rewriting process from being whitespace-split into a spurious holder.
|
|
103
|
+
// Chromium profile paths are `~/.<brand>/chromium-profile` and never contain
|
|
104
|
+
// spaces, so re-splitting the blob on whitespace recovers a clean argv.
|
|
105
|
+
function splitCmdline(cmdline) {
|
|
106
|
+
const nulArgv = cmdline.split("\0").filter(s => s.length > 0);
|
|
107
|
+
if (nulArgv.length === 1 && /\s/.test(nulArgv[0])) {
|
|
108
|
+
const wsArgv = nulArgv[0].split(/\s+/).filter(s => s.length > 0);
|
|
109
|
+
if (wsArgv.length > 0 && CHROMIUM_BASENAMES.has(basename(wsArgv[0]))) {
|
|
110
|
+
return wsArgv;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return nulArgv;
|
|
114
|
+
}
|
|
86
115
|
function detectHolderType(argv) {
|
|
87
116
|
if (argv.length === 0)
|
|
88
117
|
return "unknown";
|
package/package.json
CHANGED
|
@@ -101,6 +101,31 @@ describe('admin-access-password', () => {
|
|
|
101
101
|
expect(await accessPasswordCollides('anything', corrupt, 'b')).toBe(false)
|
|
102
102
|
})
|
|
103
103
|
|
|
104
|
+
it('setAccessPassword throws and writes nothing when the password collides with another admin', async () => {
|
|
105
|
+
const f = tmpUsers([{ userId: 'a', pin: 'x' }, { userId: 'b', pin: 'y' }])
|
|
106
|
+
await setAccessPassword('a', 'Sw0rdfish!', f, aud(f))
|
|
107
|
+
const before = readFileSync(f, 'utf-8')
|
|
108
|
+
await expect(setAccessPassword('b', 'Sw0rdfish!', f, aud(f))).rejects.toThrow(/in use by another admin/)
|
|
109
|
+
// b gained no hash; a's hash is untouched; the file is byte-identical
|
|
110
|
+
expect(readFileSync(f, 'utf-8')).toBe(before)
|
|
111
|
+
// the rejected write emits no audit line (only a's earlier set-access is logged)
|
|
112
|
+
expect(readAudit(aud(f).logFile)).toHaveLength(1)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('setAccessPassword allows a password no other admin holds', async () => {
|
|
116
|
+
const f = tmpUsers([{ userId: 'a', pin: 'x' }, { userId: 'b', pin: 'y' }])
|
|
117
|
+
await setAccessPassword('a', 'Sw0rdfish!', f, aud(f))
|
|
118
|
+
await setAccessPassword('b', 'Different1!', f, aud(f))
|
|
119
|
+
expect(await verifyAccessPassword('Different1!', f)).toBe('b')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('setAccessPassword allows the same admin to re-set a password only they hold', async () => {
|
|
123
|
+
const f = tmpUsers([{ userId: 'a', pin: 'x' }, { userId: 'b', pin: 'y' }])
|
|
124
|
+
await setAccessPassword('a', 'Sw0rdfish!', f, aud(f))
|
|
125
|
+
await setAccessPassword('a', 'Sw0rdfish!', f, aud(f)) // self-rotation, not a collision
|
|
126
|
+
expect(await verifyAccessPassword('Sw0rdfish!', f)).toBe('a')
|
|
127
|
+
})
|
|
128
|
+
|
|
104
129
|
it('accessStoreUnreadable: false for absent/clean, true for corrupt (fail-closed signal)', () => {
|
|
105
130
|
const dir = mkdtempSync(join(tmpdir(), 'aapu-'))
|
|
106
131
|
const absent = join(dir, 'users.json')
|
|
@@ -22,7 +22,13 @@ export interface AuditContext {
|
|
|
22
22
|
}
|
|
23
23
|
export declare function hashAccessPassword(password: string): Promise<string>;
|
|
24
24
|
declare function matchesHash(password: string, stored: string): Promise<boolean>;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Write `accessHash` onto the userId's record. Throws if the record is absent,
|
|
27
|
+
* or if `password` already verifies against a DIFFERENT record — uniqueness is
|
|
28
|
+
* an invariant of this writer, so no path (including ones that skip the
|
|
29
|
+
* call-site pre-check) can create a credential a login scan would resolve
|
|
30
|
+
* ambiguously.
|
|
31
|
+
*/
|
|
26
32
|
export declare function setAccessPassword(userId: string, password: string, usersFile: string, audit: AuditContext): Promise<void>;
|
|
27
33
|
/** Remove `accessHash` from the userId's record. No-op (no write, no audit
|
|
28
34
|
* line) when the record is absent or already carries no accessHash. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACrB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AA+CD,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI1E;AAED,iBAAe,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAS7E;AAiBD
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACrB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AA+CD,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI1E;AAED,iBAAe,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAS7E;AAiBD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAa/H;AAED;wEACwE;AACxE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAYhG;AAED,2DAA2D;AAC3D,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAStG;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED,2DAA2D;AAC3D,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAItE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAGhE;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,gBAAgB,GAAG,eAAe,GAAG,qBAAqB,CAAA;AAEnG;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,YAAY,GAClB,aAAa,CAkBf;AAED;kEACkE;AAClE,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAItE;AAED,OAAO,EAAE,WAAW,IAAI,mBAAmB,EAAE,CAAA"}
|
|
@@ -96,12 +96,23 @@ function writeUsersAtomic(usersFile, users) {
|
|
|
96
96
|
(0, node_fs_1.writeFileSync)(tmp, JSON.stringify(users, null, 2) + '\n', { mode: 0o600 });
|
|
97
97
|
(0, node_fs_1.renameSync)(tmp, usersFile);
|
|
98
98
|
}
|
|
99
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Write `accessHash` onto the userId's record. Throws if the record is absent,
|
|
101
|
+
* or if `password` already verifies against a DIFFERENT record — uniqueness is
|
|
102
|
+
* an invariant of this writer, so no path (including ones that skip the
|
|
103
|
+
* call-site pre-check) can create a credential a login scan would resolve
|
|
104
|
+
* ambiguously.
|
|
105
|
+
*/
|
|
100
106
|
async function setAccessPassword(userId, password, usersFile, audit) {
|
|
101
107
|
const users = readUsers(usersFile);
|
|
102
108
|
const idx = users.findIndex(u => u.userId === userId);
|
|
103
109
|
if (idx === -1)
|
|
104
110
|
throw new Error(`no users.json record for userId=${userId}`);
|
|
111
|
+
// Refuse a cross-admin collision before hashing or writing — a self-rotation
|
|
112
|
+
// (same userId already holding this password) is excluded and still allowed.
|
|
113
|
+
if (await accessPasswordCollides(password, usersFile, userId)) {
|
|
114
|
+
throw new Error('accessHash collision: password already in use by another admin');
|
|
115
|
+
}
|
|
105
116
|
const rows = rowIds(users);
|
|
106
117
|
users[idx] = { ...users[idx], accessHash: await hashAccessPassword(password) };
|
|
107
118
|
writeUsersAtomic(usersFile, users);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAiFA,gDAIC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -111,11 +111,22 @@ function writeUsersAtomic(usersFile: string, users: AccessUserEntry[]): void {
|
|
|
111
111
|
renameSync(tmp, usersFile)
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Write `accessHash` onto the userId's record. Throws if the record is absent,
|
|
116
|
+
* or if `password` already verifies against a DIFFERENT record — uniqueness is
|
|
117
|
+
* an invariant of this writer, so no path (including ones that skip the
|
|
118
|
+
* call-site pre-check) can create a credential a login scan would resolve
|
|
119
|
+
* ambiguously.
|
|
120
|
+
*/
|
|
115
121
|
export async function setAccessPassword(userId: string, password: string, usersFile: string, audit: AuditContext): Promise<void> {
|
|
116
122
|
const users = readUsers(usersFile)
|
|
117
123
|
const idx = users.findIndex(u => u.userId === userId)
|
|
118
124
|
if (idx === -1) throw new Error(`no users.json record for userId=${userId}`)
|
|
125
|
+
// Refuse a cross-admin collision before hashing or writing — a self-rotation
|
|
126
|
+
// (same userId already holding this password) is excluded and still allowed.
|
|
127
|
+
if (await accessPasswordCollides(password, usersFile, userId)) {
|
|
128
|
+
throw new Error('accessHash collision: password already in use by another admin')
|
|
129
|
+
}
|
|
119
130
|
const rows = rowIds(users)
|
|
120
131
|
users[idx] = { ...users[idx], accessHash: await hashAccessPassword(password) }
|
|
121
132
|
writeUsersAtomic(usersFile, users)
|
|
@@ -26,7 +26,7 @@ The HMAC secret lives at `~/.${brand}/credentials/admin-session-secret` (mode `0
|
|
|
26
26
|
|
|
27
27
|
**Observability.** `[session-rehydrate-from-token] sessionKey=… accountId=… userId=… ageMs=…` fires once per successful rehydrate in `server.log`. A tampered token fails HMAC and returns `invalid-token` silently (no log line — would otherwise be a noise/attack-amplifier surface); the request's downstream `[session] middleware-reject status=401 code=session-not-registered` is sufficient.
|
|
28
28
|
|
|
29
|
-
**Adjacent 401 surface — remote-auth gate.** The PIN-session reject-code taxonomy (`session-missing`, `session-not-registered`, `session-expired-age`, `grant-expired` — the `AdminRejectCode` union in `platform/ui/app/lib/useAdminFetch.ts`) shares its 401 transport with one code from a different security domain: the remote-auth gate (`server/index.ts`, the `__remote_session` cookie's 24h TTL) answers an expired/missing/invalid remote-auth token on any `/api/*` path with `401 {"code":"remote-auth-required"}` — document navigations keep getting the login HTML with a redirect. Client handling is deliberately split: the PIN codes route to enter-pin recovery, while `remote-auth-required` triggers a full page reload (the gate then serves the login page for the current path), because a PIN screen behind an expired remote-auth token cannot reach its own API. The gate's log line is `[remote-auth] login required … respond=401|html`.
|
|
29
|
+
**Adjacent 401 surface — remote-auth gate.** The PIN-session reject-code taxonomy (`session-missing`, `session-not-registered`, `session-expired-age`, `grant-expired` — the `AdminRejectCode` union in `platform/ui/app/lib/useAdminFetch.ts`) shares its 401 transport with one code from a different security domain: the remote-auth gate (`server/index.ts`, the `__remote_session` cookie's 24h TTL) answers an expired/missing/invalid remote-auth token on any `/api/*` path with `401 {"code":"remote-auth-required"}` — document navigations keep getting the login HTML with a redirect. Client handling is deliberately split: the PIN codes route to enter-pin recovery, while `remote-auth-required` triggers a full page reload (the gate then serves the login page for the current path), because a PIN screen behind an expired remote-auth token cannot reach its own API. Every client site that POSTs to this 401 surface honours the split: `useAdminFetch`, the auth heartbeat, and the pre-auth login/onboarding POSTs in `useAdminAuth.ts` (`doLogin` → `POST /api/admin/session`; `handleSetPin`/`handleChangePin` → `POST`/`DELETE /api/onboarding/set-pin`). The login POST is the one most likely to hit an expired gate first, since the 24h TTL can lapse while the operator sits on `enter-pin`; treating its `remote-auth-required` 401 as a bad PIN (the pre-fix behaviour) left the operator stuck on "Invalid PIN" with the correct PIN. The gate's log line is `[remote-auth] login required … respond=401|html`.
|
|
30
30
|
|
|
31
31
|
## SDK-resume contract on PIN-rebind
|
|
32
32
|
|
|
@@ -73,6 +73,10 @@ const ALLOWLIST = new Set([
|
|
|
73
73
|
// before the `data-path` mock fires, so the serve endpoint reads real
|
|
74
74
|
// sidecar fixtures written under it.
|
|
75
75
|
'platform/ui/server/routes/__tests__/public-reader.test.ts',
|
|
76
|
+
// Same vi.hoisted pattern: the set-pin-identity test builds tmpdir users/audit
|
|
77
|
+
// paths inside the hoisted block (vi.mock factories hoist above the top-level
|
|
78
|
+
// path/os imports, so the block requires node builtins directly).
|
|
79
|
+
'platform/ui/server/routes/__tests__/set-pin-identity.test.ts',
|
|
76
80
|
])
|
|
77
81
|
|
|
78
82
|
const REQUIRE_CALL_RE = /\brequire\s*\(/
|
|
@@ -2829,6 +2829,9 @@ async function setAccessPassword(userId, password, usersFile, audit) {
|
|
|
2829
2829
|
const users = readUsers(usersFile);
|
|
2830
2830
|
const idx = users.findIndex((u) => u.userId === userId);
|
|
2831
2831
|
if (idx === -1) throw new Error(`no users.json record for userId=${userId}`);
|
|
2832
|
+
if (await accessPasswordCollides(password, usersFile, userId)) {
|
|
2833
|
+
throw new Error("accessHash collision: password already in use by another admin");
|
|
2834
|
+
}
|
|
2832
2835
|
const rows = rowIds(users);
|
|
2833
2836
|
users[idx] = { ...users[idx], accessHash: await hashAccessPassword(password) };
|
|
2834
2837
|
writeUsersAtomic(usersFile, users);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as e}from"./chunk-CAM3fms7.js";import{A as t,C as n,U as r,b as i,x as a,z as o}from"./OperatorConversations-BJY9FZbr.js";import{i as s}from"./admin-types-DJoj6VJv.js";import{_ as c,u as l}from"./AdminShell-Bc9F7X_N.js";import{t as u}from"./Checkbox-DoQV18Ow.js";var d=`admin-landing-redirected`,f=`/graph`;function p(e){return e.variant===`operator`?!1:e.appState===`chat`&&!e.alreadyRedirected}var m=e(r(),1);function h(e=`admin`){let[t,n]=(0,m.useState)(`loading`),[r,i]=(0,m.useState)(``),[a,l]=(0,m.useState)(``),[u,h]=(0,m.useState)(``),[g,_]=(0,m.useState)(!1),[v,y]=(0,m.useState)(!1),[b,x]=(0,m.useState)(!1),[S,C]=(0,m.useState)(!1),[w,T]=(0,m.useState)(!1),[E,D]=(0,m.useState)(null),[O,k]=(0,m.useState)(null),[A,j]=(0,m.useState)(void 0),[M,N]=(0,m.useState)(null),[P,F]=(0,m.useState)(void 0),[I,L]=(0,m.useState)(null),[ee,R]=(0,m.useState)(null),[z,B]=(0,m.useState)([]),[V,H]=(0,m.useState)(!1),[U,W]=(0,m.useState)(void 0),G=(0,m.useRef)(void 0),[K,q]=(0,m.useState)(!1);(0,m.useEffect)(()=>{typeof window>`u`||fetch(`/api/remote-auth/status`).then(e=>e.ok?e.json():null).then(e=>{e?.configured&&q(!0)}).catch(()=>{})},[]);let J=(0,m.useRef)(null),Y=(0,m.useRef)(null);(0,m.useEffect)(()=>{async function e(){let e=null;try{e=sessionStorage.getItem(`maxy-admin-session-key`)}catch{}if(!e)return!1;try{let t=await fetch(`/api/admin/session?session_key=${encodeURIComponent(e)}`);if(t.status===401){try{sessionStorage.removeItem(`maxy-admin-session-key`)}catch{}return!1}if(!t.ok)return!1;let r=await t.json();D(r.session_key),R(r.sessionId??null),j(r.businessName),N(r.role??null),F(r.userName===void 0?null:r.userName),L(r.avatar??null);let i=s(r.thinkingView);return G.current=i,W(i),n(`chat`),!0}catch(e){return console.error(`[admin] session restore failed:`,e),!1}}async function t(r=2){try{let i=await fetch(`/api/health`);if(!i.ok){if(r>0)return await new Promise(e=>setTimeout(e,1500)),t(r-1);console.error(`[admin] health check returned ${i.status} after retries`),n(`set-pin`);return}let a=await i.json();if(!a.pin_configured){n(`set-pin`);return}if(!a.claude_authenticated){n(`connect-claude`);return}if(await e())return;n(`enter-pin`)}catch(e){if(r>0)return await new Promise(e=>setTimeout(e,1500)),t(r-1);console.error(`[admin] health check failed:`,e),n(`set-pin`)}}t()},[]),(0,m.useEffect)(()=>{t===`chat`&&fetch(`/api/admin/claude-info`).then(e=>{if(e.ok)return e.json()}).then(e=>{e&&k(e)}).catch(()=>{})},[t]),(0,m.useEffect)(()=>{if(typeof window>`u`)return;let n=!1;try{n=sessionStorage.getItem(d)===`1`}catch{}if(p({appState:t,alreadyRedirected:n,variant:e})){try{sessionStorage.setItem(d,`1`)}catch{}console.info(`[admin-ui] landing-redirect target=${f}`),window.location.replace(f)}},[t,e]);let X=(0,m.useRef)(null);(0,m.useEffect)(()=>{if(t!==`chat`)return;let e=setInterval(async()=>{try{let e=await fetch(`/api/health`);if(e.ok){let t=await e.json();if(t.auth_status===`dead`||t.auth_status===`missing`){n(`connect-claude`);return}}}catch{}if(E)try{let e=await fetch(`/api/admin/session?session_key=${encodeURIComponent(E)}`);if(e.status!==401)return;let t=(await e.clone().json().catch(()=>null))?.code??`unknown-401`;if(t===`remote-auth-required`){o(`heartbeat`,`/api/admin/session`);return}console.warn(`[admin-auth] outcome=heartbeat-detected-expiry code=${t}`),X.current?.()}catch{}},300*1e3);return()=>clearInterval(e)},[t,E]),(0,m.useEffect)(()=>{t===`connect-claude`&&fetch(`/api/health`).then(e=>e.ok?e.json():null).then(e=>{e?.claude_authenticated&&n(`enter-pin`)}).catch(()=>{})},[t]);async function Z(e,t){y(!0);try{let r=await fetch(`/api/admin/session`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({pin:e,...t?{accountId:t}:{}})});if(!r.ok){let e=await r.json().catch(()=>({}));if(r.status===401&&e?.code===`remote-auth-required`){o(`enter-pin`,`/api/admin/session`);return}h(e.error||`Invalid PIN`);return}let a=await r.json();if(a.accounts&&!a.session_key){console.log(`[admin] account picker shown: userId=${a.userId} accountCount=${a.accounts.length}`),B(a.accounts),n(`account-picker`);return}D(a.session_key),R(a.sessionId??null),j(a.businessName),N(a.role??null),F(a.userName===void 0?null:a.userName),L(a.avatar??null);let c=s(a.thinkingView);if(G.current=c,W(c),t)try{sessionStorage.setItem(`maxy-account-id`,t)}catch{}try{sessionStorage.setItem(`maxy-admin-session-key`,a.session_key)}catch{}i(``),n(`chat`)}catch(e){console.error(`[admin] connection error:`,e),h(`Could not connect.`)}finally{y(!1),H(!1)}}let Q=(0,m.useCallback)(async e=>{if(e.preventDefault(),v)return;h(``);let t=a.trim();if(!t){h(`Please enter your name.`);return}if(r.length<4){h(`PIN must be at least 4 characters.`);return}let s=r;y(!0);try{let e=await fetch(`/api/onboarding/set-pin`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({pin:s,name:t})});if(!e.ok){let t=await e.json().catch(()=>({}));if(e.status===401&&t?.code===`remote-auth-required`){o(`set-pin`,`/api/onboarding/set-pin`);return}if(e.status===409){console.log(`[admin] PIN already configured — re-checking health`);try{let e=await fetch(`/api/health`);if(e.ok){let r=await e.json();r.pin_configured&&r.claude_authenticated?n(`enter-pin`):r.pin_configured?n(`connect-claude`):h(t.error||`Failed to set PIN.`)}else n(`enter-pin`)}catch{n(`enter-pin`)}return}h(t.error||`Failed to set PIN.`);return}let r=await fetch(`/api/health`);if((r.ok?await r.json():null)?.claude_authenticated){await Z(s);return}i(``),n(`connect-claude`)}catch(e){console.error(`[admin] connection error:`,e),h(`Could not connect.`)}finally{y(!1)}},[r,v,a]),te=(0,m.useCallback)(async e=>{e.preventDefault(),h(``),await Z(r)},[r]),ne=(0,m.useCallback)(async()=>{T(!0);try{if(!await c())return console.warn(`[admin-ui] claude-disconnect not verified — credentials may persist; staying put`),!1;D(null),N(null),F(void 0),L(null);try{sessionStorage.removeItem(`maxy-admin-session-key`),sessionStorage.removeItem(`maxy-account-id`),sessionStorage.removeItem(d)}catch{}return n(`connect-claude`),!0}finally{T(!1)}},[]),$=(0,m.useCallback)(()=>{D(null),N(null),F(void 0),L(null);try{sessionStorage.removeItem(`maxy-admin-session-key`),sessionStorage.removeItem(`maxy-account-id`),sessionStorage.removeItem(d)}catch{}i(``),h(``),n(`enter-pin`)},[]);return(0,m.useEffect)(()=>{X.current=$},[$]),{appState:t,setAppState:n,pin:r,setPin:i,operatorName:a,setOperatorName:l,pinError:u,setPinError:h,showPin:g,setShowPin:_,pinLoading:v,authPolling:b,setAuthPolling:x,authLoading:S,setAuthLoading:C,disconnecting:w,cacheKey:E,setCacheKey:D,claudeInfo:O,setClaudeInfo:k,businessName:A,role:M,userName:P,userAvatar:I,sessionId:ee,setSessionId:R,accounts:z,accountPickerLoading:V,expandAll:U,setExpandAll:W,expandAllDefaultRef:G,remoteAuthEnabled:K,pinInputRef:J,setPinFormRef:Y,handleSetPin:Q,handleLogin:te,handleAccountSelect:(0,m.useCallback)(async e=>{H(!0),h(``),await Z(r,e)},[r]),handleDisconnect:ne,handleLogout:$}}var g=n();function _({inputRef:e,value:t,onChange:n,onComplete:r,showPin:i,autoFocus:a}){let o=(0,m.useRef)([]);function s(e,r){r.key===`Backspace`?(r.preventDefault(),t[e]?n(t.slice(0,e)+t.slice(e+1)):e>0&&(n(t.slice(0,e-1)+t.slice(e)),o.current[e-1]?.focus())):r.key===`ArrowLeft`&&e>0?o.current[e-1]?.focus():r.key===`ArrowRight`&&e<5?o.current[e+1]?.focus():r.key===`Enter`&&(r.preventDefault(),r.currentTarget.form?.requestSubmit())}function c(e,i){let a=i.nativeEvent.data;if(!a||!/^\d$/.test(a))return;let s=t.split(``);for(s[e]=a;s.length<e;)s.push(``);let c=s.join(``).replace(/\D/g,``).slice(0,6);n(c),c.length===6?r?.(c):e<5&&o.current[e+1]?.focus()}function l(e){e.preventDefault();let t=e.clipboardData.getData(`text`).replace(/\D/g,``).slice(0,6);t&&(n(t),t.length===6?r?.(t):o.current[t.length]?.focus())}return(0,g.jsx)(`div`,{className:`pin-field`,children:Array.from({length:6}).map((n,r)=>(0,g.jsx)(`input`,{ref:t=>{o.current[r]=t,r===0&&e&&(e.current=t)},type:`text`,inputMode:`numeric`,className:`pin-box${t[r]?` pin-box-filled`:``}`,value:t[r]?i?t[r]:`•`:``,onKeyDown:e=>s(r,e),onInput:e=>c(r,e),onPaste:l,onFocus:e=>e.target.select(),autoFocus:a&&r===0,autoComplete:`off`,maxLength:1,"aria-label":`PIN digit ${r+1}`},r))})}function v(e){let{pin:t,setPin:n,showPin:r,setShowPin:o,pinLoading:s,pinError:c,pinInputRef:d,setPinFormRef:f,onSubmit:p,operatorName:m,setOperatorName:h}=e;return(0,g.jsx)(`div`,{className:`connect-page`,children:(0,g.jsxs)(`div`,{className:`connect-content`,children:[(0,g.jsx)(`img`,{src:a,alt:i.productName,className:`connect-logo connect-logo--maxy`}),!i.logoContainsName&&(0,g.jsxs)(`h1`,{className:`connect-title`,children:[`Welcome to `,i.productName]}),(0,g.jsxs)(`p`,{className:`connect-subtitle`,children:[`Tell `,i.productName,` who you are, then choose a PIN.`]}),(0,g.jsxs)(`form`,{ref:f,onSubmit:p,className:`connect-pin-form`,children:[(0,g.jsxs)(`div`,{className:`pin-input-row`,children:[(0,g.jsx)(`input`,{type:`text`,className:`connect-name-input`,placeholder:`Your full name`,value:m,onChange:e=>h(e.target.value),autoComplete:`name`,autoFocus:!0,required:!0,"aria-label":`Your full name`}),(0,g.jsx)(`div`,{style:{width:38,flexShrink:0},"aria-hidden":`true`})]}),(0,g.jsxs)(`div`,{className:`pin-input-row`,children:[(0,g.jsx)(_,{inputRef:d,value:t,onChange:n,onComplete:()=>{},showPin:r}),(0,g.jsx)(l,{variant:`send`,type:`submit`,disabled:!t||!m.trim(),loading:s,"aria-label":`Set PIN`,children:(0,g.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,g.jsx)(`line`,{x1:`5`,y1:`12`,x2:`19`,y2:`12`}),(0,g.jsx)(`polyline`,{points:`12 5 19 12 12 19`})]})})]}),(0,g.jsx)(u,{checked:r,onChange:()=>o(e=>!e),label:`Show PIN`})]}),c&&(0,g.jsx)(`p`,{className:`admin-pin-error`,children:c})]})})}function y(e){let{pin:t,setPin:n,showPin:r,setShowPin:o,pinLoading:s,pinError:c,pinInputRef:d,onSubmit:f,remoteAuthEnabled:p,onSignOutRemote:m}=e;return(0,g.jsxs)(`div`,{className:`connect-page`,children:[p&&m&&(0,g.jsx)(`button`,{type:`button`,className:`connect-signout`,onClick:m,children:`Sign out`}),(0,g.jsxs)(`div`,{className:`connect-content`,children:[(0,g.jsx)(`img`,{src:a,alt:i.productName,className:`connect-logo connect-logo--maxy`}),!i.logoContainsName&&(0,g.jsx)(`h1`,{className:`connect-title`,children:i.productName}),(0,g.jsxs)(`form`,{onSubmit:f,className:`connect-pin-form`,children:[(0,g.jsxs)(`div`,{className:`pin-input-row`,children:[(0,g.jsx)(_,{inputRef:d,value:t,onChange:n,onComplete:()=>{},showPin:r,autoFocus:!0}),(0,g.jsx)(l,{variant:`send`,type:`submit`,disabled:!t,loading:s,children:(0,g.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,g.jsx)(`line`,{x1:`5`,y1:`12`,x2:`19`,y2:`12`}),(0,g.jsx)(`polyline`,{points:`12 5 19 12 12 19`})]})})]}),(0,g.jsx)(`div`,{className:`pin-options`,children:(0,g.jsx)(u,{checked:r,onChange:()=>o(e=>!e),label:`Show PIN`})})]}),c&&(0,g.jsx)(`p`,{className:`admin-pin-error`,children:c})]})]})}function b(e){let{accounts:n,loading:r,error:o,onSelect:s}=e;return(0,g.jsx)(`div`,{className:`connect-page`,children:(0,g.jsxs)(`div`,{className:`connect-content`,children:[(0,g.jsx)(`img`,{src:a,alt:i.productName,className:`connect-logo connect-logo--maxy`}),!i.logoContainsName&&(0,g.jsx)(`h1`,{className:`connect-title`,children:i.productName}),(0,g.jsx)(`p`,{className:`connect-subtitle`,children:`Select an account`}),(0,g.jsx)(`div`,{className:`account-picker-list`,children:n.map(e=>(0,g.jsxs)(`button`,{className:`account-picker-card`,onClick:()=>s(e.accountId),disabled:r,type:`button`,children:[(0,g.jsx)(`span`,{className:`account-picker-name`,children:e.businessName||e.accountId}),(0,g.jsx)(`span`,{className:`account-picker-role`,children:e.role}),r&&(0,g.jsx)(t,{className:`account-picker-spinner`,size:16})]},e.accountId))}),o&&(0,g.jsx)(`p`,{className:`admin-pin-error`,children:o})]})})}function x(e){let{authPolling:t,setAuthPolling:n,authLoading:r,setAuthLoading:o,pinError:s,setPinError:c,setAppState:u}=e,[d,f]=(0,m.useState)(!1),[p,h]=(0,m.useState)(!1);async function _(){h(!0),c(``);try{let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`launch-browser`})})).json();e.launched?f(!0):e.error&&c(e.error)}catch(e){console.error(`[admin] browser launch error:`,e),c(`Could not launch browser.`)}h(!1)}async function v(){o(!0),c(``);try{let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`})).json();if(e.started){n(!0),f(!0),o(!1);for(let e=0;e<120;e++)if(await new Promise(e=>setTimeout(e,2e3)),(await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`wait`})})).json()).authenticated){await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`stop`})}),u(`enter-pin`);return}c(`Timed out waiting for sign-in. Try again.`),n(!1)}else e.error&&c(e.error)}catch(e){console.error(`[admin] auth flow error:`,e),c(`Could not start auth flow.`)}o(!1)}async function y(){await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`stop`})}),n(!1),c(``)}return t||d?(0,g.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,height:`100dvh`,overflow:`auto`},children:[(0,g.jsxs)(`header`,{className:`chat-header`,style:{paddingBottom:`12px`,flexShrink:0,position:`relative`,maxWidth:`680px`,width:`100%`,margin:`0 auto`,padding:`24px 20px 12px`},children:[t?(0,g.jsx)(`button`,{onClick:y,style:{position:`absolute`,top:`12px`,right:`12px`,background:`none`,border:`none`,color:`#999`,fontSize:`13px`,cursor:`pointer`,padding:`4px 8px`},"aria-label":`Cancel`,children:`✕`}):(0,g.jsx)(`button`,{onClick:()=>f(!1),style:{position:`absolute`,top:`12px`,right:`12px`,background:`none`,border:`none`,color:`#999`,fontSize:`13px`,cursor:`pointer`,padding:`4px 8px`},"aria-label":`Close browser`,children:`✕`}),(0,g.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`chat-logo`}),(0,g.jsx)(`h1`,{className:`chat-tagline`,children:`Connect Claude`}),(0,g.jsx)(`p`,{className:`chat-intro`,children:t?`Sign in and authorize in the browser below.`:`Open your email or prepare your accounts, then sign in.`}),!t&&(0,g.jsx)(`div`,{style:{marginTop:`12px`},children:(0,g.jsx)(l,{variant:`primary`,onClick:v,disabled:r,children:r?(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`span`,{className:`spin`,style:{display:`inline-block`},children:`✱`}),` Connecting…`]}):`Sign in to Claude`})})]}),(0,g.jsx)(`div`,{style:{flex:1,display:`flex`,flexDirection:`column`,minHeight:0,gap:`10px`,padding:`0 0 16px`},children:(0,g.jsx)(`iframe`,{src:`/vnc-viewer.html`,style:{flex:1,width:`100%`,minHeight:0,border:`none`,background:`#111`,display:`block`},title:`Claude Sign-in`})}),s&&(0,g.jsx)(`p`,{className:`admin-pin-error`,style:{textAlign:`center`,padding:`0 20px 16px`},children:s})]}):(0,g.jsx)(`div`,{className:`connect-page`,children:(0,g.jsxs)(`div`,{className:`connect-content`,children:[(0,g.jsxs)(`div`,{className:`connect-logos`,children:[(0,g.jsx)(`div`,{className:`connect-logo-wrap`,children:(0,g.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`connect-logo`})}),(0,g.jsx)(`svg`,{className:`connect-arrow`,viewBox:`0 0 48 24`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:(0,g.jsx)(`path`,{d:`M0 12h44m0 0l-8-8m8 8l-8 8`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`})}),(0,g.jsxs)(`div`,{className:`connect-logo-wrap`,children:[(0,g.jsx)(`img`,{src:a,alt:i.productName,className:`connect-logo connect-logo--maxy`}),!i.logoContainsName&&(0,g.jsx)(`span`,{className:`connect-logo-label`,children:i.productName})]})]}),(0,g.jsxs)(`h1`,{className:`connect-title`,children:[`Connect Claude to power `,i.productName]}),(0,g.jsx)(`p`,{className:`connect-subtitle`,children:`Sign in with your Anthropic account to get started.`}),(0,g.jsx)(l,{variant:`primary`,onClick:v,disabled:r,children:r?(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`span`,{className:`spin`,style:{display:`inline-block`},children:`✱`}),` Connecting…`]}):`Sign in to Claude`}),(0,g.jsx)(`p`,{style:{marginTop:`6px`,fontSize:`11px`,color:`#999`,maxWidth:`300px`,textAlign:`center`,lineHeight:`1.4`},children:`First time? You may need to sign into your email and Anthropic account in the browser before connecting.`}),(0,g.jsx)(`button`,{onClick:_,disabled:p,style:{marginTop:`12px`,background:`none`,border:`none`,color:`var(--color-primary, #666)`,fontSize:`13px`,cursor:`pointer`,textDecoration:`underline`,textUnderlineOffset:`3px`},children:p?`Launching…`:`Open browser first`}),s&&(0,g.jsx)(`p`,{className:`admin-pin-error`,children:s})]})})}function S({auth:e}){return e.appState===`loading`?(0,g.jsx)(`div`,{className:`connect-page`}):e.appState===`set-pin`?(0,g.jsx)(v,{pin:e.pin,setPin:e.setPin,showPin:e.showPin,setShowPin:e.setShowPin,pinLoading:e.pinLoading,pinError:e.pinError,pinInputRef:e.pinInputRef,setPinFormRef:e.setPinFormRef,onSubmit:e.handleSetPin,operatorName:e.operatorName,setOperatorName:e.setOperatorName}):e.appState===`connect-claude`?(0,g.jsx)(x,{authPolling:e.authPolling,setAuthPolling:e.setAuthPolling,authLoading:e.authLoading,setAuthLoading:e.setAuthLoading,pinError:e.pinError,setPinError:e.setPinError,setAppState:e.setAppState}):e.appState===`enter-pin`?(0,g.jsx)(y,{pin:e.pin,setPin:e.setPin,showPin:e.showPin,setShowPin:e.setShowPin,pinLoading:e.pinLoading,pinError:e.pinError,pinInputRef:e.pinInputRef,onSubmit:e.handleLogin,remoteAuthEnabled:e.remoteAuthEnabled,onSignOutRemote:()=>{console.info(`[admin-ui] remote-auth sign-out → /__remote-auth/logout`),window.location.href=`/__remote-auth/logout`}}):e.appState===`account-picker`?(0,g.jsx)(b,{accounts:e.accounts,loading:e.accountPickerLoading,error:e.pinError,onSelect:e.handleAccountSelect}):null}export{h as n,S as t};
|