@kennofizet/apphub-frontend 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kennofizet/apphub-frontend",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "App Hub Vue 3 UI — Windows-style desktop shell and modular apps (App Store default).",
5
5
  "main": "./src/index.js",
6
6
  "module": "./src/index.js",
@@ -1,11 +1,18 @@
1
- import { getCurrentInstance } from 'vue'
2
- import { getAppHubStore } from '../moduleStore.js'
1
+ import { getCurrentInstance, inject } from 'vue'
2
+ import { APPHUB_MODULE_STORE_KEY, getAppHubStore } from '../moduleStore.js'
3
3
 
4
4
  export function resolveRootApp(instance = getCurrentInstance()) {
5
5
  if (!instance) return null
6
6
  return instance.appContext?.app ?? instance.app ?? null
7
7
  }
8
8
 
9
+ /** Resolve module store — inject first (always same instance), then WeakMap by Vue app. */
10
+ export function useAppHubModuleStore() {
11
+ const fromInject = inject(APPHUB_MODULE_STORE_KEY, null)
12
+ if (fromInject) return fromInject
13
+ return getAppHubStore(resolveRootApp())
14
+ }
15
+
9
16
  export function getHostApiForApp(app) {
10
17
  return getAppHubStore(app)?.facade ?? null
11
18
  }
