/** * @module context-mode-service * * Implements the WebContextModeService for @ibgib/web-gib. * Manages the active runtime ModeExecutionContext and handles discrete, fail-fast * transitions between: * - Mode 1: Ephemeral Playground Sandbox ('playground') * - Mode 2: Locally Installed Sovereign Workspace ('local') * - Mode 3: Remote Server Browsing ('remote') */ import { delay, extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { ContextMode, ContextModeCapabilities, ModeExecutionContext, ContextModeInitOptions, CONTEXT_MODE_PLAYGROUND, CONTEXT_MODE_LOCAL, CONTEXT_MODE_REMOTE, PLAYGROUND_TENANT_HANDLE, PLAYGROUND_AUTHOR_HANDLE, CONTEXT_MODE_CAPABILITIES, } from "@ibgib/core-gib/dist/context/context-mode-types.mjs"; import { MetaspaceService } from "@ibgib/core-gib/dist/witness/space/metaspace/metaspace-types.mjs"; import { IbGibSpaceAny } from "@ibgib/core-gib/dist/witness/space/space-base-v1.mjs"; import { createInMemoryTempMetaspaceAndSpace } from "@ibgib/core-gib/dist/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace.mjs"; import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs"; import { POOL_ID_DEFAULT, POOL_ID_MANAGE, KEYSTONE_VERB_AUTHOR, KEYSTONE_VERB_SYNC } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs"; import { createStandardPoolConfig, createManagePoolConfig } from "@ibgib/core-gib/dist/keystone/keystone-config-builder.mjs"; import { getTjpAddr } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs"; import { KeystoneIbGib_V1 } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs"; import { createSovereignDeviceAuthorDelegate } from "../identity/device-delegate-helper.mjs"; import { EVENT_IBGIB_CONTEXT_MODE_CHANGED } from "../ui/ui-constants.mjs"; import { getIbGibGlobalThis_IbGib } from "../helpers.mjs"; const lc = `[ContextModeService]`; /** * Device-scoped identity info (resolved by the identity header). */ export interface PersistentIdentityInfo { domainIdentity: KeystoneIbGib_V1; syncDelegateIdentity?: KeystoneIbGib_V1; tjpAddr: string; username?: string; siteUsername?: string; aggregateDetails?: { username: string, description: string, isPrimary: boolean }; authenticated: boolean; resolved: boolean; } export class WebContextModeService { private _activeContext: ModeExecutionContext | null = null; private _playgroundContext: ModeExecutionContext | null = null; private _localUserContext: ModeExecutionContext | null = null; private _persistentLocalMetaspace: MetaspaceService | null = null; private _persistentIdentity: PersistentIdentityInfo | undefined = undefined; /** * Registers (or clears, with `undefined`) the device-scoped identity. * Driven by the identity header. Identity is mode-agnostic: it pairs with * the persistent metaspace, not with the active execution context. */ public registerPersistentIdentity(identity: PersistentIdentityInfo | undefined): void { this._persistentIdentity = identity; } /** * Returns the device-scoped identity, or `undefined` if the user is not * authenticated on this device (a valid state, not an error). */ public getPersistentIdentity(): PersistentIdentityInfo | undefined { return this._persistentIdentity; } /** * Registers the persistent Metaspace created during app bootstrap (e.g. Metaspace_Webspace) * to ensure Mode 1 in-memory sandbox never clobbers Mode 2 local storage. */ public registerPersistentMetaspace(metaspace: MetaspaceService): void { this._persistentLocalMetaspace = metaspace; } /** * Returns the persistent device Metaspace (e.g. Metaspace_Webspace) registered * during app bootstrap, independent of the currently active context mode. * * Device-scoped concerns (e.g. Keystone identities, the identity header) MUST use * this rather than the active metaspace, since Mode 1 swaps in an ephemeral * in-memory metaspace. * * @throws if bootstrap has not yet registered the persistent metaspace (fail fast). */ public getPersistentMetaspace(): MetaspaceService { if (!this._persistentLocalMetaspace) { throw new Error(`${lc} Persistent local Metaspace not registered. registerPersistentMetaspace must be called during bootstrap before device-scoped consumers run. (E: 43ccf8a4b61d0fe728c1987563234826)`); } return this._persistentLocalMetaspace; } /** * Returns the persistent device Metaspace, waiting if bootstrap registration * is still completing. */ public async getPersistentMetaspace_waitIfNeeded({ delayIntervalMs = 50, maxWaitMs = 15000, } = {}): Promise { if (this._persistentLocalMetaspace) { return this._persistentLocalMetaspace; } const start = Date.now(); while (!this._persistentLocalMetaspace) { if (Date.now() - start > maxWaitMs) { throw new Error(`${lc} Timed out waiting for persistent local Metaspace registration (E: genuuid)`); } await delay(delayIntervalMs); } return this._persistentLocalMetaspace; } /** * Returns the ephemeral in-memory @playground metaspace regardless of the * currently active mode. * * @throws if the playground context has not been initialized (fail fast). */ public getPlaygroundMetaspace(): MetaspaceService { if (!this._playgroundContext) { throw new Error(`${lc} Playground context not initialized. activateMode('playground') must run first. (E: genuuid)`); } return this._playgroundContext.metaspace; } /** * Gets the currently active execution context. * Fails fast if no mode has been activated yet. */ get current(): ModeExecutionContext { if (!this._activeContext) { throw new Error(`${lc} No active execution context initialized. Call activateMode() first. (E: e53c36307858a2c2eea8547d5650c426)`); } return this._activeContext; } /** * Returns the active mode ('playground' | 'local' | 'remote') or undefined if uninitialized. */ get activeMode(): ContextMode | undefined { return this._activeContext?.mode; } /** * Checks if an execution context has already been activated. */ get hasActiveContext(): boolean { return !!this._activeContext; } /** * Helper to retrieve the active Metaspace instance. * Used by getGlobalMetaspace_waitIfNeeded() for transparent delegation. */ async getActiveMetaspace(): Promise { if (this._activeContext) { return this._activeContext.metaspace; } const globalIbGib = getIbGibGlobalThis_IbGib(); if (globalIbGib.metaspace) { return globalIbGib.metaspace; } // If not explicitly activated yet, default to Mode 1 Playground for safe, fail-fast cold landing const ctx = await this.activateMode(CONTEXT_MODE_PLAYGROUND); return ctx.metaspace; } /** * Activates a specific context mode, booting or restoring its backing space and identity context. */ async activateMode(mode: ContextMode, opts?: ContextModeInitOptions): Promise { if (this._activeContext && this._activeContext.mode === mode) { const currentTenant = this._activeContext.tenantHandle; const requestedTenant = opts?.tenantHandle; if (!requestedTenant || requestedTenant === currentTenant) { return this._activeContext; } } console.log(`${lc} Activating context mode: [${mode}]...`); let context: ModeExecutionContext; switch (mode) { case CONTEXT_MODE_PLAYGROUND: context = await this.initMode1PlaygroundContext(opts); break; case CONTEXT_MODE_LOCAL: context = await this.initMode2LocalUserContext(opts); break; case CONTEXT_MODE_REMOTE: throw new Error(`${lc} Mode 3 (Remote Server Browsing) is deferred to Phase 2. (E: 5e81088ad5e23c4b888255484500f426)`); default: throw new Error(`${lc} Unknown context mode: [${mode}]. (E: b48058281518e622b4dd6af8e84d6826)`); } const previousMode = this._activeContext?.mode; this._activeContext = context; // Sync globalThis.ibgib.metaspace so legacy consumers receive the active metaspace const globalIbGib = getIbGibGlobalThis_IbGib(); globalIbGib.metaspace = context.metaspace; // Dispatch mode change event if running in browser DOM if (typeof window !== 'undefined' && previousMode !== mode) { window.dispatchEvent(new CustomEvent(EVENT_IBGIB_CONTEXT_MODE_CHANGED, { detail: { mode: context.mode, previousMode, context: this.current, }, bubbles: true, composed: true, })); } console.log(`${lc} Successfully activated mode: [${mode}] with author: [${context.authorHandle}]`); return context; } /** * Initializes or restores Mode 1: Ephemeral Playground Sandbox. * Backed 100% by in-memory InnerSpace_V1 with an ephemeral sovereign keystone. * Invariant: Never contacts server, never writes to persistent disk. */ private async initMode1PlaygroundContext(opts?: ContextModeInitOptions): Promise { if (this._playgroundContext) { return this._playgroundContext; } console.log(`${lc}[initMode1PlaygroundContext] Creating in-memory temporary Metaspace & InnerSpace...`); const { metaspace, space } = await createInMemoryTempMetaspaceAndSpace(); // Generate ephemeral sovereign sandbox keystone strictly within RAM console.log(`${lc}[initMode1PlaygroundContext] Generating ephemeral sandbox keystone (@playground) in RAM...`); const keystoneService = new KeystoneService_V1(); const masterSecret = `playground_secret_${Math.random().toString(36).substring(2)}`; const salt = `playground_salt_${Math.random().toString(36).substring(2)}`; const genesisKeystone = await keystoneService.genesis({ masterSecret, configs: [ createStandardPoolConfig({ id: POOL_ID_DEFAULT, salt }), createManagePoolConfig({ id: POOL_ID_MANAGE, salt: `${salt}_manage` }), ], frameDetails: { username: 'playground', isPrimary: true }, metaspace, space, }); const primaryTjpAddr = getTjpAddr({ ibGib: genesisKeystone })!; const { delegateKeystone: authorDelegate } = await createSovereignDeviceAuthorDelegate({ username: 'playground', primaryTjpAddr, masterSecret, metaspace, space, persistToStorage: false, }); const evolvedKeystone = await keystoneService.registerDelegate({ parentKeystone: genesisKeystone, delegateKeystone: authorDelegate, masterSecret, signingPoolId: POOL_ID_MANAGE, metaspace, space, allowedVerbs: [KEYSTONE_VERB_AUTHOR, KEYSTONE_VERB_SYNC], deviceName: 'playground-device', }); this._playgroundContext = { mode: CONTEXT_MODE_PLAYGROUND, tenantHandle: PLAYGROUND_TENANT_HANDLE, metaspace, rootSpace: space, identity: evolvedKeystone, delegateIdentity: authorDelegate, authorHandle: PLAYGROUND_AUTHOR_HANDLE, capabilities: CONTEXT_MODE_CAPABILITIES[CONTEXT_MODE_PLAYGROUND], }; return this._playgroundContext; } /** * Initializes or restores Mode 2: Locally Installed Sovereign Workspace. * Backed by persistent WebFilesystemSpace_V1 (OPFS/IDB) and user's DomainKeystone. */ private async initMode2LocalUserContext(opts?: ContextModeInitOptions): Promise { // Retrieve persistent metaspace initialized by bootstrap const metaspace = this._persistentLocalMetaspace; if (!metaspace) { throw new Error(`metaspace falsy? bootstrap should have initialized this already. (E: 000fc7c9d5080443f8522a08c931d826)`); } const userSpace = await metaspace.getLocalUserSpace({ lock: false }); if (!userSpace) { throw new Error(`${lc}[initMode2LocalUserContext] Could not resolve local user space. (E: c907c8c029f8581948a63983e32e9826)`); } // Resolve active Keystone identity from global state or persistent special index const globalIdentity = this._persistentIdentity; const domainIdentity = globalIdentity?.domainIdentity; const delegateIdentity = globalIdentity?.syncDelegateIdentity; let authorHandle = '@unauthenticated'; let tenantHandle = opts?.tenantHandle || '@local'; if (domainIdentity) { const rawHandle = globalIdentity?.siteUsername || globalIdentity?.username || globalIdentity?.aggregateDetails?.username; if (rawHandle) { authorHandle = rawHandle.startsWith('@') ? rawHandle : `@${rawHandle}`; tenantHandle = authorHandle; } } this._localUserContext = { mode: CONTEXT_MODE_LOCAL, tenantHandle, metaspace, rootSpace: userSpace, identity: domainIdentity || null, delegateIdentity: delegateIdentity || null, authorHandle, capabilities: CONTEXT_MODE_CAPABILITIES[CONTEXT_MODE_LOCAL], }; return this._localUserContext; } } let globalContextModeService: WebContextModeService | undefined; /** * Singleton accessor for the WebContextModeService. */ export function getContextModeService(): WebContextModeService { if (!globalContextModeService) { globalContextModeService = new WebContextModeService(); } return globalContextModeService; }