Initial commit: bface library, build fixes, and refreshed docs

- Externalize all @tamagui/* and tamagui subpaths so dist no longer vendors Tamagui.
- Emit TypeScript declarations with vite-plugin-dts; fix package exports types for ui/*.
- Align initEnv with profiles: displayName, brandLogo, api.baseURL, themeColor, uiShell.
- Stabilize tests with Node localStorage file; env tests pass.
- Update README and component docs for services, menus, API client, and development.
This commit is contained in:
Amer Agovic
2026-04-18 10:43:52 -05:00
commit 94a9f32969
87 changed files with 19750 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
import { useSyncExternalStore } from 'react';
const accountTabs = [];
const listeners = new Set();
let cachedTabs = [];
function rebuildSnapshot() {
cachedTabs = [...accountTabs].sort((a, b) => (a.order || 0) - (b.order || 0));
}
function emit() {
listeners.forEach((listener) => {
try {
listener();
} catch (error) {
console.warn('[Security] Account tab listener failed:', error);
}
});
}
export function publishAccountTab(tab) {
if (!tab || !tab.id || !tab.label || !tab.component) {
console.warn('[Security] publishAccountTab() requires id, label, and component');
return;
}
const existingIndex = accountTabs.findIndex((item) => item.id === tab.id);
if (existingIndex >= 0) {
accountTabs[existingIndex] = tab;
} else {
accountTabs.push(tab);
}
rebuildSnapshot();
emit();
}
export function retractAccountTab(tabId) {
const nextTabs = accountTabs.filter((tab) => tab.id !== tabId);
if (nextTabs.length !== accountTabs.length) {
accountTabs.length = 0;
accountTabs.push(...nextTabs);
rebuildSnapshot();
emit();
}
}
export function getAccountTabs() {
return cachedTabs;
}
export function subscribeToAccountTabs(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function useAccountTabs() {
return useSyncExternalStore(subscribeToAccountTabs, getAccountTabs, getAccountTabs);
}
+12
View File
@@ -0,0 +1,12 @@
export function createSecurityRequestInterceptor(securityService) {
return (config = {}) => securityService.injectRequestConfig(config);
}
export function createSecurityResponseInterceptor(securityService) {
return (response) => {
if (response && response.status === 401) {
securityService.handleUnauthorizedResponse();
}
return response;
};
}
+42
View File
@@ -0,0 +1,42 @@
import { normalizeRightsInput } from '../model/rights.js';
export async function evaluateRouteAccess(route = {}, securityService) {
const securityState = securityService.getState();
const options = route?.options || {};
const resourcePath = options.resource_path || route?.path || '/';
const requestedRights = normalizeRightsInput(options.required_rights || 0);
if (!securityState.enabled) {
return {
allowed: true,
requires_login: false,
reason: 'Security disabled'
};
}
if (!options.require_user && requestedRights === 0) {
return {
allowed: true,
requires_login: false,
reason: 'Route has no security requirements'
};
}
if (options.require_user && !securityState.isAuthenticated) {
return {
allowed: false,
requires_login: true,
reason: 'Login required for route'
};
}
if (requestedRights !== 0) {
return securityService.userPermitted(requestedRights, resourcePath, { redirectOnFail: false });
}
return {
allowed: true,
requires_login: false,
reason: 'Authenticated route'
};
}
+371
View File
@@ -0,0 +1,371 @@
import { useSyncExternalStore } from 'react';
import { setRouterPath } from '../../platform/compat.js';
import { normalizeRightsInput } from '../model/rights.js';
import { BasicSecurityPolicy, BstoreSecurityPolicy } from '../policy/index.js';
import { createSecurityRequestInterceptor, createSecurityResponseInterceptor } from './api-auth.js';
const DEFAULT_SECURITY_CONFIG = {
enabled: false,
provider: 'basic',
require_login: false,
login_route: '/login',
logout_route: '/login',
default_realm: 'local',
debug_errors: false
};
function normalizeSecurityConfig(config = {}) {
return {
...DEFAULT_SECURITY_CONFIG,
...(config || {})
};
}
function createInitialState() {
return {
initialized: false,
loading: false,
enabled: false,
provider: 'basic',
requireLogin: false,
config: normalizeSecurityConfig(),
policy: null,
session: null,
user: null,
profile: null,
realm: null,
isAuthenticated: false,
error: null
};
}
class SecurityService {
constructor() {
this.state = createInitialState();
this.listeners = new Set();
this.apiHooksInstalled = false;
}
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
emit() {
this.listeners.forEach((listener) => {
try {
listener();
} catch (error) {
console.warn('[Security] Subscriber failed:', error);
}
});
}
getSnapshot = () => this.state;
getState() { return this.state; }
setState(patch) {
this.state = {
...this.state,
...patch
};
this.emit();
}
_resolvePolicy(config) {
if (config.provider === 'bstore') {
return new BstoreSecurityPolicy(config);
}
return new BasicSecurityPolicy(config);
}
async init(config = {}) {
const normalizedConfig = normalizeSecurityConfig(config);
if (!normalizedConfig.enabled) {
this.setState({
...createInitialState(),
initialized: true,
config: normalizedConfig,
enabled: false,
provider: normalizedConfig.provider,
requireLogin: normalizedConfig.require_login
});
return this.state;
}
this.setState({
loading: true,
error: null,
enabled: true,
provider: normalizedConfig.provider,
requireLogin: normalizedConfig.require_login,
config: normalizedConfig
});
try {
const policy = this._resolvePolicy(normalizedConfig);
await policy.init();
const session = await policy.getCurrentSession();
const user = session?.user_id ? await policy.getUser(session.user_id) : null;
const realm = user?.realm_id ? await policy.getRealm(user.realm_id) : null;
const profile = user ? await policy.getAccountProfile(user.id) : null;
this.setState({
initialized: true,
loading: false,
enabled: true,
provider: normalizedConfig.provider,
requireLogin: normalizedConfig.require_login,
config: normalizedConfig,
policy,
session,
user,
profile,
realm,
isAuthenticated: Boolean(session && user),
error: null
});
} catch (error) {
console.error('[Security] Failed to initialize security service:', error);
this.setState({
initialized: true,
loading: false,
enabled: normalizedConfig.enabled,
provider: normalizedConfig.provider,
requireLogin: normalizedConfig.require_login,
config: normalizedConfig,
policy: null,
session: null,
user: null,
profile: null,
realm: null,
isAuthenticated: false,
error
});
}
return this.state;
}
installAPIClient(apiClient) {
if (!apiClient || this.apiHooksInstalled) {
return;
}
apiClient.addRequestInterceptor(createSecurityRequestInterceptor(this));
apiClient.addResponseInterceptor(createSecurityResponseInterceptor(this));
this.apiHooksInstalled = true;
}
injectRequestConfig(config = {}) {
if (!this.state.session?.jwt_token) {
return config;
}
return {
...config,
headers: {
...(config.headers || {}),
Authorization: `Bearer ${this.state.session.jwt_token}`
}
};
}
async injectAuthHeaders(headers = {}) {
return this.injectRequestConfig({ headers }).headers;
}
async handleUnauthorizedResponse() {
if (this.state.isAuthenticated) {
await this.logout({ redirect: true });
}
}
async login(credentials = {}) {
if (!this.state.policy) {
throw new Error('Security policy is not initialized');
}
this.setState({ loading: true, error: null });
try {
const result = await this.state.policy.authenticate(credentials);
const realm = result.user?.realm_id ? await this.state.policy.getRealm(result.user.realm_id) : null;
this.setState({
loading: false,
session: result.session,
user: result.user,
profile: result.profile || null,
realm,
isAuthenticated: true,
error: null
});
return result;
} catch (error) {
this.setState({
loading: false,
error
});
throw error;
}
}
async logout(options = {}) {
const { redirect = true } = options;
if (this.state.policy && this.state.session) {
try {
await this.state.policy.logout(this.state.session);
} catch (error) {
console.warn('[Security] Policy logout failed:', error);
}
}
if (this.state.policy) {
await this.state.policy.clearSession();
}
this.setState({
session: null,
user: null,
profile: null,
realm: null,
isAuthenticated: false,
error: null
});
if (redirect) {
await setRouterPath(this.state.config.logout_route || this.state.config.login_route || '/login', true);
}
}
async refreshSession() {
if (!this.state.policy) {
return this.state;
}
const session = await this.state.policy.getCurrentSession();
const user = session?.user_id ? await this.state.policy.getUser(session.user_id) : null;
const realm = user?.realm_id ? await this.state.policy.getRealm(user.realm_id) : null;
const profile = user ? await this.state.policy.getAccountProfile(user.id) : null;
this.setState({
session,
user,
profile,
realm,
isAuthenticated: Boolean(session && user)
});
return this.state;
}
async registerResource(resource) {
if (!this.state.policy) {
return null;
}
return this.state.policy.registerResource(resource);
}
async userRequired(options = {}) {
if (!this.state.enabled) {
return { allowed: true, requires_login: false, reason: 'Security disabled' };
}
if (this.state.isAuthenticated) {
return { allowed: true, requires_login: false, reason: 'User authenticated' };
}
if (options.redirect !== false) {
await setRouterPath(this.state.config.login_route || '/login', true);
}
return { allowed: false, requires_login: true, reason: 'User login required' };
}
async userPermitted(rights, resourcePath, options = {}) {
if (!this.state.enabled || !this.state.policy) {
return { allowed: true, requires_login: false, reason: 'Security disabled', matched_permits: [] };
}
if (!this.state.isAuthenticated) {
const response = { allowed: false, requires_login: true, reason: 'User login required', matched_permits: [] };
if (options.redirectOnFail) {
await setRouterPath(this.state.config.login_route || '/login', true);
}
return response;
}
const result = await this.state.policy.evaluate(
this.state.user.id,
normalizeRightsInput(rights),
resourcePath,
options
);
if (!result.allowed && result.requires_login && options.redirectOnFail) {
await setRouterPath(this.state.config.login_route || '/login', true);
}
return result;
}
isPermitted(rights, resourcePath, options = {}) {
if (!this.state.enabled || !this.state.policy) {
return true;
}
if (!this.state.isAuthenticated || !this.state.user?.id) {
return false;
}
if (typeof this.state.policy.evaluateSync !== 'function') {
return false;
}
const result = this.state.policy.evaluateSync(
this.state.user.id,
normalizeRightsInput(rights),
resourcePath,
options
);
return result?.allowed === true;
}
async updateAccountProfile(patch) {
if (!this.state.policy || !this.state.user) {
throw new Error('No authenticated user to update');
}
const profile = await this.state.policy.updateAccountProfile(this.state.user.id, patch);
const user = await this.state.policy.getUser(this.state.user.id);
this.setState({
profile,
user
});
return profile;
}
async changePassword(passwordInput) {
if (!this.state.policy || !this.state.user) {
throw new Error('No authenticated user to update');
}
await this.state.policy.changePassword(this.state.user.id, passwordInput);
}
async listUsers() { return this.state.policy ? this.state.policy.listUsers() : []; }
async listRoles() { return this.state.policy ? this.state.policy.listRoles() : []; }
async listRealms() { return this.state.policy ? this.state.policy.listRealms() : []; }
async listResources() { return this.state.policy ? this.state.policy.listResources() : []; }
async listPermits() { return this.state.policy ? this.state.policy.listPermits() : []; }
}
export const securityService = new SecurityService();
export function useSecurityState() {
return useSyncExternalStore(
(listener) => securityService.subscribe(listener),
securityService.getSnapshot,
securityService.getSnapshot
);
}
export { normalizeSecurityConfig, DEFAULT_SECURITY_CONFIG };