All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 3m6s
CI/CD / build-and-deploy (push) Successful in 29s
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
296 lines
10 KiB
JavaScript
296 lines
10 KiB
JavaScript
// ==========================================
|
|
// 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<object>} 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;
|
|
}
|