// ========================================== // Armature — SDK Entry Point // ========================================== // Assembles all SDK modules into the `sw` object // and runs the boot sequence. // // Usage: // import { boot } from './js/sw/sdk/index.js'; // const sw = await boot(); // // Exports: boot() → window.sw // ========================================== import { createRestClient } from './rest-client.js'; import { createAuth } from './auth.js'; import { createCan } from './can.js'; import { createDomains } from './api-domains.js'; import { createEvents } from './events.js'; import { createPipe } from './pipe.js'; import { createTheme } from './theme.js'; import { createStorage } from './storage.js'; import { createSlots } from './slots.js'; import { createActions } from './actions.js'; import { createRealtime } from './realtime.js'; import { createRenderers } from './renderers.js'; import { createMarkdown } from './markdown.js'; import { createUsers } from './users.js'; import { createTesting } from './testing.js'; import { createForms } from './forms.js'; import { confirm } from '../primitives/confirm.js'; import { prompt } from '../primitives/prompt.js'; /** * Boot the SDK. Assembles modules, loads auth state, connects WebSocket. * Idempotent — returns the same sw object on subsequent calls. * * @returns {Promise} sw */ export async function boot() { // Idempotent if (window.sw?._sdk) return window.sw; // 1. Auth — owns token state const auth = createAuth(); // 2. REST client — uses auth for token injection + refresh const restClient = createRestClient( () => auth._getToken(), () => auth._onRefresh(), () => auth._on401Failure() ); // 3. Wire rest client into auth (auth needs it for login/refresh/permissions calls) auth._setRestClient(restClient); // 4. Events — with WS auth callback const events = createEvents(async () => { // Prefer ticket exchange try { const data = await restClient.post('/api/v1/ws/ticket', {}); if (data?.ticket) return `ticket=${encodeURIComponent(data.ticket)}`; } catch (_) { /* fall through to legacy */ } // Legacy fallback: JWT in query param const token = auth._getToken(); return token ? `token=${encodeURIComponent(token)}` : null; }); // 5. Wire emit into auth auth._setEmit(events.emit.bind(events)); // 6. Remaining modules const { can, isAdmin, isTeamAdmin } = createCan(auth); const api = createDomains(restClient); const pipe = createPipe(); const theme = createTheme(events.emit.bind(events)); const storage = createStorage(); const slots = createSlots(events.emit.bind(events)); const actions = createActions(events.emit.bind(events)); const realtime = createRealtime(events); const renderers = createRenderers(events.emit.bind(events)); const markdown = createMarkdown(renderers); // 7. Assemble sw object const sw = Object.create(null); // Auth sw.auth = auth; // API domains sw.api = api; // RBAC sw.can = can; Object.defineProperty(sw, 'isAdmin', { get() { return isAdmin(); }, enumerable: true, }); sw.isTeamAdmin = isTeamAdmin; // Events (expose as top-level convenience + events namespace for connect/disconnect) sw.on = events.on.bind(events); sw.once = events.once.bind(events); sw.off = events.off.bind(events); sw.emit = events.emit.bind(events); sw.events = events; // Pipe, Theme, Storage, Slots, Actions sw.pipe = pipe; sw.theme = theme; sw.storage = storage; sw.slots = slots; sw.actions = actions; // Realtime — room-scoped pub/sub over WebSocket sw.realtime = realtime; // Renderers — kernel-level block/post renderer registry sw.renderers = renderers; // Markdown — unified renderer (marked + DOMPurify + sw.renderers pipeline) sw.markdown = markdown; // Users — identity resolution with local caching sw.users = createUsers(restClient); // Testing — structured test framework for surface runners sw.testing = createTesting(events, restClient); // Forms — typed form rendering + validation (v0.9.5) sw.forms = createForms(restClient); // Shell helpers — imperative confirm/prompt backed by primitives sw.confirm = confirm; sw.prompt = prompt; // Shell — layout utilities (decouples surface code from shell DOM) const _shell = { /** @deprecated v0.6.10 — CSS zoom handles reflow; no scale correction needed. */ getScale() { return 1; }, /** Topbar — set after dynamic import below */ Topbar: null, }; sw.shell = _shell; // Toast — dynamic import to avoid module resolution issues try { const toastMod = await import('../primitives/toast.js'); sw.toast = toastMod.toast; events.on('toast', ({ message, variant, duration }) => { toastMod.toast(message, variant, duration); }); } catch (e) { console.warn('[sw] Toast import failed:', e.message); } // UI primitives — extensions use sw.ui.Button, sw.ui.Dialog, etc. try { const primitives = await import('../primitives/index.js'); sw.ui = Object.freeze({ ...primitives }); } catch (e) { console.warn('[sw] Primitives import failed:', e.message); sw.ui = Object.freeze({}); } // Topbar — standard navigation bar for surfaces (legacy API) try { const topbarMod = await import('../shell/topbar.js'); _shell.Topbar = topbarMod.Topbar; } catch (e) { console.warn('[sw] Topbar import failed:', e.message); } // Shell Topbar — kernel-injected two-slot topbar try { const shellTopbarMod = await import('../shell/shell-topbar.js'); _shell.topbar = shellTopbarMod.shellTopbarAPI; _shell._ShellTopbar = shellTopbarMod.ShellTopbar; } catch (e) { console.warn('[sw] ShellTopbar import failed:', e.message); } // Freeze shell now that all shell modules are loaded sw.shell = Object.freeze(_shell); // UserMenu render helper — surfaces call sw.userMenu(container, opts) sw.userMenu = function (container, opts = {}) { import('../shell/user-menu.js').then(({ UserMenu }) => { const { render } = preact; render(html`<${UserMenu} ...${opts} />`, container); }); }; // Navigate — multi-surface intra-package routing sw.navigate = function (path, params) { const manifest = window.__MANIFEST__; if (!manifest?.id) { console.warn('[sw] navigate: no manifest — not inside an extension surface'); return; } // Interpolate :param placeholders let resolved = path; if (params) { for (const [k, v] of Object.entries(params)) { resolved = resolved.replace(':' + k, encodeURIComponent(v)); } } const base = window.__BASE__ || ''; const url = base + '/s/' + manifest.id + resolved; // Update globals so the package can re-render window.__SURFACE_PATH__ = path; window.__SURFACE_PARAMS__ = params || {}; history.pushState({ surfacePath: path, surfaceParams: params }, '', url); events.emit('surface.navigate', { path, params: params || {}, url }, { localOnly: true }); }; // Listen for popstate (back/forward) to emit navigation events window.addEventListener('popstate', (e) => { if (e.state?.surfacePath != null) { window.__SURFACE_PATH__ = e.state.surfacePath; window.__SURFACE_PARAMS__ = e.state.surfaceParams || {}; events.emit('surface.navigate', { path: e.state.surfacePath, params: e.state.surfaceParams || {}, url: location.pathname, }, { localOnly: true }); } }); // Seed initial history state so back-navigation from sw.navigate() works. // Without this, the initial server-rendered page has no pushState entry, // so pressing back after sw.navigate() fires popstate with null state. if (window.__SURFACE_PATH__) { history.replaceState( { surfacePath: window.__SURFACE_PATH__, surfaceParams: window.__SURFACE_PARAMS__ || {} }, '' ); } // Marker for idempotency sw._sdk = '0.9.5'; // 8. Expose globally window.sw = sw; // 9. Boot sequence theme.init(); try { await auth.boot(); } catch (e) { console.warn('[sw] Auth boot failed (expected without backend):', e.message); } if (auth.isAuthenticated) { const BASE = window.__BASE__ || ''; events.connect(BASE + '/ws'); } // 10. Mount imperative hosts (Toast, DialogStack) if not already present. // Core surfaces mount these via AppShell; extension surfaces need SDK to do it. try { const hostId = 'sw-imperative-hosts'; if (!document.getElementById(hostId)) { const mount = document.createElement('div'); mount.id = hostId; document.body.appendChild(mount); const { ToastContainer } = await import('../primitives/toast.js'); const { DialogStack } = await import('../shell/dialog-stack.js'); const { render } = preact; render(html`<${ToastContainer} /><${DialogStack} />`, mount); } } catch (e) { console.warn('[sw] Imperative host mount failed:', e.message); } // 11. Auto-mount shell topbar if #shell-topbar exists try { const shellMount = document.getElementById('shell-topbar'); if (shellMount && sw.shell._ShellTopbar && auth.isAuthenticated) { const { render } = preact; render(html`<${sw.shell._ShellTopbar} />`, shellMount); } } catch (e) { console.warn('[sw] Shell topbar mount failed:', e.message); } // 12. Signal ready events.emit('sdk.ready', {}, { localOnly: true }); document.dispatchEvent(new CustomEvent('sw:ready', { detail: { sw } })); console.log(`[sw] SDK v${sw._sdk} ready`); return sw; }