@@ -20,5 +27,9 @@ export function isBackendReadyForApp(app) {
20
27
  * so publisher app code cannot access grantBridgeScope or internal docs.
21
28
  */
22
29
  export function useAppHubHostApi() {
23
- return getHostApiForApp(resolveRootApp())
30
+ return useAppHubModuleStore()?.facade ?? null
31
+ }
32
+
33
+ export function isBackendReadyFromStore(store) {
34
+ return !!(store?.credentials?.backendUrl && store?.credentials?.token)
24
35
  }
package/src/index.js CHANGED
@@ -1,507 +1,449 @@
1
- import './modules/desktop/styles/desktop.css'
2
- import './modules/desktop/styles/theme.css'
3
- import { reactive } from 'vue'
4
- import { createAppHubApi } from './api/index.js'
5
- import { createCoreApi } from './api/coreApi.js'
6
- import { createZoneContextState } from './composables/createZoneContext.js'
7
- import { APPHUB_ZONE_CONTEXT_KEY } from './composables/useAppHubZoneContext.js'
8
- import { createAppStoreState, provideAppStore } from './modules/app-store/index.js'
9
- import { AppHubDesktop } from './modules/desktop/index.js'
10
- import { AppHubRunner } from './modules/runner/index.js'
11
- import { getAppHubStore, registerAppHubStore } from './moduleStore.js'
12
- import {
13
- createWindowManagerState,
14
- provideWindowManager,
15
- } from './modules/window-manager/index.js'
16
- import {
17
- evaluateOriginSafety,
18
- parseDevUserFromBootstrap,
19
- parseOriginsFromBootstrap,
20
- } from './utils/originSafety.js'
21
- import { loadDevFriendlyOriginsPreference } from './utils/devOriginSettings.js'
22
- import {
23
- bootstrapResponseFromCache,
24
- loadBootstrapCache,
25
- saveBootstrapCache,
26
- } from './utils/bootstrapCache.js'
27
- import {
28
- apphubDebug,
29
- describeHostApi,
30
- describeStore,
31
- } from './utils/apphubDebug.js'
32
-
33
- const bootstrapInflight = new WeakMap()
34
-
35
- function buildPublicOptions(options = {}) {
36
- const origins = Array.isArray(options.allowedRuntimeOrigins)
37
- ? options.allowedRuntimeOrigins.filter((o) => typeof o === 'string')
38
- : []
39
-
40
- return {
41
- language: options.language || 'vi',
42
- theme: options.theme ?? 'auto',
43
- themeToggle: options.themeToggle,
44
- openAppStoreOnMount: options.openAppStoreOnMount !== false,
45
- allowedRuntimeOrigins: origins,
46
- coreUrl: options.coreUrl || '',
47
- backendUrl: (options.backendUrl || '').replace(/\/$/, ''),
48
- hasToken: !!(options.token),
49
- allowSameOriginEmbed: options.allowSameOriginEmbed === true,
50
- allowUnsafeOrigin: options.allowUnsafeOrigin === true,
51
- hubOrigin: typeof options.hubOrigin === 'string' ? options.hubOrigin.trim() : '',
52
- runtimePublicUrl: typeof options.runtimePublicUrl === 'string' ? options.runtimePublicUrl.trim() : '',
53
- enforceDedicatedHubOrigin: options.enforceDedicatedHubOrigin !== false,
54
- enforceIsolatedHostedRuntime: options.enforceIsolatedHostedRuntime !== false,
55
- allowSameOriginHostedRuntime: options.allowSameOriginHostedRuntime === true,
56
- hostedSandboxSameOrigin: options.hostedSandboxSameOrigin === true,
57
- enforceDevFriendlyOrigins: typeof options.enforceDevFriendlyOrigins === 'boolean'
58
- ? options.enforceDevFriendlyOrigins
59
- : true,
60
- isDevUser: options.isDevUser === true,
61
- serverHubPublicUrl: typeof options.serverHubPublicUrl === 'string' ? options.serverHubPublicUrl.trim() : '',
62
- serverFrontendOrigin: typeof options.serverFrontendOrigin === 'string' ? options.serverFrontendOrigin.trim() : '',
63
- serverRuntimePublicUrl: typeof options.serverRuntimePublicUrl === 'string' ? options.serverRuntimePublicUrl.trim() : '',
64
- serverOriginsAuto: options.serverOriginsAuto === true,
65
- serverOriginsResolved: options.serverOriginsResolved === true,
66
- originBootstrapLoading: options.originBootstrapLoading === true,
67
- originBlocked: false,
68
- originCheckPending: false,
69
- originBlockReason: null,
70
- originBlockParentOrigin: null,
71
- originBlockExpectedHubOrigin: null,
72
- originBlockExpectedRuntimeOrigin: null,
73
- }
74
- }
75
-
76
- /** @param {Record<string, unknown>} target */
77
- function applyOriginSafety(target, sourceOptions = {}) {
78
- const check = evaluateOriginSafety({ ...target, ...sourceOptions })
79
- target.originBootstrapLoading = check.loading === true
80
- target.originCheckPending = check.pending === true
81
- target.originBlocked = !check.safe && check.loading !== true
82
- target.originBlockReason = check.reason
83
- target.originBlockParentOrigin = check.parentOrigin
84
- target.originBlockExpectedHubOrigin = check.expectedHubOrigin
85
- target.originBlockExpectedRuntimeOrigin = check.expectedRuntimeOrigin
86
- return check
87
- }
88
-
89
- function parseUserFromBootstrap(resp) {
90
- const data = resp?.data?.data ?? resp?.data ?? {}
91
- const user = data.user
92
- if (!user || user.id == null) return null
93
- return {
94
- id: user.id,
95
- name: user.name ?? String(user.id),
96
- }
97
- }
98
-
99
- function applyBootstrapOrigins(store, bootstrapResponse, { fromCache = false } = {}) {
100
- const {
101
- hubPublicUrl,
102
- frontendOrigin,
103
- runtimePublicUrl,
104
- originsAuto,
105
- } = parseOriginsFromBootstrap(bootstrapResponse)
106
-
107
- store.options.isDevUser = parseDevUserFromBootstrap(bootstrapResponse)
108
- store.options.serverOriginsAuto = originsAuto
109
-
110
- if (!store.options.isDevUser) {
111
- store.options.enforceDevFriendlyOrigins = true
112
- } else {
113
- store.options.enforceDevFriendlyOrigins = loadDevFriendlyOriginsPreference()
114
- }
115
-
116
- if (hubPublicUrl) {
117
- store.options.serverHubPublicUrl = hubPublicUrl
118
- }
119
- if (frontendOrigin) {
120
- store.options.serverFrontendOrigin = frontendOrigin
121
- }
122
- if (runtimePublicUrl) {
123
- store.options.serverRuntimePublicUrl = runtimePublicUrl
124
- if (!store.options.runtimePublicUrl) store.options.runtimePublicUrl = runtimePublicUrl
125
- }
126
-
127
- store.options.serverOriginsResolved = true
128
-
129
- if (!fromCache) {
130
- saveBootstrapCache(store.credentials.backendUrl, bootstrapResponse)
131
- }
132
-
133
- const user = parseUserFromBootstrap(bootstrapResponse)
134
- if (user && store.zoneContext?.state) {
135
- store.zoneContext.state.user.id = user.id
136
- store.zoneContext.state.user.name = user.name
137
- }
138
-
139
- // Reconcile while originBootstrapLoading may still be true so enableModuleApi runs
140
- // after disableModuleServices (wasLoading must be captured before clearing the flag).
141
- reconcileOriginSafety(store)
142
- store.options.originBootstrapLoading = false
143
- applyOriginSafety(store.options)
144
- apphubDebug('bootstrap', 'applyBootstrapOrigins done', describeStore(store))
145
- }
146
-
147
- function applyCachedBootstrapIfAny(store) {
148
- const cached = loadBootstrapCache(store.credentials.backendUrl)
149
- if (!cached) return false
150
- applyBootstrapOrigins(store, bootstrapResponseFromCache(cached), { fromCache: true })
151
- return true
152
- }
153
-
154
- async function fetchBootstrapSession(store) {
155
- let promise = bootstrapInflight.get(store)
156
- if (promise) return promise
157
-
158
- promise = (async () => {
159
- apphubDebug('bootstrap', 'fetchBootstrapSession start', describeStore(store))
160
- syncApi(store)
161
- if (!store.facade?.bootstrap) {
162
- apphubDebug('bootstrap', 'fetchBootstrapSession abort — no bootstrap fn', describeStore(store))
163
- return
164
- }
165
-
166
- const res = await store.facade.bootstrap()
167
- apphubDebug('bootstrap', 'fetchBootstrapSession bootstrap response', {
168
- ...describeStore(store),
169
- hasResponse: !!res,
170
- })
171
- if (res) applyBootstrapOrigins(store, res)
172
- })().catch((err) => {
173
- apphubDebug('bootstrap', 'fetchBootstrapSession failed', {
174
- ...describeStore(store),
175
- error: err?.message ?? String(err),
176
- })
177
- store.options.originBootstrapLoading = false
178
- if (!store.options.serverOriginsResolved) {
179
- reconcileOriginSafety(store)
180
- }
181
- }).finally(() => {
182
- bootstrapInflight.delete(store)
183
- })
184
-
185
- bootstrapInflight.set(store, promise)
186
- return promise
187
- }
188
-
189
- function startBootstrapSession(store) {
190
- const { backendUrl, token } = store.credentials
191
- apphubDebug('bootstrap', 'startBootstrapSession', {
192
- ...describeStore(store),
193
- hasBackendUrl: !!backendUrl,
194
- hasToken: !!token,
195
- })
196
- if (!backendUrl || !token) {
197
- store.options.originBootstrapLoading = false
198
- reconcileOriginSafety(store)
199
- return Promise.resolve()
200
- }
201
-
202
- syncApi(store)
203
- applyOriginSafety(store.options)
204
-
205
- const hadCache = applyCachedBootstrapIfAny(store)
206
- apphubDebug('bootstrap', 'startBootstrapSession after cache', {
207
- hadCache,
208
- ...describeStore(store),
209
- })
210
- if (!hadCache && !store.options.originBlocked) {
211
- store.options.originBootstrapLoading = true
212
- store.options.originBlocked = false
213
- applyOriginSafety(store.options)
214
- disableModuleServices(store, 'startBootstrapSession:no-cache')
215
- }
216
-
217
- return fetchBootstrapSession(store)
218
- }
219
-
220
- function reconcileOriginSafety(store) {
221
- const wasBlocked = store.options.originBlocked === true
222
- const wasLoading = store.options.originBootstrapLoading === true
223
- applyOriginSafety(store.options)
224
-
225
- let action = 'noop'
226
- if (store.options.originBootstrapLoading) {
227
- disableModuleServices(store, 'reconcileOriginSafety:loading')
228
- action = 'disable:loading'
229
- } else if (store.options.originBlocked && !wasBlocked) {
230
- disableModuleServices(store, 'reconcileOriginSafety:blocked')
231
- action = 'disable:blocked'
232
- } else if (!store.options.originBlocked && (wasBlocked || wasLoading)) {
233
- enableModuleApi(store)
234
- action = 'enable'
235
- }
236
-
237
- apphubDebug('origin', 'reconcileOriginSafety', {
238
- wasLoading,
239
- wasBlocked,
240
- action,
241
- ...describeStore(store),
242
- })
243
- }
244
-
245
- function disableModuleServices(store, reason = 'unknown') {
246
- store.facade.setImpl(null)
247
- store.coreApi = null
248
- apphubDebug('api', 'disableModuleServices', { reason, ...describeStore(store) })
249
- }
250
-
251
- /** Window manager + app store — required even when origin is blocked (AppHubDesktop setup). */
252
- function ensureModuleInfrastructure(vueApp, store) {
253
- ensureZoneContext(vueApp, store)
254
- ensureModuleState(vueApp, store)
255
- }
256
-
257
- function enableModuleApi(store) {
258
- syncApi(store)
259
- syncZoneContext(store)
260
- apphubDebug('api', 'enableModuleApi', describeStore(store))
261
- }
262
-
263
- function enableModuleServices(vueApp, store) {
264
- ensureModuleInfrastructure(vueApp, store)
265
- enableModuleApi(store)
266
- }
267
-
268
- function buildCredentials(options = {}) {
269
- return {
270
- coreUrl: options.coreUrl || '',
271
- backendUrl: options.backendUrl || '',
272
- token: options.token || '',
273
- hostAccessSecret: options.hostAccessSecret || '',
274
- }
275
- }
276
-
277
- function createApiFacade() {
278
- let impl = null
279
- return {
280
- setImpl(next) {
281
- impl = next
282
- },
283
- hasImpl() {
284
- return impl != null
285
- },
286
- bootstrap: (...args) => impl?.bootstrap?.(...args),
287
- apps: (...args) => impl?.apps?.(...args),
288
- launch: (...args) => impl?.launch?.(...args),
289
- ping: (...args) => impl?.ping?.(...args),
290
- verifyLaunchToken: (...args) => impl?.verifyLaunchToken?.(...args),
291
- usage: (...args) => impl?.usage?.(...args),
292
- devApps: (...args) => impl?.devApps?.(...args),
293
- devInspectBundle: (...args) => impl?.devInspectBundle?.(...args),
294
- devDisableApp: (...args) => impl?.devDisableApp?.(...args),
295
- devSetAppStatus: (...args) => impl?.devSetAppStatus?.(...args),
296
- registerApp: (...args) => impl?.registerApp?.(...args),
297
- appVersions: (...args) => impl?.appVersions?.(...args),
298
- integrationDocs: (...args) => impl?.integrationDocs?.(...args),
299
- integrationDocsInternal: (...args) => impl?.integrationDocsInternal?.(...args),
300
- grantBridgeScope: (...args) => impl?.grantBridgeScope?.(...args),
301
- bridgeUser: (...args) => impl?.bridgeUser?.(...args),
302
- bridgeDesktopMessage: (...args) => impl?.bridgeDesktopMessage?.(...args),
303
- }
304
- }
305
-
306
- function credentialsUnchanged(prev, next) {
307
- return prev.backendUrl === next.backendUrl
308
- && prev.token === next.token
309
- && prev.coreUrl === next.coreUrl
310
- && prev.hostAccessSecret === next.hostAccessSecret
311
- }
312
-
313
- function applyModuleOptions(store, options = {}) {
314
- const prevCredentials = { ...store.credentials }
315
- const nextCredentials = buildCredentials(options)
316
- Object.assign(store.credentials, nextCredentials)
317
- const credsUnchanged = credentialsUnchanged(prevCredentials, nextCredentials)
318
-
319
- const nextPublic = buildPublicOptions({ ...options, token: options.token ?? store.credentials.token })
320
- Object.assign(store.options, {
321
- language: nextPublic.language,
322
- theme: nextPublic.theme,
323
- themeToggle: nextPublic.themeToggle,
324
- openAppStoreOnMount: nextPublic.openAppStoreOnMount,
325
- allowedRuntimeOrigins: nextPublic.allowedRuntimeOrigins,
326
- coreUrl: nextPublic.coreUrl,
327
- backendUrl: nextPublic.backendUrl,
328
- hasToken: nextPublic.hasToken,
329
- allowSameOriginEmbed: nextPublic.allowSameOriginEmbed,
330
- allowUnsafeOrigin: nextPublic.allowUnsafeOrigin,
331
- hubOrigin: nextPublic.hubOrigin,
332
- runtimePublicUrl: nextPublic.runtimePublicUrl,
333
- enforceDedicatedHubOrigin: nextPublic.enforceDedicatedHubOrigin,
334
- enforceIsolatedHostedRuntime: nextPublic.enforceIsolatedHostedRuntime,
335
- allowSameOriginHostedRuntime: nextPublic.allowSameOriginHostedRuntime,
336
- hostedSandboxSameOrigin: nextPublic.hostedSandboxSameOrigin,
337
- enforceDevFriendlyOrigins: nextPublic.enforceDevFriendlyOrigins,
338
- })
339
- applyOriginSafety(store.options, options)
340
- const branch = !store.credentials.backendUrl || !store.credentials.token
341
- ? 'no-credentials'
342
- : (credsUnchanged ? 'creds-unchanged' : 'start-bootstrap')
343
- apphubDebug('install', 'applyModuleOptions', {
344
- branch,
345
- credsUnchanged,
346
- ...describeStore(store),
347
- })
348
- if (store.credentials.backendUrl && store.credentials.token) {
349
- if (credsUnchanged) {
350
- reconcileOriginSafety(store)
351
- } else {
352
- void startBootstrapSession(store)
353
- }
354
- } else {
355
- reconcileOriginSafety(store)
356
- }
357
- }
358
-
359
- function syncApi(store) {
360
- const { credentials, facade, zoneContext } = store
361
- const api = credentials.backendUrl && credentials.token
362
- ? createAppHubApi(credentials.backendUrl, credentials.token, {
363
- hostAccessSecret: credentials.hostAccessSecret,
364
- getZoneHeaderId: () => zoneContext?.getZoneHeaderId?.() ?? null,
365
- })
366
- : null
367
- facade.setImpl(api)
368
- apphubDebug('api', 'syncApi', {
369
- implSet: api != null,
370
- ...describeStore(store),
371
- })
372
- }
373
-
374
- function syncCoreApi(store) {
375
- const { credentials } = store
376
- store.coreApi = credentials.coreUrl && credentials.token
377
- ? createCoreApi(credentials.coreUrl, credentials.token)
378
- : null
379
- }
380
-
381
- function ensureZoneContext(app, store) {
382
- if (store.zoneContext) return
383
- store.zoneContext = createZoneContextState(
384
- () => store.coreApi,
385
- () => store.facade,
386
- {
387
- ensureBootstrapSession: () => fetchBootstrapSession(store),
388
- },
389
- )
390
- app.provide(APPHUB_ZONE_CONTEXT_KEY, store.zoneContext)
391
- }
392
-
393
- function syncZoneContext(store) {
394
- syncCoreApi(store)
395
- if (store.zoneContext) {
396
- store.zoneContext.refresh({ skipBootstrap: true })
397
- }
398
- }
399
-
400
- function ensureModuleState(app, store) {
401
- if (store.windowManager && store.appStore) {
402
- return
403
- }
404
-
405
- store.windowManager = createWindowManagerState()
406
- store.appStore = createAppStoreState()
407
- provideWindowManager(app, store.windowManager)
408
- provideAppStore(app, store.appStore)
409
- }
410
-
411
- /**
412
- * Install App Hub Windows desktop shell + modular apps (App Store default).
413
- * Returns host API facade — keep in host app code, not publisher apps.
414
- */
415
- export function installAppHubModule(vueApp, options = {}) {
416
- let store = getAppHubStore(vueApp)
417
-
418
- if (store) {
419
- vueApp.provide('apphubHostApp', vueApp)
420
- apphubDebug('install', 'installAppHubModule re-install', {
421
- appUid: vueApp._uid ?? null,
422
- hasToken: !!(options.token),
423
- theme: options.theme,
424
- language: options.language,
425
- })
426
- applyModuleOptions(store, options)
427
- ensureModuleInfrastructure(vueApp, store)
428
- reconcileOriginSafety(store)
429
- return store.facade
430
- }
431
-
432
- apphubDebug('install', 'installAppHubModule first install', {
433
- appUid: vueApp._uid ?? null,
434
- hasToken: !!(options.token),
435
- backendUrl: options.backendUrl ? '(set)' : '(empty)',
436
- })
437
-
438
- const facade = createApiFacade()
439
- const moduleOptions = reactive(buildPublicOptions(options))
440
- applyOriginSafety(moduleOptions, options)
441
- const credentials = buildCredentials(options)
442
-
443
- store = {
444
- app: vueApp,
445
- facade,
446
- options: moduleOptions,
447
- credentials,
448
- coreApi: null,
449
- zoneContext: null,
450
- windowManager: null,
451
- appStore: null,
452
- }
453
- registerAppHubStore(vueApp, store)
454
-
455
- vueApp.provide('apphubOptions', moduleOptions)
456
- vueApp.provide('apphubHostApp', vueApp)
457
- vueApp.component('AppHubDesktop', AppHubDesktop)
458
- vueApp.component('AppHubRunner', AppHubRunner)
459
-
460
- ensureModuleInfrastructure(vueApp, store)
461
- void startBootstrapSession(store)
462
-
463
- return facade
464
- }
465
-
466
- /** Whether installAppHubModule has been called for this Vue app instance. */
467
- export function isAppHubModuleInstalled(vueApp) {
468
- return vueApp != null && getAppHubStore(vueApp) != null
469
- }
470
-
471
- export { useAppHubHostApi } from './composables/useAppHubHostApi.js'
472
- export { useAppHubZoneContext } from './composables/useAppHubZoneContext.js'
473
- export { createAppHubApi } from './api/index.js'
474
- export { createCoreApi } from './api/coreApi.js'
475
- export { AppHubDesktop } from './modules/desktop/index.js'
476
- export {
477
- createDesktopNotificationsState,
478
- useDesktopNotifications,
479
- AppHubDesktopNotifications,
480
- parseApiError,
481
- } from './modules/notifications/index.js'
482
- export { AppHubRunner } from './modules/runner/index.js'
483
- export { resolveLang } from './i18n/resolveLang.js'
484
- export { resolveTheme, normalizeTheme, isThemeLocked } from './i18n/resolveTheme.js'
485
- export { t } from './i18n/index.js'
486
- export {
487
- evaluateOriginSafety,
488
- isEmbeddedFrame,
489
- isLocalDevOrigin,
490
- parseOriginsFromBootstrap,
491
- resolveEffectiveOrigins,
492
- ORIGIN_UNSAFE_SAME_ORIGIN_EMBED,
493
- ORIGIN_UNSAFE_NOT_CONFIGURED,
494
- ORIGIN_UNSAFE_WRONG_ORIGIN,
495
- ORIGIN_UNSAFE_RUNTIME_NOT_CONFIGURED,
496
- ORIGIN_UNSAFE_RUNTIME_SAME_ORIGIN,
497
- resolveRuntimeApiBase,
498
- } from './utils/originSafety.js'
499
-
500
- /** True when installAppHubModule blocked due to unsafe same-origin embed. */
501
- export function isAppHubOriginBlocked(vueApp) {
502
- const store = getAppHubStore(vueApp)
503
- return store?.options?.originBlocked === true
504
- }
505
- export * from './modules/app-store/index.js'
506
- export * from './modules/window-manager/index.js'
507
- export * from './modules/runner/index.js'
1
+ import './modules/desktop/styles/desktop.css'
2
+ import './modules/desktop/styles/theme.css'
3
+ import { reactive } from 'vue'
4
+ import { createAppHubApi } from './api/index.js'
5
+ import { createCoreApi } from './api/coreApi.js'
6
+ import { createZoneContextState } from './composables/createZoneContext.js'
7
+ import { APPHUB_ZONE_CONTEXT_KEY } from './composables/useAppHubZoneContext.js'
8
+ import { createAppStoreState, provideAppStore } from './modules/app-store/index.js'
9
+ import { AppHubDesktop } from './modules/desktop/index.js'
10
+ import { AppHubRunner } from './modules/runner/index.js'
11
+ import { getAppHubStore, registerAppHubStore, APPHUB_MODULE_STORE_KEY } from './moduleStore.js'
12
+ import {
13
+ createWindowManagerState,
14
+ provideWindowManager,
15
+ } from './modules/window-manager/index.js'
16
+ import {
17
+ evaluateOriginSafety,
18
+ parseDevUserFromBootstrap,
19
+ parseOriginsFromBootstrap,
20
+ } from './utils/originSafety.js'
21
+ import { loadDevFriendlyOriginsPreference } from './utils/devOriginSettings.js'
22
+ import {
23
+ bootstrapResponseFromCache,
24
+ loadBootstrapCache,
25
+ saveBootstrapCache,
26
+ } from './utils/bootstrapCache.js'
27
+
28
+ const bootstrapInflight = new WeakMap()
29
+
30
+ function buildPublicOptions(options = {}) {
31
+ const origins = Array.isArray(options.allowedRuntimeOrigins)
32
+ ? options.allowedRuntimeOrigins.filter((o) => typeof o === 'string')
33
+ : []
34
+
35
+ return {
36
+ language: options.language || 'vi',
37
+ theme: options.theme ?? 'auto',
38
+ themeToggle: options.themeToggle,
39
+ openAppStoreOnMount: options.openAppStoreOnMount !== false,
40
+ allowedRuntimeOrigins: origins,
41
+ coreUrl: options.coreUrl || '',
42
+ backendUrl: (options.backendUrl || '').replace(/\/$/, ''),
43
+ hasToken: !!(options.token),
44
+ allowSameOriginEmbed: options.allowSameOriginEmbed === true,
45
+ allowUnsafeOrigin: options.allowUnsafeOrigin === true,
46
+ hubOrigin: typeof options.hubOrigin === 'string' ? options.hubOrigin.trim() : '',
47
+ runtimePublicUrl: typeof options.runtimePublicUrl === 'string' ? options.runtimePublicUrl.trim() : '',
48
+ enforceDedicatedHubOrigin: options.enforceDedicatedHubOrigin !== false,
49
+ enforceIsolatedHostedRuntime: options.enforceIsolatedHostedRuntime !== false,
50
+ allowSameOriginHostedRuntime: options.allowSameOriginHostedRuntime === true,
51
+ hostedSandboxSameOrigin: options.hostedSandboxSameOrigin === true,
52
+ enforceDevFriendlyOrigins: typeof options.enforceDevFriendlyOrigins === 'boolean'
53
+ ? options.enforceDevFriendlyOrigins
54
+ : true,
55
+ isDevUser: options.isDevUser === true,
56
+ serverHubPublicUrl: typeof options.serverHubPublicUrl === 'string' ? options.serverHubPublicUrl.trim() : '',
57
+ serverFrontendOrigin: typeof options.serverFrontendOrigin === 'string' ? options.serverFrontendOrigin.trim() : '',
58
+ serverRuntimePublicUrl: typeof options.serverRuntimePublicUrl === 'string' ? options.serverRuntimePublicUrl.trim() : '',
59
+ serverOriginsAuto: options.serverOriginsAuto === true,
60
+ serverOriginsResolved: options.serverOriginsResolved === true,
61
+ originBootstrapLoading: options.originBootstrapLoading === true,
62
+ originBlocked: false,
63
+ originCheckPending: false,
64
+ originBlockReason: null,
65
+ originBlockParentOrigin: null,
66
+ originBlockExpectedHubOrigin: null,
67
+ originBlockExpectedRuntimeOrigin: null,
68
+ }
69
+ }
70
+
71
+ /** @param {Record<string, unknown>} target */
72
+ function applyOriginSafety(target, sourceOptions = {}) {
73
+ const check = evaluateOriginSafety({ ...target, ...sourceOptions })
74
+ target.originBootstrapLoading = check.loading === true
75
+ target.originCheckPending = check.pending === true
76
+ target.originBlocked = !check.safe && check.loading !== true
77
+ target.originBlockReason = check.reason
78
+ target.originBlockParentOrigin = check.parentOrigin
79
+ target.originBlockExpectedHubOrigin = check.expectedHubOrigin
80
+ target.originBlockExpectedRuntimeOrigin = check.expectedRuntimeOrigin
81
+ return check
82
+ }
83
+
84
+ function parseUserFromBootstrap(resp) {
85
+ const data = resp?.data?.data ?? resp?.data ?? {}
86
+ const user = data.user
87
+ if (!user || user.id == null) return null
88
+ return {
89
+ id: user.id,
90
+ name: user.name ?? String(user.id),
91
+ }
92
+ }
93
+
94
+ function applyBootstrapOrigins(store, bootstrapResponse, { fromCache = false } = {}) {
95
+ const {
96
+ hubPublicUrl,
97
+ frontendOrigin,
98
+ runtimePublicUrl,
99
+ originsAuto,
100
+ } = parseOriginsFromBootstrap(bootstrapResponse)
101
+
102
+ store.options.isDevUser = parseDevUserFromBootstrap(bootstrapResponse)
103
+ store.options.serverOriginsAuto = originsAuto
104
+
105
+ if (!store.options.isDevUser) {
106
+ store.options.enforceDevFriendlyOrigins = true
107
+ } else {
108
+ store.options.enforceDevFriendlyOrigins = loadDevFriendlyOriginsPreference()
109
+ }
110
+
111
+ if (hubPublicUrl) {
112
+ store.options.serverHubPublicUrl = hubPublicUrl
113
+ }
114
+ if (frontendOrigin) {
115
+ store.options.serverFrontendOrigin = frontendOrigin
116
+ }
117
+ if (runtimePublicUrl) {
118
+ store.options.serverRuntimePublicUrl = runtimePublicUrl
119
+ if (!store.options.runtimePublicUrl) store.options.runtimePublicUrl = runtimePublicUrl
120
+ }
121
+
122
+ store.options.serverOriginsResolved = true
123
+
124
+ if (!fromCache) {
125
+ saveBootstrapCache(store.credentials.backendUrl, bootstrapResponse)
126
+ }
127
+
128
+ const user = parseUserFromBootstrap(bootstrapResponse)
129
+ if (user && store.zoneContext?.state) {
130
+ store.zoneContext.state.user.id = user.id
131
+ store.zoneContext.state.user.name = user.name
132
+ }
133
+
134
+ // Reconcile while originBootstrapLoading may still be true so enableModuleApi runs
135
+ // after disableModuleServices (wasLoading must be captured before clearing the flag).
136
+ reconcileOriginSafety(store)
137
+ store.options.originBootstrapLoading = false
138
+ applyOriginSafety(store.options)
139
+ }
140
+
141
+ function applyCachedBootstrapIfAny(store) {
142
+ const cached = loadBootstrapCache(store.credentials.backendUrl)
143
+ if (!cached) return false
144
+ applyBootstrapOrigins(store, bootstrapResponseFromCache(cached), { fromCache: true })
145
+ return true
146
+ }
147
+
148
+ async function fetchBootstrapSession(store) {
149
+ let promise = bootstrapInflight.get(store)
150
+ if (promise) return promise
151
+
152
+ promise = (async () => {
153
+ syncApi(store)
154
+ if (!store.facade?.bootstrap) return
155
+
156
+ const res = await store.facade.bootstrap()
157
+ if (res) applyBootstrapOrigins(store, res)
158
+ })().catch(() => {
159
+ store.options.originBootstrapLoading = false
160
+ if (!store.options.serverOriginsResolved) {
161
+ reconcileOriginSafety(store)
162
+ }
163
+ }).finally(() => {
164
+ bootstrapInflight.delete(store)
165
+ })
166
+
167
+ bootstrapInflight.set(store, promise)
168
+ return promise
169
+ }
170
+
171
+ function startBootstrapSession(store) {
172
+ const { backendUrl, token } = store.credentials
173
+ if (!backendUrl || !token) {
174
+ store.options.originBootstrapLoading = false
175
+ reconcileOriginSafety(store)
176
+ return Promise.resolve()
177
+ }
178
+
179
+ syncApi(store)
180
+ applyOriginSafety(store.options)
181
+
182
+ const hadCache = applyCachedBootstrapIfAny(store)
183
+ if (!hadCache && !store.options.originBlocked) {
184
+ store.options.originBootstrapLoading = true
185
+ store.options.originBlocked = false
186
+ applyOriginSafety(store.options)
187
+ disableModuleServices(store)
188
+ }
189
+
190
+ return fetchBootstrapSession(store)
191
+ }
192
+
193
+ function reconcileOriginSafety(store) {
194
+ const wasBlocked = store.options.originBlocked === true
195
+ const wasLoading = store.options.originBootstrapLoading === true
196
+ applyOriginSafety(store.options)
197
+
198
+ if (store.options.originBootstrapLoading) {
199
+ disableModuleServices(store)
200
+ return
201
+ }
202
+
203
+ if (store.options.originBlocked && !wasBlocked) {
204
+ disableModuleServices(store)
205
+ } else if (!store.options.originBlocked && (wasBlocked || wasLoading)) {
206
+ enableModuleApi(store)
207
+ }
208
+ }
209
+
210
+ function disableModuleServices(store) {
211
+ store.facade.setImpl(null)
212
+ store.coreApi = null
213
+ }
214
+
215
+ /** Window manager + app store — required even when origin is blocked (AppHubDesktop setup). */
216
+ function ensureModuleInfrastructure(vueApp, store) {
217
+ ensureZoneContext(vueApp, store)
218
+ ensureModuleState(vueApp, store)
219
+ }
220
+
221
+ function enableModuleApi(store) {
222
+ syncApi(store)
223
+ syncZoneContext(store)
224
+ }
225
+
226
+ function enableModuleServices(vueApp, store) {
227
+ ensureModuleInfrastructure(vueApp, store)
228
+ enableModuleApi(store)
229
+ }
230
+
231
+ function buildCredentials(options = {}) {
232
+ return {
233
+ coreUrl: options.coreUrl || '',
234
+ backendUrl: options.backendUrl || '',
235
+ token: options.token || '',
236
+ hostAccessSecret: options.hostAccessSecret || '',
237
+ }
238
+ }
239
+
240
+ function createApiFacade() {
241
+ let impl = null
242
+ return {
243
+ setImpl(next) {
244
+ impl = next
245
+ },
246
+ hasImpl() {
247
+ return impl != null
248
+ },
249
+ bootstrap: (...args) => impl?.bootstrap?.(...args),
250
+ apps: (...args) => impl?.apps?.(...args),
251
+ launch: (...args) => impl?.launch?.(...args),
252
+ ping: (...args) => impl?.ping?.(...args),
253
+ verifyLaunchToken: (...args) => impl?.verifyLaunchToken?.(...args),
254
+ usage: (...args) => impl?.usage?.(...args),
255
+ devApps: (...args) => impl?.devApps?.(...args),
256
+ devInspectBundle: (...args) => impl?.devInspectBundle?.(...args),
257
+ devDisableApp: (...args) => impl?.devDisableApp?.(...args),
258
+ devSetAppStatus: (...args) => impl?.devSetAppStatus?.(...args),
259
+ registerApp: (...args) => impl?.registerApp?.(...args),
260
+ appVersions: (...args) => impl?.appVersions?.(...args),
261
+ integrationDocs: (...args) => impl?.integrationDocs?.(...args),
262
+ integrationDocsInternal: (...args) => impl?.integrationDocsInternal?.(...args),
263
+ grantBridgeScope: (...args) => impl?.grantBridgeScope?.(...args),
264
+ bridgeUser: (...args) => impl?.bridgeUser?.(...args),
265
+ bridgeDesktopMessage: (...args) => impl?.bridgeDesktopMessage?.(...args),
266
+ }
267
+ }
268
+
269
+ function credentialsUnchanged(prev, next) {
270
+ return prev.backendUrl === next.backendUrl
271
+ && prev.token === next.token
272
+ && prev.coreUrl === next.coreUrl
273
+ && prev.hostAccessSecret === next.hostAccessSecret
274
+ }
275
+
276
+ function applyModuleOptions(store, options = {}) {
277
+ const prevCredentials = { ...store.credentials }
278
+ const nextCredentials = buildCredentials(options)
279
+ Object.assign(store.credentials, nextCredentials)
280
+ const credsUnchanged = credentialsUnchanged(prevCredentials, nextCredentials)
281
+
282
+ const nextPublic = buildPublicOptions({ ...options, token: options.token ?? store.credentials.token })
283
+ Object.assign(store.options, {
284
+ language: nextPublic.language,
285
+ theme: nextPublic.theme,
286
+ themeToggle: nextPublic.themeToggle,
287
+ openAppStoreOnMount: nextPublic.openAppStoreOnMount,
288
+ allowedRuntimeOrigins: nextPublic.allowedRuntimeOrigins,
289
+ coreUrl: nextPublic.coreUrl,
290
+ backendUrl: nextPublic.backendUrl,
291
+ hasToken: nextPublic.hasToken,
292
+ allowSameOriginEmbed: nextPublic.allowSameOriginEmbed,
293
+ allowUnsafeOrigin: nextPublic.allowUnsafeOrigin,
294
+ hubOrigin: nextPublic.hubOrigin,
295
+ runtimePublicUrl: nextPublic.runtimePublicUrl,
296
+ enforceDedicatedHubOrigin: nextPublic.enforceDedicatedHubOrigin,
297
+ enforceIsolatedHostedRuntime: nextPublic.enforceIsolatedHostedRuntime,
298
+ allowSameOriginHostedRuntime: nextPublic.allowSameOriginHostedRuntime,
299
+ hostedSandboxSameOrigin: nextPublic.hostedSandboxSameOrigin,
300
+ enforceDevFriendlyOrigins: nextPublic.enforceDevFriendlyOrigins,
301
+ })
302
+ applyOriginSafety(store.options, options)
303
+ if (store.credentials.backendUrl && store.credentials.token) {
304
+ if (credsUnchanged) {
305
+ reconcileOriginSafety(store)
306
+ } else {
307
+ void startBootstrapSession(store)
308
+ }
309
+ } else {
310
+ reconcileOriginSafety(store)
311
+ }
312
+ }
313
+
314
+ function syncApi(store) {
315
+ const { credentials, facade, zoneContext } = store
316
+ const api = credentials.backendUrl && credentials.token
317
+ ? createAppHubApi(credentials.backendUrl, credentials.token, {
318
+ hostAccessSecret: credentials.hostAccessSecret,
319
+ getZoneHeaderId: () => zoneContext?.getZoneHeaderId?.() ?? null,
320
+ })
321
+ : null
322
+ facade.setImpl(api)
323
+ }
324
+
325
+ function syncCoreApi(store) {
326
+ const { credentials } = store
327
+ store.coreApi = credentials.coreUrl && credentials.token
328
+ ? createCoreApi(credentials.coreUrl, credentials.token)
329
+ : null
330
+ }
331
+
332
+ function ensureZoneContext(app, store) {
333
+ if (store.zoneContext) return
334
+ store.zoneContext = createZoneContextState(
335
+ () => store.coreApi,
336
+ () => store.facade,
337
+ {
338
+ ensureBootstrapSession: () => fetchBootstrapSession(store),
339
+ },
340
+ )
341
+ app.provide(APPHUB_ZONE_CONTEXT_KEY, store.zoneContext)
342
+ }
343
+
344
+ function syncZoneContext(store) {
345
+ syncCoreApi(store)
346
+ if (store.zoneContext) {
347
+ store.zoneContext.refresh({ skipBootstrap: true })
348
+ }
349
+ }
350
+
351
+ function ensureModuleState(app, store) {
352
+ if (store.windowManager && store.appStore) {
353
+ return
354
+ }
355
+
356
+ store.windowManager = createWindowManagerState()
357
+ store.appStore = createAppStoreState()
358
+ provideWindowManager(app, store.windowManager)
359
+ provideAppStore(app, store.appStore)
360
+ }
361
+
362
+ /**
363
+ * Install App Hub — Windows desktop shell + modular apps (App Store default).
364
+ * Returns host API facade — keep in host app code, not publisher apps.
365
+ */
366
+ export function installAppHubModule(vueApp, options = {}) {
367
+ let store = getAppHubStore(vueApp)
368
+
369
+ if (store) {
370
+ vueApp.provide('apphubHostApp', vueApp)
371
+ vueApp.provide(APPHUB_MODULE_STORE_KEY, store)
372
+ applyModuleOptions(store, options)
373
+ ensureModuleInfrastructure(vueApp, store)
374
+ reconcileOriginSafety(store)
375
+ return store.facade
376
+ }
377
+
378
+ const facade = createApiFacade()
379
+ const moduleOptions = reactive(buildPublicOptions(options))
380
+ applyOriginSafety(moduleOptions, options)
381
+ const credentials = buildCredentials(options)
382
+
383
+ store = {
384
+ app: vueApp,
385
+ facade,
386
+ options: moduleOptions,
387
+ credentials,
388
+ coreApi: null,
389
+ zoneContext: null,
390
+ windowManager: null,
391
+ appStore: null,
392
+ }
393
+ registerAppHubStore(vueApp, store)
394
+
395
+ vueApp.provide('apphubOptions', moduleOptions)
396
+ vueApp.provide('apphubHostApp', vueApp)
397
+ vueApp.provide(APPHUB_MODULE_STORE_KEY, store)
398
+ vueApp.component('AppHubDesktop', AppHubDesktop)
399
+ vueApp.component('AppHubRunner', AppHubRunner)
400
+
401
+ ensureModuleInfrastructure(vueApp, store)
402
+ void startBootstrapSession(store)
403
+
404
+ return facade
405
+ }
406
+
407
+ /** Whether installAppHubModule has been called for this Vue app instance. */
408
+ export function isAppHubModuleInstalled(vueApp) {
409
+ return vueApp != null && getAppHubStore(vueApp) != null
410
+ }
411
+
412
+ export { useAppHubHostApi, useAppHubModuleStore } from './composables/useAppHubHostApi.js'
413
+ export { useAppHubZoneContext } from './composables/useAppHubZoneContext.js'
414
+ export { createAppHubApi } from './api/index.js'
415
+ export { createCoreApi } from './api/coreApi.js'
416
+ export { AppHubDesktop } from './modules/desktop/index.js'
417
+ export {
418
+ createDesktopNotificationsState,
419
+ useDesktopNotifications,
420
+ AppHubDesktopNotifications,
421
+ parseApiError,
422
+ } from './modules/notifications/index.js'
423
+ export { AppHubRunner } from './modules/runner/index.js'
424
+ export { resolveLang } from './i18n/resolveLang.js'
425
+ export { resolveTheme, normalizeTheme, isThemeLocked } from './i18n/resolveTheme.js'
426
+ export { t } from './i18n/index.js'
427
+ export {
428
+ evaluateOriginSafety,
429
+ isEmbeddedFrame,
430
+ isLocalDevOrigin,
431
+ parseOriginsFromBootstrap,
432
+ resolveEffectiveOrigins,
433
+ ORIGIN_UNSAFE_SAME_ORIGIN_EMBED,
434
+ ORIGIN_UNSAFE_NOT_CONFIGURED,
435
+ ORIGIN_UNSAFE_WRONG_ORIGIN,
436
+ ORIGIN_UNSAFE_RUNTIME_NOT_CONFIGURED,
437
+ ORIGIN_UNSAFE_RUNTIME_SAME_ORIGIN,
438
+ resolveRuntimeApiBase,
439
+ } from './utils/originSafety.js'
440
+
441
+ /** True when installAppHubModule blocked due to unsafe same-origin embed. */
442
+ export function isAppHubOriginBlocked(vueApp) {
443
+ const store = getAppHubStore(vueApp)
444
+ return store?.options?.originBlocked === true
445
+ }
446
+ export * from './modules/app-store/index.js'
447
+ export * from './modules/window-manager/index.js'
448
+ export * from './modules/runner/index.js'
449
+
@@ -1,5 +1,18 @@
1
- /** Private per-Vue-app store — not exposed via provide/inject. */
2
- const installedApps = new WeakMap()
1
+ /** Internal inject keypackage components only; not part of public host API. */
2
+ export const APPHUB_MODULE_STORE_KEY = Symbol('apphubModuleStore')
3
+
4
+ const REGISTRY_KEY = '__kennofizet_apphub_installed_apps__'
5
+
6
+ /** Shared WeakMap — Vite may load this module twice (host bundle vs SFC chunks). */
7
+ function getInstalledAppsRegistry() {
8
+ const root = typeof globalThis !== 'undefined' ? globalThis : global
9
+ if (!root[REGISTRY_KEY]) {
10
+ root[REGISTRY_KEY] = new WeakMap()
11
+ }
12
+ return root[REGISTRY_KEY]
13
+ }
14
+
15
+ const installedApps = getInstalledAppsRegistry()
3
16
 
4
17
  export function registerAppHubStore(app, store) {
5
18
  installedApps.set(app, store)
@@ -97,11 +97,11 @@
97
97
  </template>
98
98
 
99
99
  <script setup>
100
- import { computed, getCurrentInstance, inject, onMounted, ref, watch } from 'vue'
100
+ import { computed, inject, onMounted, ref, watch } from 'vue'
101
101
  import {
102
- getHostApiForApp,
103
- isBackendReadyForApp,
104
- resolveRootApp,
102
+ isBackendReadyFromStore,
103
+ useAppHubHostApi,
104
+ useAppHubModuleStore,
105
105
  } from '../../../composables/useAppHubHostApi.js'
106
106
  import { useAppHubZoneContext } from '../../../composables/useAppHubZoneContext.js'
107
107
  import { t } from '../../../i18n/index.js'
@@ -109,7 +109,6 @@ import { resolveLang } from '../../../i18n/resolveLang.js'
109
109
  import { CATALOG_MODE_STORE } from '../constants/catalogModes.js'
110
110
  import { useCatalogInfiniteScroll } from '../composables/useCatalogInfiniteScroll.js'
111
111
  import { useAppStore } from '../composables/useAppStore.js'
112
- import { apphubDebug, describeApp, describeHostApi } from '../../../utils/apphubDebug.js'
113
112
  import AppHubAppStoreCard from './AppHubAppStoreCard.vue'
114
113
  import AppHubAppStoreSettingsPanel from './AppHubAppStoreSettingsPanel.vue'
115
114
 
@@ -123,8 +122,9 @@ const props = defineProps({
123
122
  const settingsOpen = ref(false)
124
123
  const appStore = useAppStore()
125
124
  const catalog = appStore.catalogs.store
126
- const rootApp = inject('apphubHostApp', null) ?? resolveRootApp(getCurrentInstance())
127
- const rootAppSource = inject('apphubHostApp', null) ? 'inject' : 'resolveRootApp'
125
+ const hubStore = useAppHubModuleStore()
126
+ const hostApi = useAppHubHostApi()
127
+ const rootApp = inject('apphubHostApp', null)
128
128
  const zone = useAppHubZoneContext()
129
129
  const moduleOptions = inject('apphubOptions', {})
130
130
  const lang = computed(() => resolveLang(moduleOptions?.language, 'vi'))
@@ -154,35 +154,17 @@ const labels = computed(() => ({
154
154
 
155
155
  function hostApiOptions() {
156
156
  return {
157
- backendReady: isBackendReadyForApp(rootApp),
157
+ backendReady: isBackendReadyFromStore(hubStore),
158
158
  mode: CATALOG_MODE_STORE,
159
159
  }
160
160
  }
161
161
 
162
- async function reloadCatalog(source = 'manual') {
163
- if (!rootApp) {
164
- apphubDebug('app-store', 'reloadCatalog skipped — no rootApp', { source, rootAppSource })
165
- return
166
- }
167
- const hostApi = getHostApiForApp(rootApp)
168
- const opts = hostApiOptions()
169
- apphubDebug('app-store', 'reloadCatalog', {
170
- source,
171
- rootAppSource,
172
- ...describeApp(rootApp),
173
- ...describeHostApi(hostApi),
174
- backendReady: opts.backendReady,
175
- originBootstrapLoading: moduleOptions?.originBootstrapLoading,
176
- originBlocked: moduleOptions?.originBlocked,
177
- catalogLoaded: catalog.loaded,
178
- catalogError: catalog.error,
179
- })
180
- await appStore.loadCatalog(hostApi, opts)
162
+ async function reloadCatalog() {
163
+ await appStore.loadCatalog(hostApi, hostApiOptions())
181
164
  }
182
165
 
183
166
  async function loadMore() {
184
- if (!rootApp) return
185
- await appStore.loadMoreCatalog(getHostApiForApp(rootApp), CATALOG_MODE_STORE, hostApiOptions())
167
+ await appStore.loadMoreCatalog(hostApi, CATALOG_MODE_STORE, hostApiOptions())
186
168
  }
187
169
 
188
170
  const { rootRef: scrollRoot, sentinelRef: scrollSentinel } = useCatalogInfiniteScroll({
@@ -209,28 +191,21 @@ async function onUninstall(app) {
209
191
  }
210
192
 
211
193
  onMounted(() => {
212
- if (!catalog.loaded) reloadCatalog('onMounted')
194
+ if (!catalog.loaded) reloadCatalog()
213
195
  })
214
196
 
215
197
  watch(
216
198
  () => moduleOptions?.hasToken,
217
199
  (hasToken) => {
218
- if (hasToken && !catalog.loaded) reloadCatalog('watch:hasToken')
200
+ if (hasToken && !catalog.loaded) reloadCatalog()
219
201
  },
220
202
  )
221
203
 
222
204
  watch(
223
205
  () => moduleOptions?.originBootstrapLoading,
224
206
  (loading, wasLoading) => {
225
- apphubDebug('app-store', 'watch originBootstrapLoading', {
226
- loading,
227
- wasLoading,
228
- originBlocked: moduleOptions?.originBlocked,
229
- catalogLoaded: catalog.loaded,
230
- catalogError: catalog.error,
231
- })
232
207
  if (wasLoading && !loading && !moduleOptions?.originBlocked) {
233
- if (!catalog.loaded || catalog.error === 'no_api') reloadCatalog('watch:bootstrap-done')
208
+ if (!catalog.loaded || catalog.error === 'no_api') reloadCatalog()
234
209
  }
235
210
  },
236
211
  )
@@ -238,7 +213,7 @@ watch(
238
213
  watch(
239
214
  () => [zone?.state?.selectedZoneId, zone?.state?.viewAllZones],
240
215
  () => {
241
- if (moduleOptions?.hasToken) reloadCatalog('watch:zone')
216
+ if (moduleOptions?.hasToken) reloadCatalog()
242
217
  },
243
218
  )
244
219
  </script>
@@ -62,11 +62,11 @@
62
62
  </template>
63
63
 
64
64
  <script setup>
65
- import { computed, getCurrentInstance, inject, onMounted, reactive, ref, watch } from 'vue'
65
+ import { computed, inject, onMounted, reactive, ref, watch } from 'vue'
66
66
  import {
67
- getHostApiForApp,
68
- isBackendReadyForApp,
69
- resolveRootApp,
67
+ isBackendReadyFromStore,
68
+ useAppHubHostApi,
69
+ useAppHubModuleStore,
70
70
  } from '../../../composables/useAppHubHostApi.js'
71
71
  import { useAppHubZoneContext } from '../../../composables/useAppHubZoneContext.js'
72
72
  import { t } from '../../../i18n/index.js'
@@ -83,7 +83,9 @@ const props = defineProps({
83
83
 
84
84
  const appStore = useAppStore()
85
85
  const catalog = appStore.catalogs.draft
86
- const rootApp = inject('apphubHostApp', null) ?? resolveRootApp(getCurrentInstance())
86
+ const hubStore = useAppHubModuleStore()
87
+ const hostApi = useAppHubHostApi()
88
+ const rootApp = inject('apphubHostApp', null)
87
89
  const zone = useAppHubZoneContext()
88
90
  const moduleOptions = inject('apphubOptions', {})
89
91
  const lang = computed(() => resolveLang(moduleOptions?.language, 'vi'))
@@ -119,19 +121,17 @@ const labels = computed(() => ({
119
121
 
120
122
  function hostApiOptions() {
121
123
  return {
122
- backendReady: isBackendReadyForApp(rootApp),
124
+ backendReady: isBackendReadyFromStore(hubStore),
123
125
  mode: CATALOG_MODE_DRAFT,
124
126
  }
125
127
  }
126
128
 
127
129
  async function reloadCatalog() {
128
- if (!rootApp) return
129
- await appStore.loadCatalog(getHostApiForApp(rootApp), hostApiOptions())
130
+ await appStore.loadCatalog(hostApi, hostApiOptions())
130
131
  }
131
132
 
132
133
  async function loadMore() {
133
- if (!rootApp) return
134
- await appStore.loadMoreCatalog(getHostApiForApp(rootApp), CATALOG_MODE_DRAFT, hostApiOptions())
134
+ await appStore.loadMoreCatalog(hostApi, CATALOG_MODE_DRAFT, hostApiOptions())
135
135
  }
136
136
 
137
137
  const { rootRef: scrollRoot, sentinelRef: scrollSentinel } = useCatalogInfiniteScroll({
@@ -150,7 +150,7 @@ async function onUninstall(app) {
150
150
  }
151
151
 
152
152
  async function onPing(app) {
153
- const api = getHostApiForApp(rootApp)
153
+ const api = hostApi
154
154
  if (!api?.ping || !app?.slug) return
155
155
  pingingSlug.value = app.slug
156
156
  delete pingResults[app.slug]
@@ -1,7 +1,6 @@
1
1
  import { computed, inject, reactive } from 'vue'
2
2
  import { CATALOG_MODE_DRAFT, CATALOG_MODE_STORE } from '../constants/catalogModes.js'
3
3
  import { normalizeCatalogList } from '../utils/normalizeCatalogApp.js'
4
- import { apphubDebug, describeHostApi } from '../../../utils/apphubDebug.js'
5
4
 
6
5
  const APP_STORE_KEY = 'apphubAppStore'
7
6
 
@@ -109,20 +108,7 @@ export function createAppStoreState(options = {}) {
109
108
  const append = options.append === true
110
109
  const backendReady = options.backendReady !== false
111
110
 
112
- apphubDebug('catalog', 'loadCatalog called', {
113
- mode,
114
- append,
115
- backendReady,
116
- ...describeHostApi(hostApi),
117
- })
118
-
119
111
  if (!backendReady || !hostApiReady(hostApi)) {
120
- apphubDebug('catalog', 'loadCatalog → no_api (not ready)', {
121
- mode,
122
- append,
123
- backendReady,
124
- ...describeHostApi(hostApi),
125
- })
126
112
  if (!append) {
127
113
  bucket.items = []
128
114
  bucket.error = 'no_api'
@@ -151,16 +137,8 @@ export function createAppStoreState(options = {}) {
151
137
  }
152
138
 
153
139
  const res = await hostApi.apps(params)
154
- apphubDebug('catalog', 'loadCatalog apps() response', {
155
- mode,
156
- append,
157
- ...describeHostApi(hostApi),
158
- resUndefined: res === undefined,
159
- resNull: res === null,
160
- })
161
140
  if (res === undefined || res === null) {
162
141
  if (!append) {
163
- apphubDebug('catalog', 'loadCatalog → no_api (undefined response)', { mode })
164
142
  bucket.items = []
165
143
  bucket.error = 'no_api'
166
144
  bucket.loaded = false
@@ -1,47 +0,0 @@
1
- /** Temporary diagnostics — filter console with `[apphub:debug]`. Disable: localStorage.setItem('apphub:debug','0') */
2
- export function isAppHubDebugEnabled() {
3
- try {
4
- if (typeof localStorage !== 'undefined' && localStorage.getItem('apphub:debug') === '0') {
5
- return false
6
- }
7
- if (typeof window !== 'undefined' && window.__APPHUB_DEBUG__ === false) {
8
- return false
9
- }
10
- } catch {
11
- /* ignore */
12
- }
13
- return true
14
- }
15
-
16
- export function apphubDebug(scope, message, data) {
17
- if (!isAppHubDebugEnabled()) return
18
- const payload = data !== undefined ? { ts: Date.now(), ...data } : { ts: Date.now() }
19
- console.info(`[apphub:debug] ${scope}: ${message}`, payload)
20
- }
21
-
22
- export function describeHostApi(hostApi) {
23
- if (!hostApi) return { hostApi: null }
24
- return {
25
- hasImpl: typeof hostApi.hasImpl === 'function' ? hostApi.hasImpl() : null,
26
- hasAppsFn: typeof hostApi.apps === 'function',
27
- }
28
- }
29
-
30
- export function describeStore(store) {
31
- if (!store) return { store: null }
32
- return {
33
- hasImpl: store.facade?.hasImpl?.() ?? null,
34
- originBootstrapLoading: store.options?.originBootstrapLoading ?? null,
35
- originBlocked: store.options?.originBlocked ?? null,
36
- originBlockReason: store.options?.originBlockReason ?? null,
37
- hasToken: store.options?.hasToken ?? null,
38
- backendUrl: store.credentials?.backendUrl ? '(set)' : '(empty)',
39
- token: store.credentials?.token ? `(len ${store.credentials.token.length})` : '(empty)',
40
- serverOriginsResolved: store.options?.serverOriginsResolved ?? null,
41
- }
42
- }
43
-
44
- export function describeApp(app) {
45
- if (!app) return { app: null }
46
- return { appUid: app._uid ?? null }
47
- }