@kennofizet/apphub-frontend 0.1.6 → 0.1.8

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.6",
3
+ "version": "0.1.8",
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
@@ -8,7 +8,7 @@ import { APPHUB_ZONE_CONTEXT_KEY } from './composables/useAppHubZoneContext.js'
8
8
  import { createAppStoreState, provideAppStore } from './modules/app-store/index.js'
9
9
  import { AppHubDesktop } from './modules/desktop/index.js'
10
10
  import { AppHubRunner } from './modules/runner/index.js'
11
- import { getAppHubStore, registerAppHubStore } from './moduleStore.js'
11
+ import { getAppHubStore, registerAppHubStore, APPHUB_MODULE_STORE_KEY } from './moduleStore.js'
12
12
  import {
13
13
  createWindowManagerState,
14
14
  provideWindowManager,
@@ -24,6 +24,11 @@ import {
24
24
  loadBootstrapCache,
25
25
  saveBootstrapCache,
26
26
  } from './utils/bootstrapCache.js'
27
+ import {
28
+ apphubDebug,
29
+ describeHostApi,
30
+ describeStore,
31
+ } from './utils/apphubDebug.js'
27
32
 
28
33
  const bootstrapInflight = new WeakMap()
29
34
 
@@ -136,6 +141,7 @@ function applyBootstrapOrigins(store, bootstrapResponse, { fromCache = false } =
136
141
  reconcileOriginSafety(store)
137
142
  store.options.originBootstrapLoading = false
138
143
  applyOriginSafety(store.options)
144
+ apphubDebug('bootstrap', 'applyBootstrapOrigins done', describeStore(store))
139
145
  }
140
146
 
141
147
  function applyCachedBootstrapIfAny(store) {
@@ -150,12 +156,24 @@ async function fetchBootstrapSession(store) {
150
156
  if (promise) return promise
151
157
 
152
158
  promise = (async () => {
159
+ apphubDebug('bootstrap', 'fetchBootstrapSession start', describeStore(store))
153
160
  syncApi(store)
154
- if (!store.facade?.bootstrap) return
161
+ if (!store.facade?.bootstrap) {
162
+ apphubDebug('bootstrap', 'fetchBootstrapSession abort — no bootstrap fn', describeStore(store))
163
+ return
164
+ }
155
165
 
156
166
  const res = await store.facade.bootstrap()
167
+ apphubDebug('bootstrap', 'fetchBootstrapSession bootstrap response', {
168
+ ...describeStore(store),
169
+ hasResponse: !!res,
170
+ })
157
171
  if (res) applyBootstrapOrigins(store, res)
158
- })().catch(() => {
172
+ })().catch((err) => {
173
+ apphubDebug('bootstrap', 'fetchBootstrapSession failed', {
174
+ ...describeStore(store),
175
+ error: err?.message ?? String(err),
176
+ })
159
177
  store.options.originBootstrapLoading = false
160
178
  if (!store.options.serverOriginsResolved) {
161
179
  reconcileOriginSafety(store)
@@ -170,6 +188,11 @@ async function fetchBootstrapSession(store) {
170
188
 
171
189
  function startBootstrapSession(store) {
172
190
  const { backendUrl, token } = store.credentials
191
+ apphubDebug('bootstrap', 'startBootstrapSession', {
192
+ ...describeStore(store),
193
+ hasBackendUrl: !!backendUrl,
194
+ hasToken: !!token,
195
+ })
173
196
  if (!backendUrl || !token) {
174
197
  store.options.originBootstrapLoading = false
175
198
  reconcileOriginSafety(store)
@@ -180,11 +203,15 @@ function startBootstrapSession(store) {
180
203
  applyOriginSafety(store.options)
181
204
 
182
205
  const hadCache = applyCachedBootstrapIfAny(store)
206
+ apphubDebug('bootstrap', 'startBootstrapSession after cache', {
207
+ hadCache,
208
+ ...describeStore(store),
209
+ })
183
210
  if (!hadCache && !store.options.originBlocked) {
184
211
  store.options.originBootstrapLoading = true
185
212
  store.options.originBlocked = false
186
213
  applyOriginSafety(store.options)
187
- disableModuleServices(store)
214
+ disableModuleServices(store, 'startBootstrapSession:no-cache')
188
215
  }
189
216
 
190
217
  return fetchBootstrapSession(store)
@@ -195,21 +222,30 @@ function reconcileOriginSafety(store) {
195
222
  const wasLoading = store.options.originBootstrapLoading === true
196
223
  applyOriginSafety(store.options)
197
224
 
225
+ let action = 'noop'
198
226
  if (store.options.originBootstrapLoading) {
199
- disableModuleServices(store)
200
- return
201
- }
202
-
203
- if (store.options.originBlocked && !wasBlocked) {
204
- disableModuleServices(store)
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'
205
232
  } else if (!store.options.originBlocked && (wasBlocked || wasLoading)) {
206
233
  enableModuleApi(store)
234
+ action = 'enable'
207
235
  }
236
+
237
+ apphubDebug('origin', 'reconcileOriginSafety', {
238
+ wasLoading,
239
+ wasBlocked,
240
+ action,
241
+ ...describeStore(store),
242
+ })
208
243
  }
209
244
 
210
- function disableModuleServices(store) {
245
+ function disableModuleServices(store, reason = 'unknown') {
211
246
  store.facade.setImpl(null)
212
247
  store.coreApi = null
248
+ apphubDebug('api', 'disableModuleServices', { reason, ...describeStore(store) })
213
249
  }
214
250
 
215
251
  /** Window manager + app store — required even when origin is blocked (AppHubDesktop setup). */
@@ -221,6 +257,7 @@ function ensureModuleInfrastructure(vueApp, store) {
221
257
  function enableModuleApi(store) {
222
258
  syncApi(store)
223
259
  syncZoneContext(store)
260
+ apphubDebug('api', 'enableModuleApi', describeStore(store))
224
261
  }
225
262
 
226
263
  function enableModuleServices(vueApp, store) {
@@ -300,6 +337,14 @@ function applyModuleOptions(store, options = {}) {
300
337
  enforceDevFriendlyOrigins: nextPublic.enforceDevFriendlyOrigins,
301
338
  })
302
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
+ })
303
348
  if (store.credentials.backendUrl && store.credentials.token) {
304
349
  if (credsUnchanged) {
305
350
  reconcileOriginSafety(store)
@@ -320,6 +365,10 @@ function syncApi(store) {
320
365
  })
321
366
  : null
322
367
  facade.setImpl(api)
368
+ apphubDebug('api', 'syncApi', {
369
+ implSet: api != null,
370
+ ...describeStore(store),
371
+ })
323
372
  }
324
373
 
325
374
  function syncCoreApi(store) {
@@ -368,12 +417,25 @@ export function installAppHubModule(vueApp, options = {}) {
368
417
 
369
418
  if (store) {
370
419
  vueApp.provide('apphubHostApp', vueApp)
420
+ vueApp.provide(APPHUB_MODULE_STORE_KEY, store)
421
+ apphubDebug('install', 'installAppHubModule re-install', {
422
+ appUid: vueApp._uid ?? null,
423
+ hasToken: !!(options.token),
424
+ theme: options.theme,
425
+ language: options.language,
426
+ })
371
427
  applyModuleOptions(store, options)
372
428
  ensureModuleInfrastructure(vueApp, store)
373
429
  reconcileOriginSafety(store)
374
430
  return store.facade
375
431
  }
376
432
 
433
+ apphubDebug('install', 'installAppHubModule first install', {
434
+ appUid: vueApp._uid ?? null,
435
+ hasToken: !!(options.token),
436
+ backendUrl: options.backendUrl ? '(set)' : '(empty)',
437
+ })
438
+
377
439
  const facade = createApiFacade()
378
440
  const moduleOptions = reactive(buildPublicOptions(options))
379
441
  applyOriginSafety(moduleOptions, options)
@@ -393,6 +455,7 @@ export function installAppHubModule(vueApp, options = {}) {
393
455
 
394
456
  vueApp.provide('apphubOptions', moduleOptions)
395
457
  vueApp.provide('apphubHostApp', vueApp)
458
+ vueApp.provide(APPHUB_MODULE_STORE_KEY, store)
396
459
  vueApp.component('AppHubDesktop', AppHubDesktop)
397
460
  vueApp.component('AppHubRunner', AppHubRunner)
398
461
 
@@ -407,7 +470,7 @@ export function isAppHubModuleInstalled(vueApp) {
407
470
  return vueApp != null && getAppHubStore(vueApp) != null
408
471
  }
409
472
 
410
- export { useAppHubHostApi } from './composables/useAppHubHostApi.js'
473
+ export { useAppHubHostApi, useAppHubModuleStore } from './composables/useAppHubHostApi.js'
411
474
  export { useAppHubZoneContext } from './composables/useAppHubZoneContext.js'
412
475
  export { createAppHubApi } from './api/index.js'
413
476
  export { createCoreApi } from './api/coreApi.js'
@@ -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,6 +109,7 @@ 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, describeHostApi } from '../../../utils/apphubDebug.js'
112
113
  import AppHubAppStoreCard from './AppHubAppStoreCard.vue'
113
114
  import AppHubAppStoreSettingsPanel from './AppHubAppStoreSettingsPanel.vue'
114
115
 
@@ -122,7 +123,9 @@ const props = defineProps({
122
123
  const settingsOpen = ref(false)
123
124
  const appStore = useAppStore()
124
125
  const catalog = appStore.catalogs.store
125
- const rootApp = inject('apphubHostApp', null) ?? resolveRootApp(getCurrentInstance())
126
+ const hubStore = useAppHubModuleStore()
127
+ const hostApi = useAppHubHostApi()
128
+ const rootApp = inject('apphubHostApp', null)
126
129
  const zone = useAppHubZoneContext()
127
130
  const moduleOptions = inject('apphubOptions', {})
128
131
  const lang = computed(() => resolveLang(moduleOptions?.language, 'vi'))
@@ -152,19 +155,29 @@ const labels = computed(() => ({
152
155
 
153
156
  function hostApiOptions() {
154
157
  return {
155
- backendReady: isBackendReadyForApp(rootApp),
158
+ backendReady: isBackendReadyFromStore(hubStore),
156
159
  mode: CATALOG_MODE_STORE,
157
160
  }
158
161
  }
159
162
 
160
- async function reloadCatalog() {
161
- if (!rootApp) return
162
- await appStore.loadCatalog(getHostApiForApp(rootApp), hostApiOptions())
163
+ async function reloadCatalog(source = 'manual') {
164
+ const api = hostApi
165
+ const opts = hostApiOptions()
166
+ apphubDebug('app-store', 'reloadCatalog', {
167
+ source,
168
+ storeFromInject: !!hubStore,
169
+ ...describeHostApi(api),
170
+ backendReady: opts.backendReady,
171
+ originBootstrapLoading: moduleOptions?.originBootstrapLoading,
172
+ originBlocked: moduleOptions?.originBlocked,
173
+ catalogLoaded: catalog.loaded,
174
+ catalogError: catalog.error,
175
+ })
176
+ await appStore.loadCatalog(api, opts)
163
177
  }
164
178
 
165
179
  async function loadMore() {
166
- if (!rootApp) return
167
- await appStore.loadMoreCatalog(getHostApiForApp(rootApp), CATALOG_MODE_STORE, hostApiOptions())
180
+ await appStore.loadMoreCatalog(hostApi, CATALOG_MODE_STORE, hostApiOptions())
168
181
  }
169
182
 
170
183
  const { rootRef: scrollRoot, sentinelRef: scrollSentinel } = useCatalogInfiniteScroll({
@@ -191,21 +204,28 @@ async function onUninstall(app) {
191
204
  }
192
205
 
193
206
  onMounted(() => {
194
- if (!catalog.loaded) reloadCatalog()
207
+ if (!catalog.loaded) reloadCatalog('onMounted')
195
208
  })
196
209
 
197
210
  watch(
198
211
  () => moduleOptions?.hasToken,
199
212
  (hasToken) => {
200
- if (hasToken && !catalog.loaded) reloadCatalog()
213
+ if (hasToken && !catalog.loaded) reloadCatalog('watch:hasToken')
201
214
  },
202
215
  )
203
216
 
204
217
  watch(
205
218
  () => moduleOptions?.originBootstrapLoading,
206
219
  (loading, wasLoading) => {
220
+ apphubDebug('app-store', 'watch originBootstrapLoading', {
221
+ loading,
222
+ wasLoading,
223
+ originBlocked: moduleOptions?.originBlocked,
224
+ catalogLoaded: catalog.loaded,
225
+ catalogError: catalog.error,
226
+ })
207
227
  if (wasLoading && !loading && !moduleOptions?.originBlocked) {
208
- if (!catalog.loaded || catalog.error === 'no_api') reloadCatalog()
228
+ if (!catalog.loaded || catalog.error === 'no_api') reloadCatalog('watch:bootstrap-done')
209
229
  }
210
230
  },
211
231
  )
@@ -213,7 +233,7 @@ watch(
213
233
  watch(
214
234
  () => [zone?.state?.selectedZoneId, zone?.state?.viewAllZones],
215
235
  () => {
216
- if (moduleOptions?.hasToken) reloadCatalog()
236
+ if (moduleOptions?.hasToken) reloadCatalog('watch:zone')
217
237
  },
218
238
  )
219
239
  </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,6 +1,7 @@
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'
4
5
 
5
6
  const APP_STORE_KEY = 'apphubAppStore'
6
7
 
@@ -28,6 +29,12 @@ function filterItems(items, search) {
28
29
  )
29
30
  }
30
31
 
32
+ function hostApiReady(hostApi) {
33
+ if (!hostApi?.apps) return false
34
+ if (typeof hostApi.hasImpl === 'function') return hostApi.hasImpl()
35
+ return true
36
+ }
37
+
31
38
  /**
32
39
  * Independent App Store module — separate catalog buckets per mode (store / draft).
33
40
  */
@@ -102,7 +109,20 @@ export function createAppStoreState(options = {}) {
102
109
  const append = options.append === true
103
110
  const backendReady = options.backendReady !== false
104
111
 
105
- if (!backendReady || !hostApi?.apps) {
112
+ apphubDebug('catalog', 'loadCatalog called', {
113
+ mode,
114
+ append,
115
+ backendReady,
116
+ ...describeHostApi(hostApi),
117
+ })
118
+
119
+ if (!backendReady || !hostApiReady(hostApi)) {
120
+ apphubDebug('catalog', 'loadCatalog → no_api (not ready)', {
121
+ mode,
122
+ append,
123
+ backendReady,
124
+ ...describeHostApi(hostApi),
125
+ })
106
126
  if (!append) {
107
127
  bucket.items = []
108
128
  bucket.error = 'no_api'
@@ -131,8 +151,16 @@ export function createAppStoreState(options = {}) {
131
151
  }
132
152
 
133
153
  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
+ })
134
161
  if (res === undefined || res === null) {
135
162
  if (!append) {
163
+ apphubDebug('catalog', 'loadCatalog → no_api (undefined response)', { mode })
136
164
  bucket.items = []
137
165
  bucket.error = 'no_api'
138
166
  bucket.loaded = false
@@ -0,0 +1,47 @@
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
+ }