refactor(cloudcp): add Pulse Account portal store

This commit is contained in:
rcourtman 2026-03-26 08:54:48 +00:00
parent 8c86c497da
commit e50048f0bd
7 changed files with 236 additions and 235 deletions

View file

@ -1,5 +1,5 @@
{
"source_hash": "cee75c53fdd6a5b26d949440b84ab98240517e04cd1e40aaefacc0cc9d20f20a",
"source_hash": "633edb73d1996633e9ccd36ec746a2ea4eb295989063af03743102265c9d3cab",
"build_inputs": [
"package.json",
"tsconfig.json",

View file

@ -1,4 +1,43 @@
(() => {
// src/store.ts
function createAnonymousBootstrap(bootstrapDefaults2, overrides = {}) {
return {
authenticated: false,
email: "",
...bootstrapDefaults2,
...overrides,
accounts: normalizeAccounts(overrides.accounts)
};
}
function normalizeBootstrap(bootstrapDefaults2, raw) {
return createAnonymousBootstrap(bootstrapDefaults2, raw || {});
}
function createPortalStore(bootstrapDefaults2, initialBootstrap) {
var bootstrapState = normalizeBootstrap(bootstrapDefaults2, initialBootstrap);
var subscribers = /* @__PURE__ */ new Set();
return {
getBootstrap: function() {
return bootstrapState;
},
setBootstrap: function(nextBootstrap) {
bootstrapState = normalizeBootstrap(bootstrapDefaults2, nextBootstrap);
subscribers.forEach(function(listener) {
listener();
});
return bootstrapState;
},
subscribe: function(listener) {
subscribers.add(listener);
return function() {
subscribers.delete(listener);
};
}
};
}
function normalizeAccounts(accounts) {
return Array.isArray(accounts) ? accounts : [];
}
// src/runtime.ts
function readEmbeddedBootstrap() {
const bootstrapEl = document.getElementById("pulse-account-bootstrap");
@ -24,65 +63,7 @@
account_api_base_path: embeddedBootstrap.account_api_base_path || "/api/accounts",
portal_api_base_path: embeddedBootstrap.portal_api_base_path || "/api/portal"
};
function normalizeAccounts(accounts) {
return Array.isArray(accounts) ? accounts : [];
}
function createAnonymousBootstrap(overrides = {}) {
return {
authenticated: false,
email: "",
...bootstrapDefaults,
...overrides,
accounts: normalizeAccounts(overrides.accounts)
};
}
function normalizeBootstrap(raw) {
return createAnonymousBootstrap(raw || {});
}
var bootstrapState = normalizeBootstrap(embeddedBootstrap);
var renderSubscribers = /* @__PURE__ */ new Set();
function getBootstrap() {
return bootstrapState;
}
function setBootstrap(nextBootstrap) {
bootstrapState = normalizeBootstrap(nextBootstrap);
return bootstrapState;
}
function getCommercialAPIBaseURL() {
return bootstrapState.commercial_api_base_url;
}
function getPortalPath() {
return bootstrapState.portal_path;
}
function getBootstrapPath() {
return bootstrapState.bootstrap_path;
}
function getMagicLinkRequestPath() {
return bootstrapState.magic_link_request_path;
}
function getSignupPath() {
return bootstrapState.signup_path;
}
function getLogoutPath() {
return bootstrapState.logout_path;
}
function getAccountAPIBasePath() {
return bootstrapState.account_api_base_path;
}
function getPortalAPIBasePath() {
return bootstrapState.portal_api_base_path;
}
function notifyPortalRender() {
renderSubscribers.forEach((listener) => {
listener();
});
}
function subscribePortalRender(listener) {
renderSubscribers.add(listener);
return () => {
renderSubscribers.delete(listener);
};
}
var portalStore = createPortalStore(bootstrapDefaults, embeddedBootstrap);
// src/account_controller.ts
function getElement(id) {
@ -724,61 +705,45 @@
}
// src/shell.ts
var portalBootstrap = getBootstrap();
var LICENSE_API_BASE = getCommercialAPIBaseURL();
var PORTAL_PATH = getPortalPath();
var BOOTSTRAP_PATH = getBootstrapPath();
var MAGIC_LINK_REQUEST_PATH = getMagicLinkRequestPath();
var SIGNUP_PATH = getSignupPath();
var LOGOUT_PATH = getLogoutPath();
var ACCOUNT_API_BASE_PATH = getAccountAPIBasePath();
var PORTAL_API_BASE_PATH = getPortalAPIBasePath();
function renderHeader() {
var userInfo = document.getElementById("portal-user-info");
if (!userInfo) return;
var portalBootstrap = portalStore.getBootstrap();
userInfo.innerHTML = renderHeaderHTML({
bootstrap: portalBootstrap,
loginState: authController.getLoginState(),
signupPath: SIGNUP_PATH,
accountAPIBasePath: ACCOUNT_API_BASE_PATH
signupPath: portalBootstrap.signup_path,
accountAPIBasePath: portalBootstrap.account_api_base_path
});
}
function renderPortalApp() {
renderHeader();
var root = document.getElementById("portal-app-root");
if (!root) return;
var portalBootstrap = portalStore.getBootstrap();
var context = {
bootstrap: portalBootstrap,
loginState: authController.getLoginState(),
signupPath: SIGNUP_PATH,
accountAPIBasePath: ACCOUNT_API_BASE_PATH
signupPath: portalBootstrap.signup_path,
accountAPIBasePath: portalBootstrap.account_api_base_path
};
root.innerHTML = portalBootstrap.authenticated ? renderAuthenticatedPortalHTML(context) : renderSignedOutPortalHTML(context);
notifyPortalRender();
}
function applyBootstrap(data) {
portalBootstrap = setBootstrap(data || createAnonymousBootstrap());
LICENSE_API_BASE = getCommercialAPIBaseURL();
PORTAL_PATH = getPortalPath();
BOOTSTRAP_PATH = getBootstrapPath();
MAGIC_LINK_REQUEST_PATH = getMagicLinkRequestPath();
SIGNUP_PATH = getSignupPath();
LOGOUT_PATH = getLogoutPath();
ACCOUNT_API_BASE_PATH = getAccountAPIBasePath();
PORTAL_API_BASE_PATH = getPortalAPIBasePath();
var portalBootstrap = portalStore.setBootstrap(data || createAnonymousBootstrap(bootstrapDefaults));
if (!portalBootstrap.authenticated) {
authController.syncBootstrapEmail(portalBootstrap.email || "");
}
renderPortalApp();
}
async function refreshBootstrap() {
if (!BOOTSTRAP_PATH) return false;
var bootstrap = portalStore.getBootstrap();
if (!bootstrap.bootstrap_path) return false;
try {
var response = await fetch(BOOTSTRAP_PATH, {
var response = await fetch(bootstrap.bootstrap_path, {
headers: { "Accept": "application/json" }
});
if (response.status === 401) {
applyBootstrap(createAnonymousBootstrap());
applyBootstrap(createAnonymousBootstrap(bootstrapDefaults));
return true;
}
if (!response.ok) return false;
@ -801,31 +766,34 @@
}
var authController = installAuthController({
getMagicLinkRequestPath: function() {
return MAGIC_LINK_REQUEST_PATH;
return portalStore.getBootstrap().magic_link_request_path;
},
getLogoutPath: function() {
return LOGOUT_PATH;
return portalStore.getBootstrap().logout_path;
},
getPortalPath: function() {
return PORTAL_PATH;
return portalStore.getBootstrap().portal_path;
},
renderPortal: renderPortalApp
});
installAccountController({
getAccountAPIBasePath: function() {
return ACCOUNT_API_BASE_PATH;
return portalStore.getBootstrap().account_api_base_path;
},
getPortalAPIBasePath: function() {
return PORTAL_API_BASE_PATH;
return portalStore.getBootstrap().portal_api_base_path;
},
getPortalPath: function() {
return PORTAL_PATH;
return portalStore.getBootstrap().portal_path;
},
refreshBootstrap,
showToast
});
applyBootstrap(portalBootstrap);
if (portalBootstrap.authenticated) {
portalStore.subscribe(function() {
renderPortalApp();
});
applyBootstrap(portalStore.getBootstrap());
if (portalStore.getBootstrap().authenticated) {
refreshBootstrap();
}
@ -1008,11 +976,8 @@
// src/services.ts
var serviceState = createPortalServiceState();
function getCommercialAPIBaseURL2() {
return getCommercialAPIBaseURL();
}
function serviceFetch(path, body) {
return fetch(getCommercialAPIBaseURL2() + path, {
return fetch(portalStore.getBootstrap().commercial_api_base_url + path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
@ -1047,7 +1012,7 @@
renderRefund();
}
function renderRefund() {
renderRefundPanel(serviceState.refund, getBootstrap());
renderRefundPanel(serviceState.refund, portalStore.getBootstrap());
renderButton("refund-inline-submit", serviceState.refund.submitting, serviceState.refund.submitting ? "Processing..." : "Process Refund");
renderStatus("refund-inline-status", serviceState.refund.status);
}
@ -1306,7 +1271,7 @@
}
}
function syncServiceStateFromBootstrap() {
var bootstrap = getBootstrap();
var bootstrap = portalStore.getBootstrap();
if (!bootstrap.authenticated) {
return;
}
@ -1318,7 +1283,7 @@
renderAllFlows();
}
renderServiceRuntime();
subscribePortalRender(renderServiceRuntime);
portalStore.subscribe(renderServiceRuntime);
installServicesController({
toggleServicePanel,
focusElement,

View file

@ -1,3 +1,4 @@
import { createPortalStore } from './store';
import type { PortalBootstrapData } from './types';
function readEmbeddedBootstrap(): Partial<PortalBootstrapData> {
@ -14,7 +15,7 @@ function readEmbeddedBootstrap(): Partial<PortalBootstrapData> {
const embeddedBootstrap = readEmbeddedBootstrap();
const bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | 'accounts'> = {
export const bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | 'accounts'> = {
public_site_url: embeddedBootstrap.public_site_url || 'https://pulserelay.pro',
support_email: embeddedBootstrap.support_email || 'support@pulserelay.pro',
commercial_api_base_url: embeddedBootstrap.commercial_api_base_url || '',
@ -27,77 +28,4 @@ const bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | '
portal_api_base_path: embeddedBootstrap.portal_api_base_path || '/api/portal',
};
function normalizeAccounts(accounts: Partial<PortalBootstrapData>['accounts']): PortalBootstrapData['accounts'] {
return Array.isArray(accounts) ? accounts : [];
}
export function createAnonymousBootstrap(overrides: Partial<PortalBootstrapData> = {}): PortalBootstrapData {
return {
authenticated: false,
email: '',
...bootstrapDefaults,
...overrides,
accounts: normalizeAccounts(overrides.accounts),
};
}
export function normalizeBootstrap(raw: Partial<PortalBootstrapData> | null | undefined): PortalBootstrapData {
return createAnonymousBootstrap(raw || {});
}
let bootstrapState: PortalBootstrapData = normalizeBootstrap(embeddedBootstrap);
const renderSubscribers = new Set<() => void>();
export function getBootstrap(): PortalBootstrapData {
return bootstrapState;
}
export function setBootstrap(nextBootstrap: Partial<PortalBootstrapData> | PortalBootstrapData): PortalBootstrapData {
bootstrapState = normalizeBootstrap(nextBootstrap);
return bootstrapState;
}
export function getCommercialAPIBaseURL(): string {
return bootstrapState.commercial_api_base_url;
}
export function getPortalPath(): string {
return bootstrapState.portal_path;
}
export function getBootstrapPath(): string {
return bootstrapState.bootstrap_path;
}
export function getMagicLinkRequestPath(): string {
return bootstrapState.magic_link_request_path;
}
export function getSignupPath(): string {
return bootstrapState.signup_path;
}
export function getLogoutPath(): string {
return bootstrapState.logout_path;
}
export function getAccountAPIBasePath(): string {
return bootstrapState.account_api_base_path;
}
export function getPortalAPIBasePath(): string {
return bootstrapState.portal_api_base_path;
}
export function notifyPortalRender(): void {
renderSubscribers.forEach((listener) => {
listener();
});
}
export function subscribePortalRender(listener: () => void): () => void {
renderSubscribers.add(listener);
return () => {
renderSubscribers.delete(listener);
};
}
export const portalStore = createPortalStore(bootstrapDefaults, embeddedBootstrap);

View file

@ -1,4 +1,4 @@
import { getBootstrap, getCommercialAPIBaseURL as readCommercialAPIBaseURL, subscribePortalRender } from './runtime';
import { portalStore } from './runtime';
import { installServicesController } from './services_controller';
import {
clearFlowStatus,
@ -59,12 +59,8 @@ interface VerificationFlowDefinition {
var serviceState = createPortalServiceState();
function getCommercialAPIBaseURL() {
return readCommercialAPIBaseURL();
}
function serviceFetch(path, body) {
return fetch(getCommercialAPIBaseURL() + path, {
return fetch(portalStore.getBootstrap().commercial_api_base_url + path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
@ -103,7 +99,7 @@ interface VerificationFlowDefinition {
}
function renderRefund() {
renderRefundPanel(serviceState.refund, getBootstrap());
renderRefundPanel(serviceState.refund, portalStore.getBootstrap());
renderButton('refund-inline-submit', serviceState.refund.submitting, serviceState.refund.submitting ? 'Processing...' : 'Process Refund');
renderStatus('refund-inline-status', serviceState.refund.status);
}
@ -369,7 +365,7 @@ interface VerificationFlowDefinition {
}
function syncServiceStateFromBootstrap() {
var bootstrap = getBootstrap();
var bootstrap = portalStore.getBootstrap();
if (!bootstrap.authenticated) {
return;
}
@ -383,7 +379,7 @@ interface VerificationFlowDefinition {
}
renderServiceRuntime();
subscribePortalRender(renderServiceRuntime);
portalStore.subscribe(renderServiceRuntime);
installServicesController({
toggleServicePanel,

View file

@ -1,17 +1,8 @@
import {
createAnonymousBootstrap,
getAccountAPIBasePath,
getBootstrap,
getBootstrapPath,
getCommercialAPIBaseURL,
getLogoutPath,
getMagicLinkRequestPath,
getPortalAPIBasePath,
getPortalPath,
getSignupPath,
notifyPortalRender,
setBootstrap,
bootstrapDefaults,
portalStore,
} from './runtime';
import { createAnonymousBootstrap } from './store';
import { installAccountController } from './account_controller';
import { installAuthController } from './auth_controller';
import {
@ -19,28 +10,18 @@ import {
renderHeaderHTML,
renderSignedOutPortalHTML,
} from './shell_view';
import type { PortalBootstrapData } from './types';
var portalBootstrap: PortalBootstrapData = getBootstrap();
var LICENSE_API_BASE = getCommercialAPIBaseURL();
var PORTAL_PATH = getPortalPath();
var BOOTSTRAP_PATH = getBootstrapPath();
var MAGIC_LINK_REQUEST_PATH = getMagicLinkRequestPath();
var SIGNUP_PATH = getSignupPath();
var LOGOUT_PATH = getLogoutPath();
var ACCOUNT_API_BASE_PATH = getAccountAPIBasePath();
var PORTAL_API_BASE_PATH = getPortalAPIBasePath();
type ToastElement = HTMLElement & { _timer?: ReturnType<typeof setTimeout> };
function renderHeader() {
var userInfo = document.getElementById('portal-user-info');
if (!userInfo) return;
var portalBootstrap = portalStore.getBootstrap();
userInfo.innerHTML = renderHeaderHTML({
bootstrap: portalBootstrap,
loginState: authController.getLoginState(),
signupPath: SIGNUP_PATH,
accountAPIBasePath: ACCOUNT_API_BASE_PATH,
signupPath: portalBootstrap.signup_path,
accountAPIBasePath: portalBootstrap.account_api_base_path,
});
}
@ -48,42 +29,34 @@ function renderPortalApp() {
renderHeader();
var root = document.getElementById('portal-app-root');
if (!root) return;
var portalBootstrap = portalStore.getBootstrap();
var context = {
bootstrap: portalBootstrap,
loginState: authController.getLoginState(),
signupPath: SIGNUP_PATH,
accountAPIBasePath: ACCOUNT_API_BASE_PATH,
signupPath: portalBootstrap.signup_path,
accountAPIBasePath: portalBootstrap.account_api_base_path,
};
root.innerHTML = portalBootstrap.authenticated
? renderAuthenticatedPortalHTML(context)
: renderSignedOutPortalHTML(context);
notifyPortalRender();
}
function applyBootstrap(data) {
portalBootstrap = setBootstrap(data || createAnonymousBootstrap());
LICENSE_API_BASE = getCommercialAPIBaseURL();
PORTAL_PATH = getPortalPath();
BOOTSTRAP_PATH = getBootstrapPath();
MAGIC_LINK_REQUEST_PATH = getMagicLinkRequestPath();
SIGNUP_PATH = getSignupPath();
LOGOUT_PATH = getLogoutPath();
ACCOUNT_API_BASE_PATH = getAccountAPIBasePath();
PORTAL_API_BASE_PATH = getPortalAPIBasePath();
var portalBootstrap = portalStore.setBootstrap(data || createAnonymousBootstrap(bootstrapDefaults));
if (!portalBootstrap.authenticated) {
authController.syncBootstrapEmail(portalBootstrap.email || '');
}
renderPortalApp();
}
async function refreshBootstrap() {
if (!BOOTSTRAP_PATH) return false;
var bootstrap = portalStore.getBootstrap();
if (!bootstrap.bootstrap_path) return false;
try {
var response = await fetch(BOOTSTRAP_PATH, {
var response = await fetch(bootstrap.bootstrap_path, {
headers: { 'Accept': 'application/json' }
});
if (response.status === 401) {
applyBootstrap(createAnonymousBootstrap());
applyBootstrap(createAnonymousBootstrap(bootstrapDefaults));
return true;
}
if (!response.ok) return false;
@ -105,32 +78,36 @@ function showToast(msg, isError = false) {
var authController = installAuthController({
getMagicLinkRequestPath: function() {
return MAGIC_LINK_REQUEST_PATH;
return portalStore.getBootstrap().magic_link_request_path;
},
getLogoutPath: function() {
return LOGOUT_PATH;
return portalStore.getBootstrap().logout_path;
},
getPortalPath: function() {
return PORTAL_PATH;
return portalStore.getBootstrap().portal_path;
},
renderPortal: renderPortalApp,
});
installAccountController({
getAccountAPIBasePath: function() {
return ACCOUNT_API_BASE_PATH;
return portalStore.getBootstrap().account_api_base_path;
},
getPortalAPIBasePath: function() {
return PORTAL_API_BASE_PATH;
return portalStore.getBootstrap().portal_api_base_path;
},
getPortalPath: function() {
return PORTAL_PATH;
return portalStore.getBootstrap().portal_path;
},
refreshBootstrap: refreshBootstrap,
showToast: showToast
});
applyBootstrap(portalBootstrap);
if (portalBootstrap.authenticated) {
portalStore.subscribe(function() {
renderPortalApp();
});
applyBootstrap(portalStore.getBootstrap());
if (portalStore.getBootstrap().authenticated) {
refreshBootstrap();
}

View file

@ -0,0 +1,77 @@
import { describe, expect, it, vi } from 'vitest';
import { createAnonymousBootstrap, createPortalStore, normalizeBootstrap } from './store';
import type { PortalBootstrapData } from './types';
const bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | 'accounts'> = {
public_site_url: 'https://pulserelay.pro',
support_email: 'support@pulserelay.pro',
commercial_api_base_url: 'https://license.pulserelay.pro',
portal_path: '/portal',
bootstrap_path: '/api/portal/bootstrap',
magic_link_request_path: '/api/public/magic-link/request',
signup_path: '/signup',
logout_path: '/auth/logout',
account_api_base_path: '/api/accounts',
portal_api_base_path: '/api/portal',
};
describe('portal store', function() {
it('normalizes anonymous bootstrap state from defaults', function() {
var bootstrap = createAnonymousBootstrap(bootstrapDefaults, {
signup_path: '/join',
});
expect(bootstrap.authenticated).toBe(false);
expect(bootstrap.email).toBe('');
expect(bootstrap.signup_path).toBe('/join');
expect(bootstrap.accounts).toEqual([]);
});
it('normalizes partial bootstrap payloads into tracked account state', function() {
var bootstrap = normalizeBootstrap(bootstrapDefaults, {
authenticated: true,
email: 'owner@example.com',
accounts: [
{
id: 'acct_1',
name: 'Acme MSP',
kind: 'msp',
kind_label: 'MSP',
role: 'owner',
can_manage: true,
has_billing: true,
workspaces: [],
},
],
});
expect(bootstrap.authenticated).toBe(true);
expect(bootstrap.email).toBe('owner@example.com');
expect(bootstrap.accounts).toHaveLength(1);
expect(bootstrap.portal_path).toBe('/portal');
});
it('publishes bootstrap changes through a subscription boundary', function() {
var store = createPortalStore(bootstrapDefaults, null);
var listener = vi.fn();
var unsubscribe = store.subscribe(listener);
store.setBootstrap({
authenticated: true,
email: 'owner@example.com',
});
expect(listener).toHaveBeenCalledTimes(1);
expect(store.getBootstrap().authenticated).toBe(true);
expect(store.getBootstrap().email).toBe('owner@example.com');
unsubscribe();
store.setBootstrap({
authenticated: false,
email: '',
});
expect(listener).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,58 @@
import type { PortalBootstrapData } from './types';
export interface PortalStore {
getBootstrap(): PortalBootstrapData;
setBootstrap(nextBootstrap: Partial<PortalBootstrapData> | PortalBootstrapData): PortalBootstrapData;
subscribe(listener: () => void): () => void;
}
export function createAnonymousBootstrap(
bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | 'accounts'>,
overrides: Partial<PortalBootstrapData> = {}
): PortalBootstrapData {
return {
authenticated: false,
email: '',
...bootstrapDefaults,
...overrides,
accounts: normalizeAccounts(overrides.accounts),
};
}
export function normalizeBootstrap(
bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | 'accounts'>,
raw: Partial<PortalBootstrapData> | null | undefined
): PortalBootstrapData {
return createAnonymousBootstrap(bootstrapDefaults, raw || {});
}
export function createPortalStore(
bootstrapDefaults: Omit<PortalBootstrapData, 'authenticated' | 'email' | 'accounts'>,
initialBootstrap: Partial<PortalBootstrapData> | null | undefined
): PortalStore {
var bootstrapState = normalizeBootstrap(bootstrapDefaults, initialBootstrap);
var subscribers = new Set<() => void>();
return {
getBootstrap: function() {
return bootstrapState;
},
setBootstrap: function(nextBootstrap) {
bootstrapState = normalizeBootstrap(bootstrapDefaults, nextBootstrap);
subscribers.forEach(function(listener) {
listener();
});
return bootstrapState;
},
subscribe: function(listener) {
subscribers.add(listener);
return function() {
subscribers.delete(listener);
};
},
};
}
function normalizeAccounts(accounts: Partial<PortalBootstrapData>['accounts']): PortalBootstrapData['accounts'] {
return Array.isArray(accounts) ? accounts : [];
}