Files
Skald-Circle/web/components/shared/connector-common.js
T
dguiducci e6c4e202a4 feat(mcp): OAuth per-user connectors (§15) — providers, PKCE copy-paste flow, env credential delivery
- oauth_providers registry table (per-provider client creds) + db/oauth_providers.rs
- mcp/oauth.rs: authorization-code + PKCE S256, RAM-only TTL'd flow store, copy-paste consent
- mcp/install.rs + verify.rs: connector file install + manifest verification
- activate persists a pending row (needs_oauth); /mcp/oauth/start + /complete exchange code for refresh token
- credential delivery via env var on docker exec (google_authorized_user JSON), never on disk
- mcp_catalog/mcp_user_servers: additive OAuth columns (ensure_column), catalog_name/oauth_provider/deliver_json bare TEXT snapshots
- frontend: connector-detail.js (OAuth login panel), shared/connector-common.js, connectors.js admin Sign-in providers modal
- API: /mcp/providers (admin OAuth creds), /mcp/oauth/start|complete
- .gitignore: add /homes/ (instance data), /connectors/, /reset.sh; drop stale /secrets/
2026-07-17 21:47:51 +01:00

89 lines
3.5 KiB
JavaScript

// Shared vocabulary for the Connectors list and a connector's own page.
//
// Both surfaces have to answer "what state is this connector in?" and both draw the
// same env/secret form. Deriving that twice is how the two drift, so the derivation
// lives here and each page only decides layout.
/// The icon of an **installed** connector, off the box's own `connectors/` folder —
/// not the marketplace proxy, which is admin-only and dies with the feed.
export function connectorIconUrl(name, size = 'sm') {
return `/api/mcp/catalog/${encodeURIComponent(name)}/icon?size=${size}`;
}
/// How each status reads on a chip. `tone` maps to the `connector-chip--*` accents
/// in `web/css/connectors.css`.
export const STATUS_LABEL = {
active: { text: 'active', tone: 'ok' },
pending: { text: 'needs fix', tone: 'script' },
needs_login: { text: 'needs sign-in', tone: 'script' },
enabled: { text: 'enabled', tone: 'scope' },
off: { text: 'off', tone: '' },
available: { text: 'available', tone: '' },
};
/// The one place that decides what a connector's state *is*, from whichever runtime
/// rows exist for it.
///
/// A per-user activation whose credentials failed verification is `pending`, not
/// `active`: the row exists but is deliberately held out of `all_startable`, and
/// calling that "active" would be a lie the user acts on.
///
/// `enabled` vs `active` for a global is the §7 distinction between *running* and
/// *reachable by me*: an admin can enable a connector for someone else and never
/// grant it to themselves, and their own list must not claim they have it.
export function statusOf(row) {
if (row._act) {
if (row._act.auth_state !== 'pending') return 'active';
// An OAuth connector sitting at `pending` is waiting for its interactive
// sign-in, not for a failed credential to be fixed — a different ask.
return row._act.oauth_provider ? 'needs_login' : 'pending';
}
if (row._glob) {
if (!row._glob.enabled) return 'off';
return row._glob.can_use ? 'active' : 'enabled';
}
return 'available';
}
/// Normalizes a catalog entry's `config_schema_json` into form-field descriptors,
/// whether the feed shipped the object-array form or the legacy bare-name list.
export function normalizeSchema(raw) {
if (!Array.isArray(raw)) return [];
return raw.map(e => {
if (typeof e === 'string') {
return { name: e, label: e, description: '', required: false, secret: false, example: '', default: '' };
}
return {
name: e.name || '',
label: e.label || e.name || '',
description: e.description || '',
required: !!e.required,
secret: !!e.secret,
example: e.example || '',
default: e.default || '',
};
});
}
export function parseJson(s, fallback) {
if (!s) return fallback;
try { return JSON.parse(s); } catch { return fallback; }
}
/// Every schema field seeded with its `default` (empty string when none).
export function seedEnv(schema) {
return Object.fromEntries(schema.map(e => [e.name, e.default || '']));
}
export async function jf(url, opts) {
const res = await fetch(url, opts);
if (!res.ok) throw new Error(await res.text() || `HTTP ${res.status}`);
const ct = res.headers.get('content-type') || '';
return ct.includes('application/json') ? res.json() : null;
}
/// Tells the Connectors list its cached state is stale.
export function announceChange() {
window.dispatchEvent(new CustomEvent('connectors-changed'));
}