@certalis/agent-smith 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +280 -67
- package/dist/post-processor.worker.js +41 -10
- package/dist/server.js +280 -67
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1275,12 +1275,30 @@ function parseNumber(value) {
|
|
|
1275
1275
|
const parsed = Number(value);
|
|
1276
1276
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1277
1277
|
}
|
|
1278
|
+
function normalizeEnrollmentEmail(value) {
|
|
1279
|
+
if (typeof value !== "string")
|
|
1280
|
+
return;
|
|
1281
|
+
const email = value.trim().toLowerCase();
|
|
1282
|
+
return email.length <= 254 && patterns.email.test(email) ? email : undefined;
|
|
1283
|
+
}
|
|
1284
|
+
function sanitizeEnrollmentEmails(value) {
|
|
1285
|
+
if (!Array.isArray(value))
|
|
1286
|
+
return;
|
|
1287
|
+
const emails = value.map(normalizeEnrollmentEmail).filter((email) => email !== undefined);
|
|
1288
|
+
return [...new Set(emails)].slice(0, MAX_ENROLLMENT_EMAILS);
|
|
1289
|
+
}
|
|
1278
1290
|
function sanitizeConfigData(value) {
|
|
1279
1291
|
if (!isRecord(value)) {
|
|
1280
1292
|
return {};
|
|
1281
1293
|
}
|
|
1282
1294
|
const sanitized = {};
|
|
1283
1295
|
for (const [key, entry] of Object.entries(value)) {
|
|
1296
|
+
if (key === "enrollment_emails") {
|
|
1297
|
+
const emails = sanitizeEnrollmentEmails(entry);
|
|
1298
|
+
if (emails)
|
|
1299
|
+
sanitized.enrollment_emails = emails;
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1284
1302
|
if (key === "team_routing") {
|
|
1285
1303
|
const teamRouting = sanitizeTeamRouting(entry);
|
|
1286
1304
|
if (teamRouting)
|
|
@@ -1430,7 +1448,7 @@ function sanitizeTeamRouting(value) {
|
|
|
1430
1448
|
}
|
|
1431
1449
|
return result;
|
|
1432
1450
|
}
|
|
1433
|
-
var log2, PROVIDER_STRATEGIES, BUILTIN_PROVIDER_CONFIGS, BUILTIN_PROVIDER_CONFIG_BY_NAME, PROVIDER_NAME_PATTERN, TEAM_ROUTING_DEFAULTS, Config;
|
|
1451
|
+
var log2, PROVIDER_STRATEGIES, BUILTIN_PROVIDER_CONFIGS, BUILTIN_PROVIDER_CONFIG_BY_NAME, PROVIDER_NAME_PATTERN, TEAM_ROUTING_DEFAULTS, MAX_ENROLLMENT_EMAILS = 200, Config;
|
|
1434
1452
|
var init_src4 = __esm(() => {
|
|
1435
1453
|
init_src2();
|
|
1436
1454
|
init_src3();
|
|
@@ -1674,11 +1692,28 @@ var init_src4 = __esm(() => {
|
|
|
1674
1692
|
this.persistAccountAllowlist(current.filter((uuid) => uuid !== accountUuid));
|
|
1675
1693
|
}
|
|
1676
1694
|
persistAccountAllowlist(accountUuids) {
|
|
1677
|
-
|
|
1695
|
+
this.persistStrictly("team_routing", {
|
|
1678
1696
|
...this.data.team_routing,
|
|
1679
1697
|
allowlisted_account_uuids: accountUuids
|
|
1680
|
-
};
|
|
1681
|
-
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
getEnrollmentEmails() {
|
|
1701
|
+
return [...this.data.enrollment_emails ?? []];
|
|
1702
|
+
}
|
|
1703
|
+
setEnrollmentEmails(emails) {
|
|
1704
|
+
const normalized = emails.map(normalizeEnrollmentEmail);
|
|
1705
|
+
if (normalized.some((email) => email === undefined)) {
|
|
1706
|
+
throw new ValidationError("Invalid enrollment email", "enrollment_emails");
|
|
1707
|
+
}
|
|
1708
|
+
const unique = [...new Set(normalized)];
|
|
1709
|
+
if (unique.length > MAX_ENROLLMENT_EMAILS) {
|
|
1710
|
+
throw new ValidationError(`At most ${MAX_ENROLLMENT_EMAILS} enrollment emails`, "enrollment_emails");
|
|
1711
|
+
}
|
|
1712
|
+
this.persistStrictly("enrollment_emails", unique);
|
|
1713
|
+
return [...unique];
|
|
1714
|
+
}
|
|
1715
|
+
persistStrictly(key, value) {
|
|
1716
|
+
const next = { ...this.data, [key]: value };
|
|
1682
1717
|
const temporary = `${this.configPath}.${crypto.randomUUID()}.tmp`;
|
|
1683
1718
|
try {
|
|
1684
1719
|
writeFileSync(temporary, JSON.stringify(next, null, 2), {
|
|
@@ -1689,13 +1724,9 @@ var init_src4 = __esm(() => {
|
|
|
1689
1724
|
} finally {
|
|
1690
1725
|
rmSync(temporary, { force: true });
|
|
1691
1726
|
}
|
|
1692
|
-
const oldValue = this.data
|
|
1727
|
+
const oldValue = this.data[key];
|
|
1693
1728
|
this.data = next;
|
|
1694
|
-
this.emit("change", {
|
|
1695
|
-
key: "team_routing",
|
|
1696
|
-
oldValue,
|
|
1697
|
-
newValue: teamRouting
|
|
1698
|
-
});
|
|
1729
|
+
this.emit("change", { key, oldValue, newValue: value });
|
|
1699
1730
|
}
|
|
1700
1731
|
getRuntime() {
|
|
1701
1732
|
const defaults = {
|
|
@@ -7014,72 +7045,157 @@ function enrollmentPage() {
|
|
|
7014
7045
|
return new Response(`<!doctype html>
|
|
7015
7046
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7016
7047
|
<title>Connect a Claude subscription</title>
|
|
7017
|
-
<style nonce="${nonce}">
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
</
|
|
7022
|
-
<
|
|
7023
|
-
<
|
|
7024
|
-
<
|
|
7025
|
-
<
|
|
7026
|
-
|
|
7027
|
-
<section
|
|
7028
|
-
<
|
|
7029
|
-
<
|
|
7030
|
-
<
|
|
7031
|
-
|
|
7048
|
+
<style nonce="${nonce}">${styles}</style></head><body>
|
|
7049
|
+
<header>Shared agents pool<small>Connect a Claude subscription</small><div class="bar"><span id="progress"></span></div></header>
|
|
7050
|
+
<main>
|
|
7051
|
+
<ol id="stepper" aria-label="Progress">
|
|
7052
|
+
<li data-step="email"><span class="dot">1</span><span class="label">Email</span></li>
|
|
7053
|
+
<li data-step="signin"><span class="dot">2</span><span class="label">Sign in</span></li>
|
|
7054
|
+
<li data-step="code"><span class="dot">3</span><span class="label">Paste code</span></li>
|
|
7055
|
+
<li data-step="confirm"><span class="dot">4</span><span class="label">Confirm</span></li>
|
|
7056
|
+
<li data-step="done"><span class="dot">5</span><span class="label">Done</span></li>
|
|
7057
|
+
</ol>
|
|
7058
|
+
<section class="card" id="step-email" hidden>
|
|
7059
|
+
<div class="avatar">${icon("mail")}</div>
|
|
7060
|
+
<h1 tabindex="-1">Connect your Claude subscription</h1>
|
|
7061
|
+
<p class="lead">This adds your subscription to the shared agents pool. Start with your work email: if it is on this pool's list, the next step lets you sign in with Claude.</p>
|
|
7062
|
+
<form id="request">
|
|
7063
|
+
<p class="notice err" id="email-error" role="alert" hidden></p>
|
|
7064
|
+
<label for="email">Work email<input id="email" type="email" autocomplete="email" spellcheck="false" required maxlength="254"></label>
|
|
7065
|
+
<button class="button" type="submit">Continue</button>
|
|
7066
|
+
</form>
|
|
7067
|
+
</section>
|
|
7068
|
+
<section class="card" id="step-signin" hidden>
|
|
7069
|
+
<p class="notice" id="signin-notice" role="status" hidden></p>
|
|
7070
|
+
<div class="avatar">${icon("key")}</div>
|
|
7071
|
+
<h1 tabindex="-1">Sign in with Claude</h1>
|
|
7072
|
+
<p class="lead" id="signin-for" hidden>Use the Claude account of <strong id="signin-email"></strong>. Only connect an account you have permission to share.</p>
|
|
7073
|
+
<ul class="facts">
|
|
7074
|
+
<li>Claude opens in a new tab. Sign in and pick the account to share.</li>
|
|
7075
|
+
<li>Your password stays with Claude. The pool only keeps the authorization it needs to run agents.</li>
|
|
7076
|
+
<li>Claude then shows a code: you paste it here in the next step.</li>
|
|
7077
|
+
</ul>
|
|
7078
|
+
<button class="button" id="start" type="button">Continue to Claude ${icon("open")}</button>
|
|
7079
|
+
</section>
|
|
7080
|
+
<section class="card" id="step-code" hidden>
|
|
7081
|
+
<div class="avatar">${icon("code")}</div>
|
|
7082
|
+
<h1 tabindex="-1">Paste the code from Claude</h1>
|
|
7083
|
+
<p class="lead" id="code-lead">Claude opened in a new tab. Once you have signed in, it shows an authorization code: copy it and paste it below.</p>
|
|
7084
|
+
<form id="verify">
|
|
7085
|
+
<label for="code">Authorization code<input id="code" type="password" autocomplete="off" spellcheck="false" required maxlength="4096"><span class="hint">The code stays hidden on screen. Never paste it into chat.</span></label>
|
|
7086
|
+
<button class="button" type="submit">Verify account</button>
|
|
7087
|
+
</form>
|
|
7088
|
+
<p class="aside">Closed the Claude tab? <a class="link" id="reopen" target="_blank" rel="noopener noreferrer">Open Claude sign-in again ${icon("open")}</a></p>
|
|
7089
|
+
</section>
|
|
7090
|
+
<section class="card" id="step-confirm" hidden>
|
|
7091
|
+
<div class="avatar">${icon("person")}</div>
|
|
7092
|
+
<h1 tabindex="-1">Is this the right account?</h1>
|
|
7093
|
+
<p class="lead">Claude verified the account below. Check it before adding it to the pool.</p>
|
|
7094
|
+
<form id="confirm">
|
|
7095
|
+
<div class="account">${icon("check")}<span id="account"></span></div>
|
|
7096
|
+
<label class="consent"><input id="consent" type="checkbox" required><span>I authorize the shared agents pool to use this subscription.</span></label>
|
|
7097
|
+
<button class="button" type="submit">Add to the pool</button>
|
|
7098
|
+
</form>
|
|
7099
|
+
<p class="aside">Wrong account? Do not confirm: start again and sign in with the right one.<a class="button secondary" href="/pool-enroll">Start again</a></p>
|
|
7100
|
+
</section>
|
|
7101
|
+
<section class="card" id="step-done" hidden>
|
|
7102
|
+
<div class="avatar ok">${icon("check")}</div>
|
|
7103
|
+
<h1 tabindex="-1">Subscription connected</h1>
|
|
7104
|
+
<p class="lead"><strong id="connected-account"></strong> is now part of the shared agents pool. You can close this page.</p>
|
|
7105
|
+
<div class="aside"><strong>Only testing?</strong> Disconnect removes this account's saved authorization and pool membership, then lets you sign in again. It stays available for 24 hours while this page is open; it does not sign you out of Claude or cancel your subscription.<button class="button secondary" id="disconnect" type="button">Disconnect and try again</button></div>
|
|
7106
|
+
</section>
|
|
7107
|
+
<section class="card" id="step-error" hidden>
|
|
7108
|
+
<div class="avatar err">${icon("alert")}</div>
|
|
7109
|
+
<h1 tabindex="-1">This sign-in can't continue</h1>
|
|
7110
|
+
<p class="lead" id="error-message"></p>
|
|
7111
|
+
<p class="aside">Nothing was added to the pool from this step.<a class="button secondary" href="/pool-enroll">Start again</a></p>
|
|
7112
|
+
</section>
|
|
7113
|
+
<p id="status" class="notice err" role="alert" hidden></p>
|
|
7114
|
+
</main>
|
|
7115
|
+
<footer>Each step expires after 10 minutes. Nothing is added to the pool until you confirm.</footer>
|
|
7116
|
+
<noscript><p class="notice err">JavaScript is required for this sign-in.</p></noscript>
|
|
7117
|
+
<script nonce="${nonce}">
|
|
7032
7118
|
let token = location.hash.slice(1);
|
|
7033
7119
|
history.replaceState(null, '', location.pathname);
|
|
7034
7120
|
addEventListener('hashchange', () => { if (location.hash) location.reload(); });
|
|
7035
7121
|
const get = id => document.getElementById(id);
|
|
7036
|
-
const
|
|
7037
|
-
|
|
7038
|
-
|
|
7122
|
+
const steps = ['email', 'signin', 'code', 'confirm', 'done'];
|
|
7123
|
+
let current = 'email';
|
|
7124
|
+
function show(step) {
|
|
7125
|
+
const failed = step === 'error';
|
|
7126
|
+
const index = steps.indexOf(failed ? current : step);
|
|
7127
|
+
if (!failed) current = step;
|
|
7128
|
+
for (const name of [...steps, 'error']) get('step-' + name).hidden = name !== step;
|
|
7129
|
+
document.querySelectorAll('#stepper li').forEach((item, position) => {
|
|
7130
|
+
item.className = position < index || step === 'done' ? 'done' : position === index ? (failed ? 'failed' : 'active') : '';
|
|
7131
|
+
item.querySelector('.dot').innerHTML = item.className === 'done' ? '${icon("check")}' : String(position + 1);
|
|
7132
|
+
if (position === index) item.setAttribute('aria-current', 'step'); else item.removeAttribute('aria-current');
|
|
7133
|
+
});
|
|
7134
|
+
get('progress').style.width = Math.max(5, ((index + (step === 'done' ? 1 : 0)) / steps.length) * 100) + '%';
|
|
7135
|
+
(step === 'email' ? get('email') : get('step-' + step).querySelector('h1')).focus();
|
|
7136
|
+
}
|
|
7137
|
+
async function send(step, body) {
|
|
7138
|
+
const response = await fetch('/pool-enroll/' + step, {method:'POST', credentials:'omit', cache:'no-store', redirect:'error', headers:{'content-type':'application/json'}, body:JSON.stringify(body)});
|
|
7039
7139
|
const data = await response.json();
|
|
7040
|
-
if (!response.ok) throw new Error(data.error || 'Sign-in failed.
|
|
7140
|
+
if (!response.ok) throw new Error(data.error || 'Sign-in failed. Start again.');
|
|
7041
7141
|
token = data.token || '';
|
|
7042
7142
|
return data;
|
|
7043
7143
|
}
|
|
7144
|
+
const post = (step, values) => send(step, {token, ...values});
|
|
7044
7145
|
function fail(error) {
|
|
7045
|
-
token = ''; get('code').value = '';
|
|
7046
|
-
|
|
7146
|
+
token = ''; get('code').value = '';
|
|
7147
|
+
get('error-message').textContent = error instanceof Error ? error.message : 'Sign-in failed. Start again.';
|
|
7148
|
+
show('error');
|
|
7047
7149
|
}
|
|
7048
|
-
|
|
7150
|
+
show(/^[A-Za-z0-9_-]{43}$/.test(token) ? 'signin' : 'email');
|
|
7151
|
+
get('request').addEventListener('submit', async event => {
|
|
7152
|
+
event.preventDefault(); event.submitter.disabled = true; get('email-error').hidden = true;
|
|
7153
|
+
try {
|
|
7154
|
+
const data = await send('request', {email: get('email').value});
|
|
7155
|
+
get('signin-email').textContent = data.email; get('signin-for').hidden = false;
|
|
7156
|
+
show('signin');
|
|
7157
|
+
} catch (error) {
|
|
7158
|
+
get('email-error').textContent = error instanceof Error ? error.message : 'This email cannot connect. Ask the operator.';
|
|
7159
|
+
get('email-error').hidden = false; get('email').focus();
|
|
7160
|
+
} finally { event.submitter.disabled = false; }
|
|
7161
|
+
});
|
|
7049
7162
|
get('start').addEventListener('click', async () => {
|
|
7163
|
+
const tab = window.open('', '_blank');
|
|
7050
7164
|
get('start').disabled = true;
|
|
7051
7165
|
try {
|
|
7052
7166
|
const data = await post('start', {});
|
|
7053
7167
|
const url = new URL(data.authUrl);
|
|
7054
|
-
if (url.origin !== 'https://claude.ai' || url.pathname !== '/oauth/authorize') throw new Error('Invalid sign-in destination. Ask
|
|
7055
|
-
get('
|
|
7056
|
-
|
|
7057
|
-
|
|
7168
|
+
if (url.origin !== 'https://claude.ai' || url.pathname !== '/oauth/authorize') throw new Error('Invalid sign-in destination. Ask for help.');
|
|
7169
|
+
get('reopen').href = url.href;
|
|
7170
|
+
if (tab) { tab.opener = null; tab.location.href = url.href; }
|
|
7171
|
+
else get('code-lead').textContent = 'Your browser blocked the new tab. Open Claude sign-in with the link below, sign in, then paste the code it shows.';
|
|
7172
|
+
show('code');
|
|
7173
|
+
} catch (error) { if (tab) tab.close(); fail(error); }
|
|
7058
7174
|
});
|
|
7059
7175
|
get('verify').addEventListener('submit', async event => {
|
|
7060
|
-
event.preventDefault();
|
|
7176
|
+
event.preventDefault(); event.submitter.disabled = true;
|
|
7061
7177
|
const code = get('code').value.trim(); get('code').value = '';
|
|
7062
|
-
try { const data = await post('verify', {code}); get('
|
|
7063
|
-
catch(error) { fail(error); }
|
|
7178
|
+
try { const data = await post('verify', {code}); get('account').textContent = data.email; show('confirm'); }
|
|
7179
|
+
catch (error) { fail(error); }
|
|
7064
7180
|
});
|
|
7065
7181
|
get('confirm').addEventListener('submit', async event => {
|
|
7066
7182
|
event.preventDefault(); if (!get('consent').checked) return; event.submitter.disabled = true;
|
|
7067
|
-
try { await post('confirm', {consent:true}); get('
|
|
7068
|
-
catch(error) { fail(error); }
|
|
7183
|
+
try { await post('confirm', {consent:true}); get('connected-account').textContent = get('account').textContent; show('done'); }
|
|
7184
|
+
catch (error) { fail(error); }
|
|
7069
7185
|
});
|
|
7070
7186
|
get('disconnect').addEventListener('click', async () => {
|
|
7071
|
-
get('disconnect').disabled = true;
|
|
7187
|
+
get('disconnect').disabled = true; get('status').hidden = true;
|
|
7072
7188
|
try {
|
|
7073
7189
|
await post('disconnect', {});
|
|
7074
|
-
get('
|
|
7075
|
-
get('consent').checked = false; get('code').value = '';
|
|
7076
|
-
get('authorize').removeAttribute('href'); get('authorize').hidden = true;
|
|
7190
|
+
get('account').textContent = ''; get('connected-account').textContent = ''; get('consent').checked = false;
|
|
7077
7191
|
document.querySelectorAll('button').forEach(button => { button.disabled = false; });
|
|
7078
|
-
get('
|
|
7079
|
-
|
|
7080
|
-
|
|
7192
|
+
get('signin-notice').textContent = 'Subscription disconnected: it is no longer in the pool. You can connect again or close this page.';
|
|
7193
|
+
get('signin-notice').hidden = false;
|
|
7194
|
+
show('signin');
|
|
7195
|
+
} catch (error) {
|
|
7081
7196
|
get('disconnect').disabled = false;
|
|
7082
|
-
status.textContent = 'Removal is not confirmed. ' + (error instanceof Error ? error.message : 'Keep this page open and ask
|
|
7197
|
+
get('status').textContent = 'Removal is not confirmed. ' + (error instanceof Error ? error.message : 'Keep this page open and ask for help with this seat.');
|
|
7198
|
+
get('status').hidden = false;
|
|
7083
7199
|
}
|
|
7084
7200
|
});
|
|
7085
7201
|
</script></body></html>`, {
|
|
@@ -7093,7 +7209,61 @@ get('disconnect').addEventListener('click', async () => {
|
|
|
7093
7209
|
}
|
|
7094
7210
|
});
|
|
7095
7211
|
}
|
|
7096
|
-
var
|
|
7212
|
+
var icons, icon = (name) => `<svg viewBox="0 0 24 24" aria-hidden="true">${icons[name]}</svg>`, styles = `
|
|
7213
|
+
:root{--page:#faf9f6;--card:#fff;--text:#191b21;--muted:#5a606b;--faint:#9aa0ab;--line:#e6e8ec;--border:rgba(25,27,33,.14);--brand:#fb5d22;--accent:#dc5518;--tint:#fff3ec;--ink:#182025;--ink-hover:#2a333b;--ok:#17a06b;--ok-bg:#e3f6ed;--ok-text:#0c6444;--err:#d92d20;--err-bg:#fef3f2;--err-text:#912018;color-scheme:light;font:15px/1.55 "Public Sans",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--text);background:var(--page)}
|
|
7214
|
+
*{box-sizing:border-box}body{margin:0;min-height:100vh}
|
|
7215
|
+
header{position:relative;background:#fff;border-bottom:1px solid var(--line);padding:16px clamp(16px,4vw,32px);font-weight:600}
|
|
7216
|
+
header small{display:block;font-weight:400;color:var(--muted);font-size:13px}
|
|
7217
|
+
.bar{position:absolute;left:0;right:0;bottom:-1px;height:4px;background:#f2f1ec}.bar span{display:block;height:100%;width:5%;background:var(--ink);transition:width .3s ease-in-out}
|
|
7218
|
+
main{max-width:560px;margin:0 auto;padding:32px 16px 48px}
|
|
7219
|
+
ol{list-style:none;display:flex;margin:0 0 28px;padding:0}
|
|
7220
|
+
ol li{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;position:relative;font-size:13px;color:var(--faint)}
|
|
7221
|
+
ol li+li::before{content:"";position:absolute;top:15px;right:calc(50% + 20px);width:calc(100% - 40px);height:2px;background:var(--line)}
|
|
7222
|
+
ol li.done+li::before{background:var(--ok-text)}
|
|
7223
|
+
ol .dot{width:32px;height:32px;border-radius:50%;display:grid;place-items:center;font-weight:600;background:var(--line);color:var(--faint);border:2px solid var(--line)}
|
|
7224
|
+
ol .dot svg{width:16px;height:16px;stroke:#fff;stroke-width:3}
|
|
7225
|
+
ol li.active{color:var(--accent);font-weight:600}ol li.active .dot{background:var(--brand);border-color:var(--brand);color:#fff}
|
|
7226
|
+
ol li.done{color:var(--muted)}ol li.done .dot{background:var(--ok);border-color:var(--ok)}
|
|
7227
|
+
ol li.failed .dot{background:var(--err);border-color:var(--err);color:#fff}
|
|
7228
|
+
.card{background:var(--card);border-radius:16px;box-shadow:0 1px 2px rgba(16,18,24,.05);border:1px solid rgba(25,27,33,.08);padding:clamp(24px,5vw,40px);text-align:center}
|
|
7229
|
+
.avatar{width:64px;height:64px;margin:0 auto 16px;border-radius:50%;display:grid;place-items:center;background:var(--tint)}
|
|
7230
|
+
.avatar svg,.button svg{width:28px;height:28px;fill:none;stroke:var(--accent);stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
|
7231
|
+
.avatar.ok{background:var(--ok-bg)}.avatar.ok svg{stroke:var(--ok-text);stroke-width:2.4}.avatar.err{background:var(--err-bg)}.avatar.err svg{stroke:var(--err)}
|
|
7232
|
+
h1{font-size:22px;line-height:1.3;margin:0 0 8px;outline:none}
|
|
7233
|
+
.lead{color:var(--muted);margin:0 auto 24px;max-width:42ch}
|
|
7234
|
+
.facts{list-style:none;padding:0;margin:0 0 28px;text-align:left;display:grid;gap:10px}
|
|
7235
|
+
.facts li{display:flex;gap:10px;align-items:flex-start}.facts li::before{content:"";flex:none;width:6px;height:6px;margin-top:9px;border-radius:50%;background:var(--brand)}
|
|
7236
|
+
.button{font:inherit;font-weight:600;display:inline-flex;align-items:center;justify-content:center;gap:8px;width:100%;min-height:44px;padding:10px 16px;border-radius:9px;border:0;background:var(--ink);color:#fff;cursor:pointer;text-decoration:none}
|
|
7237
|
+
.button:hover{background:var(--ink-hover);box-shadow:0 6px 18px -6px rgb(24 32 37/.5)}.button svg{width:18px;height:18px;stroke:currentColor}
|
|
7238
|
+
.button.secondary{background:#fff;color:var(--text);border:1px solid var(--border)}.button.secondary:hover{box-shadow:0 1px 3px rgba(47,43,67,.12)}
|
|
7239
|
+
.button:disabled{opacity:.5;cursor:progress;box-shadow:none}
|
|
7240
|
+
.button:focus-visible,input:focus-visible,a:focus-visible{outline:3px solid var(--brand);outline-offset:3px}
|
|
7241
|
+
.link{display:inline-flex;align-items:center;gap:6px;color:var(--accent);font-weight:600;text-decoration:none}.link svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2}
|
|
7242
|
+
form{text-align:left;display:grid;gap:16px}label{font-weight:600}
|
|
7243
|
+
input[type=password],input[type=email]{display:block;width:100%;font:inherit;padding:11px 12px;margin-top:6px;border:1px solid var(--border);border-radius:9px;background:#fff}
|
|
7244
|
+
.hint{font-size:13px;color:var(--muted);margin:6px 0 0;font-weight:400}
|
|
7245
|
+
.account{display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:12px;background:var(--page);border:1px solid rgba(25,27,33,.08);font-weight:600;overflow-wrap:anywhere}
|
|
7246
|
+
.account svg{flex:none;width:22px;height:22px;fill:none;stroke:var(--ok-text);stroke-width:2.4}
|
|
7247
|
+
.consent{display:flex;gap:10px;align-items:flex-start;font-weight:400}.consent input{width:18px;height:18px;margin:2px 0 0;accent-color:var(--ink)}
|
|
7248
|
+
.notice{border-radius:12px;padding:12px 16px;margin:0 0 20px;text-align:left;background:var(--ok-bg);color:var(--ok-text)}
|
|
7249
|
+
.notice.err{background:var(--err-bg);color:var(--err-text)}
|
|
7250
|
+
.aside{margin-top:24px;padding-top:20px;border-top:1px solid var(--line);text-align:left;color:var(--muted);font-size:14px}
|
|
7251
|
+
.aside .button{margin-top:12px}
|
|
7252
|
+
footer{max-width:560px;margin:0 auto;padding:0 16px 32px;color:var(--muted);font-size:13px;text-align:center}
|
|
7253
|
+
@media (max-width:480px){ol li:not(.active) .label{display:none}}
|
|
7254
|
+
[hidden]{display:none!important}
|
|
7255
|
+
`;
|
|
7256
|
+
var init_enrollment_page = __esm(() => {
|
|
7257
|
+
icons = {
|
|
7258
|
+
mail: '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/>',
|
|
7259
|
+
key: '<path d="M15 7.5a3.5 3.5 0 1 1-3.4 4.3L4 19.5V22h3v-2h2v-2h2l1.2-1.2A3.5 3.5 0 0 1 15 7.5Z"/><circle cx="16" cy="11" r="1"/>',
|
|
7260
|
+
code: '<rect x="4" y="6" width="16" height="12" rx="2"/><path d="M8 10h.01M12 10h.01M16 10h.01M8 14h8"/>',
|
|
7261
|
+
person: '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
|
|
7262
|
+
check: '<path d="m5 12.5 4.5 4.5L19 7.5"/>',
|
|
7263
|
+
alert: '<path d="M12 8v5M12 16.5h.01"/><circle cx="12" cy="12" r="9"/>',
|
|
7264
|
+
open: '<path d="M14 5h5v5M19 5l-8 8M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/>'
|
|
7265
|
+
};
|
|
7266
|
+
});
|
|
7097
7267
|
|
|
7098
7268
|
// ../../packages/api/src/handlers/enrollment.ts
|
|
7099
7269
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
@@ -7135,13 +7305,59 @@ class EnrollmentHandlers {
|
|
|
7135
7305
|
return await action();
|
|
7136
7306
|
} catch (error) {
|
|
7137
7307
|
return error instanceof EnrollmentError ? json({ error: error.message }, error.status) : json({
|
|
7138
|
-
error: "Enrollment could not be saved. Ask the operator to check the seat
|
|
7308
|
+
error: "Enrollment could not be saved. Ask the operator to check the seat."
|
|
7139
7309
|
}, 500);
|
|
7140
7310
|
}
|
|
7141
7311
|
}
|
|
7142
|
-
|
|
7312
|
+
requireClaudeCode() {
|
|
7143
7313
|
if (!this.context.providerRegistry.getOAuthProvider("claude-code"))
|
|
7144
7314
|
throw new EnrollmentError(400, "Provider 'claude-code' is disabled. Set providers.claude-code.enabled to true in your config.");
|
|
7315
|
+
}
|
|
7316
|
+
reserve(name) {
|
|
7317
|
+
if (this.context.dbOps.getAccountByName(name))
|
|
7318
|
+
throw new EnrollmentError(409, "This seat is already in the pool.");
|
|
7319
|
+
for (const entry of this.entries.values())
|
|
7320
|
+
if (entry.details.name === name && entry.state.kind === "busy")
|
|
7321
|
+
throw new EnrollmentError(409, "Sign-in is already in progress");
|
|
7322
|
+
for (const [key, entry] of this.entries)
|
|
7323
|
+
if (entry.details.name === name)
|
|
7324
|
+
this.entries.delete(key);
|
|
7325
|
+
if (this.entries.size >= 100)
|
|
7326
|
+
throw new EnrollmentError(429, "Too many pending invitations");
|
|
7327
|
+
}
|
|
7328
|
+
request(body) {
|
|
7329
|
+
this.requireClaudeCode();
|
|
7330
|
+
if (Object.keys(body).some((key) => key !== "email"))
|
|
7331
|
+
throw new EnrollmentError(400, "Unknown request field");
|
|
7332
|
+
const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
|
|
7333
|
+
if (!this.context.config.getEnrollmentEmails().includes(email))
|
|
7334
|
+
throw new EnrollmentError(403, "This email is not on the list. Ask the operator to add it.");
|
|
7335
|
+
const name = email.slice(0, email.indexOf("@")).replace(/[^a-z0-9._+-]/g, "-").slice(0, 64);
|
|
7336
|
+
this.reserve(name);
|
|
7337
|
+
const issued = this.mint({ name, email, contactEmail: email }, { kind: "invited" }, tenMinutes);
|
|
7338
|
+
return json({ ...issued, email });
|
|
7339
|
+
}
|
|
7340
|
+
emails = (req) => this.guarded(async () => {
|
|
7341
|
+
const body = await readBody(req);
|
|
7342
|
+
const lists = Object.entries(body);
|
|
7343
|
+
if (lists.some(([key, value]) => key !== "add" && key !== "remove" || !Array.isArray(value) || value.some((email) => typeof email !== "string")))
|
|
7344
|
+
throw new EnrollmentError(400, "Expected { add?: string[], remove?: string[] }");
|
|
7345
|
+
const { config } = this.context;
|
|
7346
|
+
const add = body.add ?? [];
|
|
7347
|
+
const remove = new Set((body.remove ?? []).map((email) => email.trim().toLowerCase()));
|
|
7348
|
+
if (add.length === 0 && remove.size === 0)
|
|
7349
|
+
return json({ emails: config.getEnrollmentEmails() });
|
|
7350
|
+
const next = [...config.getEnrollmentEmails(), ...add].filter((email) => !remove.has(email.trim().toLowerCase()));
|
|
7351
|
+
try {
|
|
7352
|
+
return json({ emails: config.setEnrollmentEmails(next) });
|
|
7353
|
+
} catch (error) {
|
|
7354
|
+
if (error instanceof ValidationError)
|
|
7355
|
+
throw new EnrollmentError(400, error.message);
|
|
7356
|
+
throw error;
|
|
7357
|
+
}
|
|
7358
|
+
});
|
|
7359
|
+
issue = (req) => this.guarded(async () => {
|
|
7360
|
+
this.requireClaudeCode();
|
|
7145
7361
|
const input = await readBody(req);
|
|
7146
7362
|
const allowed = new Set(["name", "email", "contactEmail", "slackUserId"]);
|
|
7147
7363
|
if (Object.keys(input).some((key) => !allowed.has(key)))
|
|
@@ -7164,16 +7380,7 @@ class EnrollmentHandlers {
|
|
|
7164
7380
|
}
|
|
7165
7381
|
if (details.name.length > 80 || details.slackUserId && !/^U[A-Z0-9]{8,20}$/.test(details.slackUserId) || [details.email, details.contactEmail].some((email) => email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)))
|
|
7166
7382
|
throw new EnrollmentError(400, "Invalid invitation details");
|
|
7167
|
-
|
|
7168
|
-
throw new EnrollmentError(409, "Seat already exists");
|
|
7169
|
-
for (const entry of this.entries.values())
|
|
7170
|
-
if (entry.details.name === details.name && entry.state.kind === "busy")
|
|
7171
|
-
throw new EnrollmentError(409, "Sign-in is already in progress");
|
|
7172
|
-
for (const [key, entry] of this.entries)
|
|
7173
|
-
if (entry.details.name === details.name)
|
|
7174
|
-
this.entries.delete(key);
|
|
7175
|
-
if (this.entries.size >= 100)
|
|
7176
|
-
throw new EnrollmentError(429, "Too many pending invitations");
|
|
7383
|
+
this.reserve(details.name);
|
|
7177
7384
|
const issued = this.mint(details, { kind: "invited" }, oneDay);
|
|
7178
7385
|
return json({
|
|
7179
7386
|
url: `${this.origin.origin}/pool-enroll#${issued.token}`,
|
|
@@ -7195,12 +7402,14 @@ class EnrollmentHandlers {
|
|
|
7195
7402
|
if (req.headers.get("origin") !== this.origin.origin)
|
|
7196
7403
|
throw new EnrollmentError(403, "Untrusted enrollment origin");
|
|
7197
7404
|
const body = await readBody(req);
|
|
7405
|
+
if (url.pathname === "/pool-enroll/request")
|
|
7406
|
+
return this.request(body);
|
|
7198
7407
|
const token = typeof body.token === "string" && /^[A-Za-z0-9_-]{43}$/.test(body.token) ? body.token : "";
|
|
7199
7408
|
const key = hash(token);
|
|
7200
7409
|
const entry = this.entries.get(key);
|
|
7201
7410
|
const expected = url.pathname === "/pool-enroll/start" ? "invited" : url.pathname === "/pool-enroll/verify" ? "started" : url.pathname === "/pool-enroll/confirm" ? "verified" : "connected";
|
|
7202
7411
|
if (!entry || entry.expiresAt <= this.now() || entry.state.kind !== expected)
|
|
7203
|
-
throw new EnrollmentError(410, "This
|
|
7412
|
+
throw new EnrollmentError(410, "This sign-in has expired or was already used. Start again.");
|
|
7204
7413
|
const state = entry.state;
|
|
7205
7414
|
entry.state = { kind: "busy" };
|
|
7206
7415
|
try {
|
|
@@ -7219,12 +7428,12 @@ class EnrollmentHandlers {
|
|
|
7219
7428
|
try {
|
|
7220
7429
|
seat = await this.oauth.verifyClaudeSeat(state.flow, body.code.trim());
|
|
7221
7430
|
} catch {
|
|
7222
|
-
throw new EnrollmentError(400, "Claude sign-in could not be verified.
|
|
7431
|
+
throw new EnrollmentError(400, "Claude sign-in could not be verified. Start again.");
|
|
7223
7432
|
}
|
|
7224
7433
|
this.assertCurrent(key, entry);
|
|
7225
7434
|
const { email } = seat.identity;
|
|
7226
7435
|
if (!email || entry.details.email && email.toLowerCase() !== entry.details.email.toLowerCase())
|
|
7227
|
-
throw new EnrollmentError(400, "This Claude account does not match
|
|
7436
|
+
throw new EnrollmentError(400, "This Claude account does not match your email. Start again and sign in with the matching account.");
|
|
7228
7437
|
this.assertNewSeat(entry.details.name, seat);
|
|
7229
7438
|
return json({
|
|
7230
7439
|
...this.mint(entry.details, { kind: "verified", seat }, tenMinutes),
|
|
@@ -7267,7 +7476,7 @@ class EnrollmentHandlers {
|
|
|
7267
7476
|
}
|
|
7268
7477
|
assertCurrent(key, entry) {
|
|
7269
7478
|
if (this.entries.get(key) !== entry || entry.expiresAt <= this.now())
|
|
7270
|
-
throw new EnrollmentError(410, "Sign-in expired.
|
|
7479
|
+
throw new EnrollmentError(410, "Sign-in expired. Start again.");
|
|
7271
7480
|
}
|
|
7272
7481
|
assertNewSeat(name, seat) {
|
|
7273
7482
|
if (this.context.dbOps.getAccountByName(name) || this.context.dbOps.getAccountsByProvider("claude-code").some((account2) => account2.account_uuid === seat.identity.accountUuid))
|
|
@@ -7347,11 +7556,13 @@ async function readBody(req) {
|
|
|
7347
7556
|
}
|
|
7348
7557
|
var paths, tenMinutes = 600000, oneDay = 86400000, headers2, json = (value, status = 200) => Response.json(value, { status, headers: headers2 }), hash = (token) => createHash("sha256").update(token).digest("hex"), EnrollmentError;
|
|
7349
7558
|
var init_enrollment = __esm(() => {
|
|
7559
|
+
init_src2();
|
|
7350
7560
|
init_src8();
|
|
7351
7561
|
init_src();
|
|
7352
7562
|
init_enrollment_page();
|
|
7353
7563
|
paths = new Set([
|
|
7354
7564
|
"/pool-enroll",
|
|
7565
|
+
"/pool-enroll/request",
|
|
7355
7566
|
"/pool-enroll/start",
|
|
7356
7567
|
"/pool-enroll/verify",
|
|
7357
7568
|
"/pool-enroll/confirm",
|
|
@@ -7768,8 +7979,10 @@ class APIRouter {
|
|
|
7768
7979
|
this.registerHandlers();
|
|
7769
7980
|
}
|
|
7770
7981
|
registerHandlers() {
|
|
7771
|
-
if (this.enrollment)
|
|
7982
|
+
if (this.enrollment) {
|
|
7772
7983
|
this.staticHandlers.set("POST:/api/enrollment-invitations", this.enrollment.issue);
|
|
7984
|
+
this.staticHandlers.set("POST:/api/enrollment-emails", this.enrollment.emails);
|
|
7985
|
+
}
|
|
7773
7986
|
const { config, dbOps, getProviders, getRuntimeHealth, providerRegistry } = this.context;
|
|
7774
7987
|
const healthHandler = createHealthHandler(dbOps, config, getProviders, getRuntimeHealth);
|
|
7775
7988
|
const statsHandler = createStatsHandler(dbOps);
|
|
@@ -1327,12 +1327,30 @@ function parseNumber(value) {
|
|
|
1327
1327
|
const parsed = Number(value);
|
|
1328
1328
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1329
1329
|
}
|
|
1330
|
+
function normalizeEnrollmentEmail(value) {
|
|
1331
|
+
if (typeof value !== "string")
|
|
1332
|
+
return;
|
|
1333
|
+
const email = value.trim().toLowerCase();
|
|
1334
|
+
return email.length <= 254 && patterns.email.test(email) ? email : undefined;
|
|
1335
|
+
}
|
|
1336
|
+
function sanitizeEnrollmentEmails(value) {
|
|
1337
|
+
if (!Array.isArray(value))
|
|
1338
|
+
return;
|
|
1339
|
+
const emails = value.map(normalizeEnrollmentEmail).filter((email) => email !== undefined);
|
|
1340
|
+
return [...new Set(emails)].slice(0, MAX_ENROLLMENT_EMAILS);
|
|
1341
|
+
}
|
|
1330
1342
|
function sanitizeConfigData(value) {
|
|
1331
1343
|
if (!isRecord(value)) {
|
|
1332
1344
|
return {};
|
|
1333
1345
|
}
|
|
1334
1346
|
const sanitized = {};
|
|
1335
1347
|
for (const [key, entry] of Object.entries(value)) {
|
|
1348
|
+
if (key === "enrollment_emails") {
|
|
1349
|
+
const emails = sanitizeEnrollmentEmails(entry);
|
|
1350
|
+
if (emails)
|
|
1351
|
+
sanitized.enrollment_emails = emails;
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1336
1354
|
if (key === "team_routing") {
|
|
1337
1355
|
const teamRouting = sanitizeTeamRouting(entry);
|
|
1338
1356
|
if (teamRouting)
|
|
@@ -1482,7 +1500,7 @@ function sanitizeTeamRouting(value) {
|
|
|
1482
1500
|
}
|
|
1483
1501
|
return result;
|
|
1484
1502
|
}
|
|
1485
|
-
var log2, PROVIDER_STRATEGIES, BUILTIN_PROVIDER_CONFIGS, BUILTIN_PROVIDER_CONFIG_BY_NAME, PROVIDER_NAME_PATTERN, TEAM_ROUTING_DEFAULTS, Config;
|
|
1503
|
+
var log2, PROVIDER_STRATEGIES, BUILTIN_PROVIDER_CONFIGS, BUILTIN_PROVIDER_CONFIG_BY_NAME, PROVIDER_NAME_PATTERN, TEAM_ROUTING_DEFAULTS, MAX_ENROLLMENT_EMAILS = 200, Config;
|
|
1486
1504
|
var init_src4 = __esm(() => {
|
|
1487
1505
|
init_src2();
|
|
1488
1506
|
init_src3();
|
|
@@ -1726,11 +1744,28 @@ var init_src4 = __esm(() => {
|
|
|
1726
1744
|
this.persistAccountAllowlist(current.filter((uuid) => uuid !== accountUuid));
|
|
1727
1745
|
}
|
|
1728
1746
|
persistAccountAllowlist(accountUuids) {
|
|
1729
|
-
|
|
1747
|
+
this.persistStrictly("team_routing", {
|
|
1730
1748
|
...this.data.team_routing,
|
|
1731
1749
|
allowlisted_account_uuids: accountUuids
|
|
1732
|
-
};
|
|
1733
|
-
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
getEnrollmentEmails() {
|
|
1753
|
+
return [...this.data.enrollment_emails ?? []];
|
|
1754
|
+
}
|
|
1755
|
+
setEnrollmentEmails(emails) {
|
|
1756
|
+
const normalized = emails.map(normalizeEnrollmentEmail);
|
|
1757
|
+
if (normalized.some((email) => email === undefined)) {
|
|
1758
|
+
throw new ValidationError("Invalid enrollment email", "enrollment_emails");
|
|
1759
|
+
}
|
|
1760
|
+
const unique = [...new Set(normalized)];
|
|
1761
|
+
if (unique.length > MAX_ENROLLMENT_EMAILS) {
|
|
1762
|
+
throw new ValidationError(`At most ${MAX_ENROLLMENT_EMAILS} enrollment emails`, "enrollment_emails");
|
|
1763
|
+
}
|
|
1764
|
+
this.persistStrictly("enrollment_emails", unique);
|
|
1765
|
+
return [...unique];
|
|
1766
|
+
}
|
|
1767
|
+
persistStrictly(key, value) {
|
|
1768
|
+
const next = { ...this.data, [key]: value };
|
|
1734
1769
|
const temporary = `${this.configPath}.${crypto.randomUUID()}.tmp`;
|
|
1735
1770
|
try {
|
|
1736
1771
|
writeFileSync(temporary, JSON.stringify(next, null, 2), {
|
|
@@ -1741,13 +1776,9 @@ var init_src4 = __esm(() => {
|
|
|
1741
1776
|
} finally {
|
|
1742
1777
|
rmSync(temporary, { force: true });
|
|
1743
1778
|
}
|
|
1744
|
-
const oldValue = this.data
|
|
1779
|
+
const oldValue = this.data[key];
|
|
1745
1780
|
this.data = next;
|
|
1746
|
-
this.emit("change", {
|
|
1747
|
-
key: "team_routing",
|
|
1748
|
-
oldValue,
|
|
1749
|
-
newValue: teamRouting
|
|
1750
|
-
});
|
|
1781
|
+
this.emit("change", { key, oldValue, newValue: value });
|
|
1751
1782
|
}
|
|
1752
1783
|
getRuntime() {
|
|
1753
1784
|
const defaults = {
|
package/dist/server.js
CHANGED
|
@@ -3831,12 +3831,30 @@ function parseNumber(value) {
|
|
|
3831
3831
|
const parsed = Number(value);
|
|
3832
3832
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
3833
3833
|
}
|
|
3834
|
+
function normalizeEnrollmentEmail(value) {
|
|
3835
|
+
if (typeof value !== "string")
|
|
3836
|
+
return;
|
|
3837
|
+
const email = value.trim().toLowerCase();
|
|
3838
|
+
return email.length <= 254 && patterns.email.test(email) ? email : undefined;
|
|
3839
|
+
}
|
|
3840
|
+
function sanitizeEnrollmentEmails(value) {
|
|
3841
|
+
if (!Array.isArray(value))
|
|
3842
|
+
return;
|
|
3843
|
+
const emails = value.map(normalizeEnrollmentEmail).filter((email) => email !== undefined);
|
|
3844
|
+
return [...new Set(emails)].slice(0, MAX_ENROLLMENT_EMAILS);
|
|
3845
|
+
}
|
|
3834
3846
|
function sanitizeConfigData(value) {
|
|
3835
3847
|
if (!isRecord(value)) {
|
|
3836
3848
|
return {};
|
|
3837
3849
|
}
|
|
3838
3850
|
const sanitized = {};
|
|
3839
3851
|
for (const [key, entry] of Object.entries(value)) {
|
|
3852
|
+
if (key === "enrollment_emails") {
|
|
3853
|
+
const emails = sanitizeEnrollmentEmails(entry);
|
|
3854
|
+
if (emails)
|
|
3855
|
+
sanitized.enrollment_emails = emails;
|
|
3856
|
+
continue;
|
|
3857
|
+
}
|
|
3840
3858
|
if (key === "team_routing") {
|
|
3841
3859
|
const teamRouting = sanitizeTeamRouting(entry);
|
|
3842
3860
|
if (teamRouting)
|
|
@@ -3986,7 +4004,7 @@ function sanitizeTeamRouting(value) {
|
|
|
3986
4004
|
}
|
|
3987
4005
|
return result;
|
|
3988
4006
|
}
|
|
3989
|
-
var log6, PROVIDER_STRATEGIES, BUILTIN_PROVIDER_CONFIGS, BUILTIN_PROVIDER_CONFIG_BY_NAME, PROVIDER_NAME_PATTERN, TEAM_ROUTING_DEFAULTS, Config;
|
|
4007
|
+
var log6, PROVIDER_STRATEGIES, BUILTIN_PROVIDER_CONFIGS, BUILTIN_PROVIDER_CONFIG_BY_NAME, PROVIDER_NAME_PATTERN, TEAM_ROUTING_DEFAULTS, MAX_ENROLLMENT_EMAILS = 200, Config;
|
|
3990
4008
|
var init_src7 = __esm(() => {
|
|
3991
4009
|
init_src2();
|
|
3992
4010
|
init_src4();
|
|
@@ -4230,11 +4248,28 @@ var init_src7 = __esm(() => {
|
|
|
4230
4248
|
this.persistAccountAllowlist(current.filter((uuid) => uuid !== accountUuid));
|
|
4231
4249
|
}
|
|
4232
4250
|
persistAccountAllowlist(accountUuids) {
|
|
4233
|
-
|
|
4251
|
+
this.persistStrictly("team_routing", {
|
|
4234
4252
|
...this.data.team_routing,
|
|
4235
4253
|
allowlisted_account_uuids: accountUuids
|
|
4236
|
-
};
|
|
4237
|
-
|
|
4254
|
+
});
|
|
4255
|
+
}
|
|
4256
|
+
getEnrollmentEmails() {
|
|
4257
|
+
return [...this.data.enrollment_emails ?? []];
|
|
4258
|
+
}
|
|
4259
|
+
setEnrollmentEmails(emails) {
|
|
4260
|
+
const normalized = emails.map(normalizeEnrollmentEmail);
|
|
4261
|
+
if (normalized.some((email) => email === undefined)) {
|
|
4262
|
+
throw new ValidationError("Invalid enrollment email", "enrollment_emails");
|
|
4263
|
+
}
|
|
4264
|
+
const unique = [...new Set(normalized)];
|
|
4265
|
+
if (unique.length > MAX_ENROLLMENT_EMAILS) {
|
|
4266
|
+
throw new ValidationError(`At most ${MAX_ENROLLMENT_EMAILS} enrollment emails`, "enrollment_emails");
|
|
4267
|
+
}
|
|
4268
|
+
this.persistStrictly("enrollment_emails", unique);
|
|
4269
|
+
return [...unique];
|
|
4270
|
+
}
|
|
4271
|
+
persistStrictly(key, value) {
|
|
4272
|
+
const next = { ...this.data, [key]: value };
|
|
4238
4273
|
const temporary = `${this.configPath}.${crypto.randomUUID()}.tmp`;
|
|
4239
4274
|
try {
|
|
4240
4275
|
writeFileSync(temporary, JSON.stringify(next, null, 2), {
|
|
@@ -4245,13 +4280,9 @@ var init_src7 = __esm(() => {
|
|
|
4245
4280
|
} finally {
|
|
4246
4281
|
rmSync(temporary, { force: true });
|
|
4247
4282
|
}
|
|
4248
|
-
const oldValue = this.data
|
|
4283
|
+
const oldValue = this.data[key];
|
|
4249
4284
|
this.data = next;
|
|
4250
|
-
this.emit("change", {
|
|
4251
|
-
key: "team_routing",
|
|
4252
|
-
oldValue,
|
|
4253
|
-
newValue: teamRouting
|
|
4254
|
-
});
|
|
4285
|
+
this.emit("change", { key, oldValue, newValue: value });
|
|
4255
4286
|
}
|
|
4256
4287
|
getRuntime() {
|
|
4257
4288
|
const defaults = {
|
|
@@ -7013,72 +7044,157 @@ function enrollmentPage() {
|
|
|
7013
7044
|
return new Response(`<!doctype html>
|
|
7014
7045
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7015
7046
|
<title>Connect a Claude subscription</title>
|
|
7016
|
-
<style nonce="${nonce}">
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
</
|
|
7021
|
-
<
|
|
7022
|
-
<
|
|
7023
|
-
<
|
|
7024
|
-
<
|
|
7025
|
-
|
|
7026
|
-
<section
|
|
7027
|
-
<
|
|
7028
|
-
<
|
|
7029
|
-
<
|
|
7030
|
-
|
|
7047
|
+
<style nonce="${nonce}">${styles}</style></head><body>
|
|
7048
|
+
<header>Shared agents pool<small>Connect a Claude subscription</small><div class="bar"><span id="progress"></span></div></header>
|
|
7049
|
+
<main>
|
|
7050
|
+
<ol id="stepper" aria-label="Progress">
|
|
7051
|
+
<li data-step="email"><span class="dot">1</span><span class="label">Email</span></li>
|
|
7052
|
+
<li data-step="signin"><span class="dot">2</span><span class="label">Sign in</span></li>
|
|
7053
|
+
<li data-step="code"><span class="dot">3</span><span class="label">Paste code</span></li>
|
|
7054
|
+
<li data-step="confirm"><span class="dot">4</span><span class="label">Confirm</span></li>
|
|
7055
|
+
<li data-step="done"><span class="dot">5</span><span class="label">Done</span></li>
|
|
7056
|
+
</ol>
|
|
7057
|
+
<section class="card" id="step-email" hidden>
|
|
7058
|
+
<div class="avatar">${icon("mail")}</div>
|
|
7059
|
+
<h1 tabindex="-1">Connect your Claude subscription</h1>
|
|
7060
|
+
<p class="lead">This adds your subscription to the shared agents pool. Start with your work email: if it is on this pool's list, the next step lets you sign in with Claude.</p>
|
|
7061
|
+
<form id="request">
|
|
7062
|
+
<p class="notice err" id="email-error" role="alert" hidden></p>
|
|
7063
|
+
<label for="email">Work email<input id="email" type="email" autocomplete="email" spellcheck="false" required maxlength="254"></label>
|
|
7064
|
+
<button class="button" type="submit">Continue</button>
|
|
7065
|
+
</form>
|
|
7066
|
+
</section>
|
|
7067
|
+
<section class="card" id="step-signin" hidden>
|
|
7068
|
+
<p class="notice" id="signin-notice" role="status" hidden></p>
|
|
7069
|
+
<div class="avatar">${icon("key")}</div>
|
|
7070
|
+
<h1 tabindex="-1">Sign in with Claude</h1>
|
|
7071
|
+
<p class="lead" id="signin-for" hidden>Use the Claude account of <strong id="signin-email"></strong>. Only connect an account you have permission to share.</p>
|
|
7072
|
+
<ul class="facts">
|
|
7073
|
+
<li>Claude opens in a new tab. Sign in and pick the account to share.</li>
|
|
7074
|
+
<li>Your password stays with Claude. The pool only keeps the authorization it needs to run agents.</li>
|
|
7075
|
+
<li>Claude then shows a code: you paste it here in the next step.</li>
|
|
7076
|
+
</ul>
|
|
7077
|
+
<button class="button" id="start" type="button">Continue to Claude ${icon("open")}</button>
|
|
7078
|
+
</section>
|
|
7079
|
+
<section class="card" id="step-code" hidden>
|
|
7080
|
+
<div class="avatar">${icon("code")}</div>
|
|
7081
|
+
<h1 tabindex="-1">Paste the code from Claude</h1>
|
|
7082
|
+
<p class="lead" id="code-lead">Claude opened in a new tab. Once you have signed in, it shows an authorization code: copy it and paste it below.</p>
|
|
7083
|
+
<form id="verify">
|
|
7084
|
+
<label for="code">Authorization code<input id="code" type="password" autocomplete="off" spellcheck="false" required maxlength="4096"><span class="hint">The code stays hidden on screen. Never paste it into chat.</span></label>
|
|
7085
|
+
<button class="button" type="submit">Verify account</button>
|
|
7086
|
+
</form>
|
|
7087
|
+
<p class="aside">Closed the Claude tab? <a class="link" id="reopen" target="_blank" rel="noopener noreferrer">Open Claude sign-in again ${icon("open")}</a></p>
|
|
7088
|
+
</section>
|
|
7089
|
+
<section class="card" id="step-confirm" hidden>
|
|
7090
|
+
<div class="avatar">${icon("person")}</div>
|
|
7091
|
+
<h1 tabindex="-1">Is this the right account?</h1>
|
|
7092
|
+
<p class="lead">Claude verified the account below. Check it before adding it to the pool.</p>
|
|
7093
|
+
<form id="confirm">
|
|
7094
|
+
<div class="account">${icon("check")}<span id="account"></span></div>
|
|
7095
|
+
<label class="consent"><input id="consent" type="checkbox" required><span>I authorize the shared agents pool to use this subscription.</span></label>
|
|
7096
|
+
<button class="button" type="submit">Add to the pool</button>
|
|
7097
|
+
</form>
|
|
7098
|
+
<p class="aside">Wrong account? Do not confirm: start again and sign in with the right one.<a class="button secondary" href="/pool-enroll">Start again</a></p>
|
|
7099
|
+
</section>
|
|
7100
|
+
<section class="card" id="step-done" hidden>
|
|
7101
|
+
<div class="avatar ok">${icon("check")}</div>
|
|
7102
|
+
<h1 tabindex="-1">Subscription connected</h1>
|
|
7103
|
+
<p class="lead"><strong id="connected-account"></strong> is now part of the shared agents pool. You can close this page.</p>
|
|
7104
|
+
<div class="aside"><strong>Only testing?</strong> Disconnect removes this account's saved authorization and pool membership, then lets you sign in again. It stays available for 24 hours while this page is open; it does not sign you out of Claude or cancel your subscription.<button class="button secondary" id="disconnect" type="button">Disconnect and try again</button></div>
|
|
7105
|
+
</section>
|
|
7106
|
+
<section class="card" id="step-error" hidden>
|
|
7107
|
+
<div class="avatar err">${icon("alert")}</div>
|
|
7108
|
+
<h1 tabindex="-1">This sign-in can't continue</h1>
|
|
7109
|
+
<p class="lead" id="error-message"></p>
|
|
7110
|
+
<p class="aside">Nothing was added to the pool from this step.<a class="button secondary" href="/pool-enroll">Start again</a></p>
|
|
7111
|
+
</section>
|
|
7112
|
+
<p id="status" class="notice err" role="alert" hidden></p>
|
|
7113
|
+
</main>
|
|
7114
|
+
<footer>Each step expires after 10 minutes. Nothing is added to the pool until you confirm.</footer>
|
|
7115
|
+
<noscript><p class="notice err">JavaScript is required for this sign-in.</p></noscript>
|
|
7116
|
+
<script nonce="${nonce}">
|
|
7031
7117
|
let token = location.hash.slice(1);
|
|
7032
7118
|
history.replaceState(null, '', location.pathname);
|
|
7033
7119
|
addEventListener('hashchange', () => { if (location.hash) location.reload(); });
|
|
7034
7120
|
const get = id => document.getElementById(id);
|
|
7035
|
-
const
|
|
7036
|
-
|
|
7037
|
-
|
|
7121
|
+
const steps = ['email', 'signin', 'code', 'confirm', 'done'];
|
|
7122
|
+
let current = 'email';
|
|
7123
|
+
function show(step) {
|
|
7124
|
+
const failed = step === 'error';
|
|
7125
|
+
const index = steps.indexOf(failed ? current : step);
|
|
7126
|
+
if (!failed) current = step;
|
|
7127
|
+
for (const name of [...steps, 'error']) get('step-' + name).hidden = name !== step;
|
|
7128
|
+
document.querySelectorAll('#stepper li').forEach((item, position) => {
|
|
7129
|
+
item.className = position < index || step === 'done' ? 'done' : position === index ? (failed ? 'failed' : 'active') : '';
|
|
7130
|
+
item.querySelector('.dot').innerHTML = item.className === 'done' ? '${icon("check")}' : String(position + 1);
|
|
7131
|
+
if (position === index) item.setAttribute('aria-current', 'step'); else item.removeAttribute('aria-current');
|
|
7132
|
+
});
|
|
7133
|
+
get('progress').style.width = Math.max(5, ((index + (step === 'done' ? 1 : 0)) / steps.length) * 100) + '%';
|
|
7134
|
+
(step === 'email' ? get('email') : get('step-' + step).querySelector('h1')).focus();
|
|
7135
|
+
}
|
|
7136
|
+
async function send(step, body) {
|
|
7137
|
+
const response = await fetch('/pool-enroll/' + step, {method:'POST', credentials:'omit', cache:'no-store', redirect:'error', headers:{'content-type':'application/json'}, body:JSON.stringify(body)});
|
|
7038
7138
|
const data = await response.json();
|
|
7039
|
-
if (!response.ok) throw new Error(data.error || 'Sign-in failed.
|
|
7139
|
+
if (!response.ok) throw new Error(data.error || 'Sign-in failed. Start again.');
|
|
7040
7140
|
token = data.token || '';
|
|
7041
7141
|
return data;
|
|
7042
7142
|
}
|
|
7143
|
+
const post = (step, values) => send(step, {token, ...values});
|
|
7043
7144
|
function fail(error) {
|
|
7044
|
-
token = ''; get('code').value = '';
|
|
7045
|
-
|
|
7145
|
+
token = ''; get('code').value = '';
|
|
7146
|
+
get('error-message').textContent = error instanceof Error ? error.message : 'Sign-in failed. Start again.';
|
|
7147
|
+
show('error');
|
|
7046
7148
|
}
|
|
7047
|
-
|
|
7149
|
+
show(/^[A-Za-z0-9_-]{43}$/.test(token) ? 'signin' : 'email');
|
|
7150
|
+
get('request').addEventListener('submit', async event => {
|
|
7151
|
+
event.preventDefault(); event.submitter.disabled = true; get('email-error').hidden = true;
|
|
7152
|
+
try {
|
|
7153
|
+
const data = await send('request', {email: get('email').value});
|
|
7154
|
+
get('signin-email').textContent = data.email; get('signin-for').hidden = false;
|
|
7155
|
+
show('signin');
|
|
7156
|
+
} catch (error) {
|
|
7157
|
+
get('email-error').textContent = error instanceof Error ? error.message : 'This email cannot connect. Ask the operator.';
|
|
7158
|
+
get('email-error').hidden = false; get('email').focus();
|
|
7159
|
+
} finally { event.submitter.disabled = false; }
|
|
7160
|
+
});
|
|
7048
7161
|
get('start').addEventListener('click', async () => {
|
|
7162
|
+
const tab = window.open('', '_blank');
|
|
7049
7163
|
get('start').disabled = true;
|
|
7050
7164
|
try {
|
|
7051
7165
|
const data = await post('start', {});
|
|
7052
7166
|
const url = new URL(data.authUrl);
|
|
7053
|
-
if (url.origin !== 'https://claude.ai' || url.pathname !== '/oauth/authorize') throw new Error('Invalid sign-in destination. Ask
|
|
7054
|
-
get('
|
|
7055
|
-
|
|
7056
|
-
|
|
7167
|
+
if (url.origin !== 'https://claude.ai' || url.pathname !== '/oauth/authorize') throw new Error('Invalid sign-in destination. Ask for help.');
|
|
7168
|
+
get('reopen').href = url.href;
|
|
7169
|
+
if (tab) { tab.opener = null; tab.location.href = url.href; }
|
|
7170
|
+
else get('code-lead').textContent = 'Your browser blocked the new tab. Open Claude sign-in with the link below, sign in, then paste the code it shows.';
|
|
7171
|
+
show('code');
|
|
7172
|
+
} catch (error) { if (tab) tab.close(); fail(error); }
|
|
7057
7173
|
});
|
|
7058
7174
|
get('verify').addEventListener('submit', async event => {
|
|
7059
|
-
event.preventDefault();
|
|
7175
|
+
event.preventDefault(); event.submitter.disabled = true;
|
|
7060
7176
|
const code = get('code').value.trim(); get('code').value = '';
|
|
7061
|
-
try { const data = await post('verify', {code}); get('
|
|
7062
|
-
catch(error) { fail(error); }
|
|
7177
|
+
try { const data = await post('verify', {code}); get('account').textContent = data.email; show('confirm'); }
|
|
7178
|
+
catch (error) { fail(error); }
|
|
7063
7179
|
});
|
|
7064
7180
|
get('confirm').addEventListener('submit', async event => {
|
|
7065
7181
|
event.preventDefault(); if (!get('consent').checked) return; event.submitter.disabled = true;
|
|
7066
|
-
try { await post('confirm', {consent:true}); get('
|
|
7067
|
-
catch(error) { fail(error); }
|
|
7182
|
+
try { await post('confirm', {consent:true}); get('connected-account').textContent = get('account').textContent; show('done'); }
|
|
7183
|
+
catch (error) { fail(error); }
|
|
7068
7184
|
});
|
|
7069
7185
|
get('disconnect').addEventListener('click', async () => {
|
|
7070
|
-
get('disconnect').disabled = true;
|
|
7186
|
+
get('disconnect').disabled = true; get('status').hidden = true;
|
|
7071
7187
|
try {
|
|
7072
7188
|
await post('disconnect', {});
|
|
7073
|
-
get('
|
|
7074
|
-
get('consent').checked = false; get('code').value = '';
|
|
7075
|
-
get('authorize').removeAttribute('href'); get('authorize').hidden = true;
|
|
7189
|
+
get('account').textContent = ''; get('connected-account').textContent = ''; get('consent').checked = false;
|
|
7076
7190
|
document.querySelectorAll('button').forEach(button => { button.disabled = false; });
|
|
7077
|
-
get('
|
|
7078
|
-
|
|
7079
|
-
|
|
7191
|
+
get('signin-notice').textContent = 'Subscription disconnected: it is no longer in the pool. You can connect again or close this page.';
|
|
7192
|
+
get('signin-notice').hidden = false;
|
|
7193
|
+
show('signin');
|
|
7194
|
+
} catch (error) {
|
|
7080
7195
|
get('disconnect').disabled = false;
|
|
7081
|
-
status.textContent = 'Removal is not confirmed. ' + (error instanceof Error ? error.message : 'Keep this page open and ask
|
|
7196
|
+
get('status').textContent = 'Removal is not confirmed. ' + (error instanceof Error ? error.message : 'Keep this page open and ask for help with this seat.');
|
|
7197
|
+
get('status').hidden = false;
|
|
7082
7198
|
}
|
|
7083
7199
|
});
|
|
7084
7200
|
</script></body></html>`, {
|
|
@@ -7092,7 +7208,61 @@ get('disconnect').addEventListener('click', async () => {
|
|
|
7092
7208
|
}
|
|
7093
7209
|
});
|
|
7094
7210
|
}
|
|
7095
|
-
var
|
|
7211
|
+
var icons, icon = (name) => `<svg viewBox="0 0 24 24" aria-hidden="true">${icons[name]}</svg>`, styles = `
|
|
7212
|
+
:root{--page:#faf9f6;--card:#fff;--text:#191b21;--muted:#5a606b;--faint:#9aa0ab;--line:#e6e8ec;--border:rgba(25,27,33,.14);--brand:#fb5d22;--accent:#dc5518;--tint:#fff3ec;--ink:#182025;--ink-hover:#2a333b;--ok:#17a06b;--ok-bg:#e3f6ed;--ok-text:#0c6444;--err:#d92d20;--err-bg:#fef3f2;--err-text:#912018;color-scheme:light;font:15px/1.55 "Public Sans",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--text);background:var(--page)}
|
|
7213
|
+
*{box-sizing:border-box}body{margin:0;min-height:100vh}
|
|
7214
|
+
header{position:relative;background:#fff;border-bottom:1px solid var(--line);padding:16px clamp(16px,4vw,32px);font-weight:600}
|
|
7215
|
+
header small{display:block;font-weight:400;color:var(--muted);font-size:13px}
|
|
7216
|
+
.bar{position:absolute;left:0;right:0;bottom:-1px;height:4px;background:#f2f1ec}.bar span{display:block;height:100%;width:5%;background:var(--ink);transition:width .3s ease-in-out}
|
|
7217
|
+
main{max-width:560px;margin:0 auto;padding:32px 16px 48px}
|
|
7218
|
+
ol{list-style:none;display:flex;margin:0 0 28px;padding:0}
|
|
7219
|
+
ol li{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;position:relative;font-size:13px;color:var(--faint)}
|
|
7220
|
+
ol li+li::before{content:"";position:absolute;top:15px;right:calc(50% + 20px);width:calc(100% - 40px);height:2px;background:var(--line)}
|
|
7221
|
+
ol li.done+li::before{background:var(--ok-text)}
|
|
7222
|
+
ol .dot{width:32px;height:32px;border-radius:50%;display:grid;place-items:center;font-weight:600;background:var(--line);color:var(--faint);border:2px solid var(--line)}
|
|
7223
|
+
ol .dot svg{width:16px;height:16px;stroke:#fff;stroke-width:3}
|
|
7224
|
+
ol li.active{color:var(--accent);font-weight:600}ol li.active .dot{background:var(--brand);border-color:var(--brand);color:#fff}
|
|
7225
|
+
ol li.done{color:var(--muted)}ol li.done .dot{background:var(--ok);border-color:var(--ok)}
|
|
7226
|
+
ol li.failed .dot{background:var(--err);border-color:var(--err);color:#fff}
|
|
7227
|
+
.card{background:var(--card);border-radius:16px;box-shadow:0 1px 2px rgba(16,18,24,.05);border:1px solid rgba(25,27,33,.08);padding:clamp(24px,5vw,40px);text-align:center}
|
|
7228
|
+
.avatar{width:64px;height:64px;margin:0 auto 16px;border-radius:50%;display:grid;place-items:center;background:var(--tint)}
|
|
7229
|
+
.avatar svg,.button svg{width:28px;height:28px;fill:none;stroke:var(--accent);stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
|
7230
|
+
.avatar.ok{background:var(--ok-bg)}.avatar.ok svg{stroke:var(--ok-text);stroke-width:2.4}.avatar.err{background:var(--err-bg)}.avatar.err svg{stroke:var(--err)}
|
|
7231
|
+
h1{font-size:22px;line-height:1.3;margin:0 0 8px;outline:none}
|
|
7232
|
+
.lead{color:var(--muted);margin:0 auto 24px;max-width:42ch}
|
|
7233
|
+
.facts{list-style:none;padding:0;margin:0 0 28px;text-align:left;display:grid;gap:10px}
|
|
7234
|
+
.facts li{display:flex;gap:10px;align-items:flex-start}.facts li::before{content:"";flex:none;width:6px;height:6px;margin-top:9px;border-radius:50%;background:var(--brand)}
|
|
7235
|
+
.button{font:inherit;font-weight:600;display:inline-flex;align-items:center;justify-content:center;gap:8px;width:100%;min-height:44px;padding:10px 16px;border-radius:9px;border:0;background:var(--ink);color:#fff;cursor:pointer;text-decoration:none}
|
|
7236
|
+
.button:hover{background:var(--ink-hover);box-shadow:0 6px 18px -6px rgb(24 32 37/.5)}.button svg{width:18px;height:18px;stroke:currentColor}
|
|
7237
|
+
.button.secondary{background:#fff;color:var(--text);border:1px solid var(--border)}.button.secondary:hover{box-shadow:0 1px 3px rgba(47,43,67,.12)}
|
|
7238
|
+
.button:disabled{opacity:.5;cursor:progress;box-shadow:none}
|
|
7239
|
+
.button:focus-visible,input:focus-visible,a:focus-visible{outline:3px solid var(--brand);outline-offset:3px}
|
|
7240
|
+
.link{display:inline-flex;align-items:center;gap:6px;color:var(--accent);font-weight:600;text-decoration:none}.link svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2}
|
|
7241
|
+
form{text-align:left;display:grid;gap:16px}label{font-weight:600}
|
|
7242
|
+
input[type=password],input[type=email]{display:block;width:100%;font:inherit;padding:11px 12px;margin-top:6px;border:1px solid var(--border);border-radius:9px;background:#fff}
|
|
7243
|
+
.hint{font-size:13px;color:var(--muted);margin:6px 0 0;font-weight:400}
|
|
7244
|
+
.account{display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:12px;background:var(--page);border:1px solid rgba(25,27,33,.08);font-weight:600;overflow-wrap:anywhere}
|
|
7245
|
+
.account svg{flex:none;width:22px;height:22px;fill:none;stroke:var(--ok-text);stroke-width:2.4}
|
|
7246
|
+
.consent{display:flex;gap:10px;align-items:flex-start;font-weight:400}.consent input{width:18px;height:18px;margin:2px 0 0;accent-color:var(--ink)}
|
|
7247
|
+
.notice{border-radius:12px;padding:12px 16px;margin:0 0 20px;text-align:left;background:var(--ok-bg);color:var(--ok-text)}
|
|
7248
|
+
.notice.err{background:var(--err-bg);color:var(--err-text)}
|
|
7249
|
+
.aside{margin-top:24px;padding-top:20px;border-top:1px solid var(--line);text-align:left;color:var(--muted);font-size:14px}
|
|
7250
|
+
.aside .button{margin-top:12px}
|
|
7251
|
+
footer{max-width:560px;margin:0 auto;padding:0 16px 32px;color:var(--muted);font-size:13px;text-align:center}
|
|
7252
|
+
@media (max-width:480px){ol li:not(.active) .label{display:none}}
|
|
7253
|
+
[hidden]{display:none!important}
|
|
7254
|
+
`;
|
|
7255
|
+
var init_enrollment_page = __esm(() => {
|
|
7256
|
+
icons = {
|
|
7257
|
+
mail: '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/>',
|
|
7258
|
+
key: '<path d="M15 7.5a3.5 3.5 0 1 1-3.4 4.3L4 19.5V22h3v-2h2v-2h2l1.2-1.2A3.5 3.5 0 0 1 15 7.5Z"/><circle cx="16" cy="11" r="1"/>',
|
|
7259
|
+
code: '<rect x="4" y="6" width="16" height="12" rx="2"/><path d="M8 10h.01M12 10h.01M16 10h.01M8 14h8"/>',
|
|
7260
|
+
person: '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
|
|
7261
|
+
check: '<path d="m5 12.5 4.5 4.5L19 7.5"/>',
|
|
7262
|
+
alert: '<path d="M12 8v5M12 16.5h.01"/><circle cx="12" cy="12" r="9"/>',
|
|
7263
|
+
open: '<path d="M14 5h5v5M19 5l-8 8M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/>'
|
|
7264
|
+
};
|
|
7265
|
+
});
|
|
7096
7266
|
|
|
7097
7267
|
// ../../packages/api/src/handlers/enrollment.ts
|
|
7098
7268
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
@@ -7134,13 +7304,59 @@ class EnrollmentHandlers {
|
|
|
7134
7304
|
return await action();
|
|
7135
7305
|
} catch (error) {
|
|
7136
7306
|
return error instanceof EnrollmentError ? json({ error: error.message }, error.status) : json({
|
|
7137
|
-
error: "Enrollment could not be saved. Ask the operator to check the seat
|
|
7307
|
+
error: "Enrollment could not be saved. Ask the operator to check the seat."
|
|
7138
7308
|
}, 500);
|
|
7139
7309
|
}
|
|
7140
7310
|
}
|
|
7141
|
-
|
|
7311
|
+
requireClaudeCode() {
|
|
7142
7312
|
if (!this.context.providerRegistry.getOAuthProvider("claude-code"))
|
|
7143
7313
|
throw new EnrollmentError(400, "Provider 'claude-code' is disabled. Set providers.claude-code.enabled to true in your config.");
|
|
7314
|
+
}
|
|
7315
|
+
reserve(name) {
|
|
7316
|
+
if (this.context.dbOps.getAccountByName(name))
|
|
7317
|
+
throw new EnrollmentError(409, "This seat is already in the pool.");
|
|
7318
|
+
for (const entry of this.entries.values())
|
|
7319
|
+
if (entry.details.name === name && entry.state.kind === "busy")
|
|
7320
|
+
throw new EnrollmentError(409, "Sign-in is already in progress");
|
|
7321
|
+
for (const [key, entry] of this.entries)
|
|
7322
|
+
if (entry.details.name === name)
|
|
7323
|
+
this.entries.delete(key);
|
|
7324
|
+
if (this.entries.size >= 100)
|
|
7325
|
+
throw new EnrollmentError(429, "Too many pending invitations");
|
|
7326
|
+
}
|
|
7327
|
+
request(body) {
|
|
7328
|
+
this.requireClaudeCode();
|
|
7329
|
+
if (Object.keys(body).some((key) => key !== "email"))
|
|
7330
|
+
throw new EnrollmentError(400, "Unknown request field");
|
|
7331
|
+
const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
|
|
7332
|
+
if (!this.context.config.getEnrollmentEmails().includes(email))
|
|
7333
|
+
throw new EnrollmentError(403, "This email is not on the list. Ask the operator to add it.");
|
|
7334
|
+
const name = email.slice(0, email.indexOf("@")).replace(/[^a-z0-9._+-]/g, "-").slice(0, 64);
|
|
7335
|
+
this.reserve(name);
|
|
7336
|
+
const issued = this.mint({ name, email, contactEmail: email }, { kind: "invited" }, tenMinutes);
|
|
7337
|
+
return json({ ...issued, email });
|
|
7338
|
+
}
|
|
7339
|
+
emails = (req) => this.guarded(async () => {
|
|
7340
|
+
const body = await readBody(req);
|
|
7341
|
+
const lists = Object.entries(body);
|
|
7342
|
+
if (lists.some(([key, value]) => key !== "add" && key !== "remove" || !Array.isArray(value) || value.some((email) => typeof email !== "string")))
|
|
7343
|
+
throw new EnrollmentError(400, "Expected { add?: string[], remove?: string[] }");
|
|
7344
|
+
const { config } = this.context;
|
|
7345
|
+
const add = body.add ?? [];
|
|
7346
|
+
const remove = new Set((body.remove ?? []).map((email) => email.trim().toLowerCase()));
|
|
7347
|
+
if (add.length === 0 && remove.size === 0)
|
|
7348
|
+
return json({ emails: config.getEnrollmentEmails() });
|
|
7349
|
+
const next = [...config.getEnrollmentEmails(), ...add].filter((email) => !remove.has(email.trim().toLowerCase()));
|
|
7350
|
+
try {
|
|
7351
|
+
return json({ emails: config.setEnrollmentEmails(next) });
|
|
7352
|
+
} catch (error) {
|
|
7353
|
+
if (error instanceof ValidationError)
|
|
7354
|
+
throw new EnrollmentError(400, error.message);
|
|
7355
|
+
throw error;
|
|
7356
|
+
}
|
|
7357
|
+
});
|
|
7358
|
+
issue = (req) => this.guarded(async () => {
|
|
7359
|
+
this.requireClaudeCode();
|
|
7144
7360
|
const input = await readBody(req);
|
|
7145
7361
|
const allowed = new Set(["name", "email", "contactEmail", "slackUserId"]);
|
|
7146
7362
|
if (Object.keys(input).some((key) => !allowed.has(key)))
|
|
@@ -7163,16 +7379,7 @@ class EnrollmentHandlers {
|
|
|
7163
7379
|
}
|
|
7164
7380
|
if (details.name.length > 80 || details.slackUserId && !/^U[A-Z0-9]{8,20}$/.test(details.slackUserId) || [details.email, details.contactEmail].some((email) => email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)))
|
|
7165
7381
|
throw new EnrollmentError(400, "Invalid invitation details");
|
|
7166
|
-
|
|
7167
|
-
throw new EnrollmentError(409, "Seat already exists");
|
|
7168
|
-
for (const entry of this.entries.values())
|
|
7169
|
-
if (entry.details.name === details.name && entry.state.kind === "busy")
|
|
7170
|
-
throw new EnrollmentError(409, "Sign-in is already in progress");
|
|
7171
|
-
for (const [key, entry] of this.entries)
|
|
7172
|
-
if (entry.details.name === details.name)
|
|
7173
|
-
this.entries.delete(key);
|
|
7174
|
-
if (this.entries.size >= 100)
|
|
7175
|
-
throw new EnrollmentError(429, "Too many pending invitations");
|
|
7382
|
+
this.reserve(details.name);
|
|
7176
7383
|
const issued = this.mint(details, { kind: "invited" }, oneDay);
|
|
7177
7384
|
return json({
|
|
7178
7385
|
url: `${this.origin.origin}/pool-enroll#${issued.token}`,
|
|
@@ -7194,12 +7401,14 @@ class EnrollmentHandlers {
|
|
|
7194
7401
|
if (req.headers.get("origin") !== this.origin.origin)
|
|
7195
7402
|
throw new EnrollmentError(403, "Untrusted enrollment origin");
|
|
7196
7403
|
const body = await readBody(req);
|
|
7404
|
+
if (url.pathname === "/pool-enroll/request")
|
|
7405
|
+
return this.request(body);
|
|
7197
7406
|
const token = typeof body.token === "string" && /^[A-Za-z0-9_-]{43}$/.test(body.token) ? body.token : "";
|
|
7198
7407
|
const key = hash(token);
|
|
7199
7408
|
const entry = this.entries.get(key);
|
|
7200
7409
|
const expected = url.pathname === "/pool-enroll/start" ? "invited" : url.pathname === "/pool-enroll/verify" ? "started" : url.pathname === "/pool-enroll/confirm" ? "verified" : "connected";
|
|
7201
7410
|
if (!entry || entry.expiresAt <= this.now() || entry.state.kind !== expected)
|
|
7202
|
-
throw new EnrollmentError(410, "This
|
|
7411
|
+
throw new EnrollmentError(410, "This sign-in has expired or was already used. Start again.");
|
|
7203
7412
|
const state = entry.state;
|
|
7204
7413
|
entry.state = { kind: "busy" };
|
|
7205
7414
|
try {
|
|
@@ -7218,12 +7427,12 @@ class EnrollmentHandlers {
|
|
|
7218
7427
|
try {
|
|
7219
7428
|
seat = await this.oauth.verifyClaudeSeat(state.flow, body.code.trim());
|
|
7220
7429
|
} catch {
|
|
7221
|
-
throw new EnrollmentError(400, "Claude sign-in could not be verified.
|
|
7430
|
+
throw new EnrollmentError(400, "Claude sign-in could not be verified. Start again.");
|
|
7222
7431
|
}
|
|
7223
7432
|
this.assertCurrent(key, entry);
|
|
7224
7433
|
const { email } = seat.identity;
|
|
7225
7434
|
if (!email || entry.details.email && email.toLowerCase() !== entry.details.email.toLowerCase())
|
|
7226
|
-
throw new EnrollmentError(400, "This Claude account does not match
|
|
7435
|
+
throw new EnrollmentError(400, "This Claude account does not match your email. Start again and sign in with the matching account.");
|
|
7227
7436
|
this.assertNewSeat(entry.details.name, seat);
|
|
7228
7437
|
return json({
|
|
7229
7438
|
...this.mint(entry.details, { kind: "verified", seat }, tenMinutes),
|
|
@@ -7266,7 +7475,7 @@ class EnrollmentHandlers {
|
|
|
7266
7475
|
}
|
|
7267
7476
|
assertCurrent(key, entry) {
|
|
7268
7477
|
if (this.entries.get(key) !== entry || entry.expiresAt <= this.now())
|
|
7269
|
-
throw new EnrollmentError(410, "Sign-in expired.
|
|
7478
|
+
throw new EnrollmentError(410, "Sign-in expired. Start again.");
|
|
7270
7479
|
}
|
|
7271
7480
|
assertNewSeat(name, seat) {
|
|
7272
7481
|
if (this.context.dbOps.getAccountByName(name) || this.context.dbOps.getAccountsByProvider("claude-code").some((account2) => account2.account_uuid === seat.identity.accountUuid))
|
|
@@ -7346,11 +7555,13 @@ async function readBody(req) {
|
|
|
7346
7555
|
}
|
|
7347
7556
|
var paths, tenMinutes = 600000, oneDay = 86400000, headers2, json = (value, status = 200) => Response.json(value, { status, headers: headers2 }), hash = (token) => createHash("sha256").update(token).digest("hex"), EnrollmentError;
|
|
7348
7557
|
var init_enrollment = __esm(() => {
|
|
7558
|
+
init_src2();
|
|
7349
7559
|
init_src6();
|
|
7350
7560
|
init_src();
|
|
7351
7561
|
init_enrollment_page();
|
|
7352
7562
|
paths = new Set([
|
|
7353
7563
|
"/pool-enroll",
|
|
7564
|
+
"/pool-enroll/request",
|
|
7354
7565
|
"/pool-enroll/start",
|
|
7355
7566
|
"/pool-enroll/verify",
|
|
7356
7567
|
"/pool-enroll/confirm",
|
|
@@ -7767,8 +7978,10 @@ class APIRouter {
|
|
|
7767
7978
|
this.registerHandlers();
|
|
7768
7979
|
}
|
|
7769
7980
|
registerHandlers() {
|
|
7770
|
-
if (this.enrollment)
|
|
7981
|
+
if (this.enrollment) {
|
|
7771
7982
|
this.staticHandlers.set("POST:/api/enrollment-invitations", this.enrollment.issue);
|
|
7983
|
+
this.staticHandlers.set("POST:/api/enrollment-emails", this.enrollment.emails);
|
|
7984
|
+
}
|
|
7772
7985
|
const { config, dbOps, getProviders, getRuntimeHealth, providerRegistry } = this.context;
|
|
7773
7986
|
const healthHandler = createHealthHandler(dbOps, config, getProviders, getRuntimeHealth);
|
|
7774
7987
|
const statsHandler = createStatsHandler(dbOps);
|
package/package.json
CHANGED