@kennofizet/apphub-frontend 0.1.0

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.
Files changed (90) hide show
  1. package/README.md +84 -0
  2. package/package.json +31 -0
  3. package/src/api/coreApi.js +25 -0
  4. package/src/api/index.js +80 -0
  5. package/src/composables/createZoneContext.js +156 -0
  6. package/src/composables/useAppHubHostApi.js +24 -0
  7. package/src/composables/useAppHubZoneContext.js +11 -0
  8. package/src/composables/useDevOriginToggle.js +40 -0
  9. package/src/i18n/index.js +16 -0
  10. package/src/i18n/resolveLang.js +6 -0
  11. package/src/i18n/resolveTheme.js +30 -0
  12. package/src/i18n/translations/en.js +303 -0
  13. package/src/i18n/translations/vi.js +302 -0
  14. package/src/index.js +427 -0
  15. package/src/moduleStore.js +10 -0
  16. package/src/modules/app-store/components/AppHubAppStoreApp.vue +210 -0
  17. package/src/modules/app-store/components/AppHubAppStoreCard.vue +88 -0
  18. package/src/modules/app-store/components/AppHubAppStoreSettingsPanel.vue +266 -0
  19. package/src/modules/app-store/components/AppHubAppVersionHistory.vue +77 -0
  20. package/src/modules/app-store/components/AppHubDevReviewPanel.vue +206 -0
  21. package/src/modules/app-store/components/AppHubDraftStoreApp.vue +184 -0
  22. package/src/modules/app-store/components/AppHubDraftStoreCard.vue +116 -0
  23. package/src/modules/app-store/composables/useAppStore.js +206 -0
  24. package/src/modules/app-store/composables/useCatalogInfiniteScroll.js +47 -0
  25. package/src/modules/app-store/constants/catalogModes.js +2 -0
  26. package/src/modules/app-store/data/defaultCatalog.js +19 -0
  27. package/src/modules/app-store/index.js +9 -0
  28. package/src/modules/app-store/utils/normalizeCatalogApp.js +37 -0
  29. package/src/modules/desktop/components/AppHubDesktop.vue +1510 -0
  30. package/src/modules/desktop/components/AppHubDesktopDevOriginBar.vue +57 -0
  31. package/src/modules/desktop/components/AppHubDesktopDropLayer.vue +15 -0
  32. package/src/modules/desktop/components/AppHubDesktopDropTarget.vue +32 -0
  33. package/src/modules/desktop/components/AppHubDesktopIconContextMenu.vue +74 -0
  34. package/src/modules/desktop/components/AppHubDesktopIconFolder.vue +60 -0
  35. package/src/modules/desktop/components/AppHubDesktopIconGroup.vue +58 -0
  36. package/src/modules/desktop/components/AppHubDesktopIconInfoDialog.vue +33 -0
  37. package/src/modules/desktop/components/AppHubDesktopIconRenameDialog.vue +62 -0
  38. package/src/modules/desktop/components/AppHubDesktopSettings.vue +28 -0
  39. package/src/modules/desktop/components/AppHubDropInstallBadge.vue +65 -0
  40. package/src/modules/desktop/components/AppHubDuplicateAppDialog.vue +38 -0
  41. package/src/modules/desktop/components/AppHubGuideApp.vue +278 -0
  42. package/src/modules/desktop/components/AppHubOriginBlockScreen.vue +105 -0
  43. package/src/modules/desktop/components/AppHubOriginLoadingScreen.vue +23 -0
  44. package/src/modules/desktop/components/AppHubPlaceholderApp.vue +14 -0
  45. package/src/modules/desktop/components/AppHubSettingsApp.vue +319 -0
  46. package/src/modules/desktop/components/AppHubStartButton.vue +24 -0
  47. package/src/modules/desktop/components/AppHubStartMenu.vue +182 -0
  48. package/src/modules/desktop/components/AppHubTaskbarPins.vue +23 -0
  49. package/src/modules/desktop/components/settings/AppHubSettingsKeyboardPanel.vue +82 -0
  50. package/src/modules/desktop/components/settings/AppHubSettingsScreenPanel.vue +41 -0
  51. package/src/modules/desktop/components/settings/AppHubSettingsStartMenuPanel.vue +95 -0
  52. package/src/modules/desktop/composables/simulateInstallProgress.js +15 -0
  53. package/src/modules/desktop/composables/useDesktopDropInstall.js +272 -0
  54. package/src/modules/desktop/composables/useDesktopHubSettings.js +51 -0
  55. package/src/modules/desktop/composables/useDesktopIconDrag.js +207 -0
  56. package/src/modules/desktop/composables/useDesktopShell.js +335 -0
  57. package/src/modules/desktop/data/builtinApps.js +77 -0
  58. package/src/modules/desktop/index.js +12 -0
  59. package/src/modules/desktop/styles/desktop.css +3104 -0
  60. package/src/modules/desktop/styles/theme.css +616 -0
  61. package/src/modules/desktop/utils/desktopGrid.js +43 -0
  62. package/src/modules/desktop/utils/desktopIconGroups.js +103 -0
  63. package/src/modules/desktop/utils/desktopSession.js +40 -0
  64. package/src/modules/desktop/utils/desktopSettings.js +37 -0
  65. package/src/modules/desktop/utils/dropPackageParser.js +140 -0
  66. package/src/modules/desktop/utils/duplicateAppUtils.js +28 -0
  67. package/src/modules/desktop/utils/hubKeyboardSettings.js +63 -0
  68. package/src/modules/desktop/utils/recentApps.js +148 -0
  69. package/src/modules/desktop/utils/startMenuFavorites.js +100 -0
  70. package/src/modules/desktop/utils/startMenuPins.js +90 -0
  71. package/src/modules/notifications/components/AppHubDesktopNotifications.vue +54 -0
  72. package/src/modules/notifications/composables/createDesktopNotifications.js +86 -0
  73. package/src/modules/notifications/index.js +9 -0
  74. package/src/modules/notifications/styles/notifications.css +118 -0
  75. package/src/modules/notifications/utils/parseApiError.js +29 -0
  76. package/src/modules/runner/components/AppHubRunner.vue +292 -0
  77. package/src/modules/runner/index.js +1 -0
  78. package/src/modules/window-manager/components/AppHubWindowFrame.vue +224 -0
  79. package/src/modules/window-manager/composables/useWindowManager.js +652 -0
  80. package/src/modules/window-manager/index.js +7 -0
  81. package/src/modules/window-manager/utils/sessionLayout.js +28 -0
  82. package/src/modules/window-manager/utils/windowLayout.js +236 -0
  83. package/src/modules/window-manager/utils/windowSnap.js +146 -0
  84. package/src/utils/bootstrapCache.js +47 -0
  85. package/src/utils/devOriginSettings.js +22 -0
  86. package/src/utils/launchUrl.js +111 -0
  87. package/src/utils/originSafety.js +267 -0
  88. package/src/utils/safeStorage.js +191 -0
  89. package/src/utils/semver.js +30 -0
  90. package/src/utils/zoneContext.js +38 -0
@@ -0,0 +1,267 @@
1
+ export const ORIGIN_UNSAFE_SAME_ORIGIN_EMBED = 'same_origin_embed'
2
+ export const ORIGIN_UNSAFE_NOT_CONFIGURED = 'hub_origin_not_configured'
3
+ export const ORIGIN_UNSAFE_WRONG_ORIGIN = 'hub_origin_mismatch'
4
+ export const ORIGIN_UNSAFE_RUNTIME_NOT_CONFIGURED = 'runtime_public_url_not_configured'
5
+ export const ORIGIN_UNSAFE_RUNTIME_SAME_ORIGIN = 'runtime_same_origin_as_hub'
6
+
7
+ function parseOrigin(value) {
8
+ const trimmed = String(value ?? '').trim()
9
+ if (!trimmed) return null
10
+ try {
11
+ return new URL(trimmed).origin
12
+ } catch {
13
+ return null
14
+ }
15
+ }
16
+
17
+ /** localhost / 127.0.0.1 — same gate as evaluateOriginSafety devFriendly bypass. */
18
+ export function isLocalDevOrigin(origin) {
19
+ try {
20
+ const host = new URL(origin).hostname
21
+ return host === 'localhost' || host === '127.0.0.1'
22
+ } catch {
23
+ return false
24
+ }
25
+ }
26
+
27
+ /** True on page when devFriendly origin rules may apply (matches originSafety.js). */
28
+ export function isLocalDevHostPage() {
29
+ if (typeof window === 'undefined') return false
30
+ return isLocalDevOrigin(window.location.origin)
31
+ }
32
+
33
+ export function parseDevUserFromBootstrap(resp) {
34
+ const data = resp?.data?.data ?? resp?.data ?? {}
35
+ return data.is_dev_user === true
36
+ }
37
+
38
+ /**
39
+ * @param {import('axios').AxiosResponse|{ data?: unknown }} resp
40
+ */
41
+ export function parseOriginsFromBootstrap(resp) {
42
+ const data = resp?.data?.data ?? resp?.data ?? {}
43
+ const origins = data.origins ?? {}
44
+ return {
45
+ hubPublicUrl: String(origins.hub_public_url ?? '').trim(),
46
+ frontendOrigin: String(origins.frontend_origin ?? '').trim(),
47
+ runtimePublicUrl: String(origins.runtime_public_url ?? '').trim(),
48
+ originsAuto: origins.auto_derived === true,
49
+ }
50
+ }
51
+
52
+ /**
53
+ * @param {{ runtimePublicUrl?: string, backendUrl?: string }} options
54
+ */
55
+ export function resolveRuntimeApiBase(options = {}) {
56
+ const configured = String(options.runtimePublicUrl ?? '').trim().replace(/\/$/, '')
57
+ if (configured) return configured
58
+ return String(options.backendUrl ?? '').trim().replace(/\/$/, '')
59
+ }
60
+
61
+ /**
62
+ * Merge client install options with server bootstrap origins.
63
+ *
64
+ * @param {{
65
+ * hubOrigin?: string,
66
+ * runtimePublicUrl?: string,
67
+ * backendUrl?: string,
68
+ * serverHubPublicUrl?: string,
69
+ * serverFrontendOrigin?: string,
70
+ * serverRuntimePublicUrl?: string,
71
+ * serverOriginsAuto?: boolean,
72
+ * enforceDevFriendlyOrigins?: boolean,
73
+ * isDevUser?: boolean,
74
+ * }} [options]
75
+ */
76
+ export function resolveEffectiveOrigins(options = {}) {
77
+ const currentOrigin = typeof window !== 'undefined' ? window.location.origin : ''
78
+ const localDev = isLocalDevOrigin(currentOrigin)
79
+ const isDevUser = options.isDevUser === true
80
+ const devFriendlyToggleOn = options.enforceDevFriendlyOrigins !== false
81
+
82
+ const clientHub = String(options.hubOrigin ?? '').trim()
83
+ const clientRuntime = String(options.runtimePublicUrl ?? '').trim()
84
+ const serverHub = String(options.serverHubPublicUrl ?? '').trim()
85
+ const serverFrontend = String(options.serverFrontendOrigin ?? '').trim()
86
+ const serverRuntime = String(options.serverRuntimePublicUrl ?? '').trim()
87
+ const originsAuto = options.serverOriginsAuto === true
88
+
89
+ let hubOrigin = clientHub || serverHub
90
+ let runtimePublicUrl = clientRuntime || serverRuntime
91
+
92
+ let devFriendly = false
93
+
94
+ // Relaxed localhost bypass — dev users only (toggle in DEV bar).
95
+ if (localDev && isDevUser && devFriendlyToggleOn) {
96
+ if (!clientHub) hubOrigin = currentOrigin
97
+ if (!clientRuntime && options.backendUrl) {
98
+ runtimePublicUrl = resolveRuntimeApiBase({ backendUrl: options.backendUrl })
99
+ }
100
+ devFriendly = true
101
+ } else if (localDev && originsAuto && serverHub) {
102
+ // Strict localhost — non-dev, or dev with strict toggle: enforce APP_URL hub host.
103
+ hubOrigin = serverHub
104
+ } else if (!localDev && (serverFrontend || serverHub)) {
105
+ hubOrigin = clientHub || serverFrontend || serverHub
106
+ }
107
+
108
+ return {
109
+ hubOrigin,
110
+ runtimePublicUrl,
111
+ devFriendly,
112
+ localDev,
113
+ originsAuto,
114
+ currentOrigin,
115
+ }
116
+ }
117
+
118
+ function safeResult(extra = {}) {
119
+ return {
120
+ safe: true,
121
+ pending: false,
122
+ loading: false,
123
+ reason: null,
124
+ parentOrigin: null,
125
+ expectedHubOrigin: null,
126
+ expectedRuntimeOrigin: null,
127
+ devFriendly: false,
128
+ ...extra,
129
+ }
130
+ }
131
+
132
+ function unsafe(reason, extra = {}) {
133
+ return {
134
+ safe: false,
135
+ pending: false,
136
+ loading: false,
137
+ reason,
138
+ parentOrigin: extra.parentOrigin ?? null,
139
+ expectedHubOrigin: extra.expectedHubOrigin ?? null,
140
+ expectedRuntimeOrigin: extra.expectedRuntimeOrigin ?? null,
141
+ devFriendly: false,
142
+ }
143
+ }
144
+
145
+ function loadingResult(extra = {}) {
146
+ return {
147
+ safe: false,
148
+ pending: true,
149
+ loading: true,
150
+ reason: null,
151
+ parentOrigin: null,
152
+ expectedHubOrigin: null,
153
+ expectedRuntimeOrigin: null,
154
+ devFriendly: false,
155
+ ...extra,
156
+ }
157
+ }
158
+
159
+ function checkSameOriginEmbed(expectedHubOrigin, expectedRuntimeOrigin) {
160
+ if (typeof window === 'undefined' || window.self === window.top) {
161
+ return null
162
+ }
163
+
164
+ try {
165
+ const parentOrigin = window.parent.location.origin
166
+ if (parentOrigin === window.location.origin) {
167
+ return unsafe(ORIGIN_UNSAFE_SAME_ORIGIN_EMBED, {
168
+ parentOrigin,
169
+ expectedHubOrigin,
170
+ expectedRuntimeOrigin,
171
+ })
172
+ }
173
+ } catch {
174
+ // Cross-origin parent — isolation OK.
175
+ }
176
+
177
+ return null
178
+ }
179
+
180
+ /**
181
+ * App Hub must run on a dedicated origin; hosted bundles on another public runtime origin.
182
+ * Local dev relaxed mode — dev users only (APPHUB_DEV_USER_IDS). Non-dev gets strict checks.
183
+ *
184
+ * @param {{
185
+ * allowSameOriginEmbed?: boolean,
186
+ * allowUnsafeOrigin?: boolean,
187
+ * allowSameOriginHostedRuntime?: boolean,
188
+ * hubOrigin?: string,
189
+ * runtimePublicUrl?: string,
190
+ * backendUrl?: string,
191
+ * serverHubPublicUrl?: string,
192
+ * serverRuntimePublicUrl?: string,
193
+ * serverOriginsResolved?: boolean,
194
+ * hasToken?: boolean,
195
+ * enforceDedicatedHubOrigin?: boolean,
196
+ * enforceIsolatedHostedRuntime?: boolean,
197
+ * enforceDevFriendlyOrigins?: boolean,
198
+ * isDevUser?: boolean,
199
+ * }} [options]
200
+ */
201
+ export function evaluateOriginSafety(options = {}) {
202
+ if (
203
+ options.allowUnsafeOrigin === true
204
+ || options.allowSameOriginEmbed === true
205
+ || options.allowSameOriginHostedRuntime === true
206
+ ) {
207
+ return safeResult()
208
+ }
209
+
210
+ if (typeof window === 'undefined') {
211
+ return safeResult()
212
+ }
213
+
214
+ const effective = resolveEffectiveOrigins(options)
215
+ const expectedHubOrigin = parseOrigin(effective.hubOrigin)
216
+ const expectedRuntimeOrigin = parseOrigin(effective.runtimePublicUrl)
217
+ const enforceDedicated = options.enforceDedicatedHubOrigin !== false
218
+ const enforceRuntime = options.enforceIsolatedHostedRuntime !== false
219
+ const canBootstrap = options.hasToken === true && Boolean(options.backendUrl)
220
+ const serverResolved = options.serverOriginsResolved === true
221
+
222
+ if (effective.devFriendly) {
223
+ const embedBlock = checkSameOriginEmbed(expectedHubOrigin, expectedRuntimeOrigin)
224
+ if (embedBlock) return embedBlock
225
+ return safeResult({
226
+ devFriendly: true,
227
+ expectedHubOrigin,
228
+ expectedRuntimeOrigin,
229
+ })
230
+ }
231
+
232
+ const embedBlock = checkSameOriginEmbed(expectedHubOrigin, expectedRuntimeOrigin)
233
+ if (embedBlock) return embedBlock
234
+
235
+ if (enforceDedicated) {
236
+ if (!expectedHubOrigin) {
237
+ if (!serverResolved && canBootstrap) {
238
+ return loadingResult({ expectedRuntimeOrigin })
239
+ }
240
+ return unsafe(ORIGIN_UNSAFE_NOT_CONFIGURED, { expectedHubOrigin: null, expectedRuntimeOrigin })
241
+ }
242
+
243
+ if (expectedHubOrigin !== effective.currentOrigin) {
244
+ return unsafe(ORIGIN_UNSAFE_WRONG_ORIGIN, { expectedHubOrigin, expectedRuntimeOrigin })
245
+ }
246
+ }
247
+
248
+ if (enforceRuntime && !effective.originsAuto && !effective.localDev) {
249
+ if (!expectedRuntimeOrigin) {
250
+ if (!serverResolved && canBootstrap) {
251
+ return loadingResult({ expectedHubOrigin, expectedRuntimeOrigin: null })
252
+ }
253
+ return unsafe(ORIGIN_UNSAFE_RUNTIME_NOT_CONFIGURED, { expectedHubOrigin, expectedRuntimeOrigin: null })
254
+ }
255
+
256
+ if (expectedRuntimeOrigin === effective.currentOrigin) {
257
+ return unsafe(ORIGIN_UNSAFE_RUNTIME_SAME_ORIGIN, { expectedHubOrigin, expectedRuntimeOrigin })
258
+ }
259
+ }
260
+
261
+ return safeResult({ expectedHubOrigin, expectedRuntimeOrigin })
262
+ }
263
+
264
+ export function isEmbeddedFrame() {
265
+ if (typeof window === 'undefined') return false
266
+ return window.self !== window.top
267
+ }
@@ -0,0 +1,191 @@
1
+ const SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/
2
+ const MAX_STRING = 200
3
+ const MAX_HINT = 500
4
+ const MAX_ICON = 32
5
+ const MAX_LIST = 200
6
+
7
+ export function isValidSlug(slug) {
8
+ return typeof slug === 'string' && SLUG_RE.test(slug)
9
+ }
10
+
11
+ export function clampString(value, max = MAX_STRING) {
12
+ if (typeof value !== 'string') return ''
13
+ return value.slice(0, max)
14
+ }
15
+
16
+ export function safeParseJson(raw, maxBytes = 512 * 1024) {
17
+ if (!raw || typeof raw !== 'string' || raw.length > maxBytes) return null
18
+ let parsed
19
+ try {
20
+ parsed = JSON.parse(raw)
21
+ } catch {
22
+ return null
23
+ }
24
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null
25
+ if (hasDangerousOwnKey(parsed)) return null
26
+ return parsed
27
+ }
28
+
29
+ function hasDangerousOwnKey(obj) {
30
+ const dangerous = ['__proto__', 'constructor', 'prototype']
31
+ return dangerous.some((key) => Object.prototype.hasOwnProperty.call(obj, key))
32
+ }
33
+
34
+ export function sanitizeGroupNames(value) {
35
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
36
+ const out = {}
37
+ let count = 0
38
+ for (const [key, name] of Object.entries(value)) {
39
+ if (count >= MAX_LIST) break
40
+ const id = clampString(key, 64)
41
+ if (!id) continue
42
+ out[id] = clampString(name, MAX_STRING)
43
+ count += 1
44
+ }
45
+ return out
46
+ }
47
+
48
+ export function sanitizeDesktopSettings(parsed) {
49
+ if (!parsed || typeof parsed !== 'object') return null
50
+ const theme = parsed.theme
51
+ return {
52
+ snapToGrid: typeof parsed.snapToGrid === 'boolean' ? parsed.snapToGrid : undefined,
53
+ theme: theme === 'dark' || theme === 'light' || theme === 'auto' ? theme : undefined,
54
+ groupNames: sanitizeGroupNames(parsed.groupNames),
55
+ builtinPositions: sanitizeBuiltinPlacements(parsed.builtinPositions),
56
+ }
57
+ }
58
+
59
+ function sanitizeBuiltinPlacements(value) {
60
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
61
+ const out = {}
62
+ let count = 0
63
+ for (const [key, pos] of Object.entries(value)) {
64
+ if (count >= MAX_LIST) break
65
+ const id = clampString(key, 64)
66
+ if (!id || !pos || typeof pos !== 'object') continue
67
+ const x = Number(pos.x)
68
+ const y = Number(pos.y)
69
+ if (!Number.isFinite(x) || !Number.isFinite(y)) continue
70
+ out[id] = { x: Math.round(x), y: Math.round(y) }
71
+ count += 1
72
+ }
73
+ return Object.keys(out).length ? out : undefined
74
+ }
75
+
76
+ export function sanitizeUserApp(app) {
77
+ if (!app || typeof app !== 'object') return null
78
+ const slug = isValidSlug(app.slug)
79
+ ? app.slug
80
+ : (isValidSlug(app.id?.replace(/^user-/, '')) ? app.id.replace(/^user-/, '') : null)
81
+ if (!slug) return null
82
+
83
+ const installMethod = app.installMethod === 'local' || app.installMethod === 'appstore' || app.installMethod === 'publish'
84
+ ? app.installMethod
85
+ : (app.local ? 'local' : 'appstore')
86
+
87
+ const status = app.status === 'draft' || app.status === 'active' || app.status === 'disabled'
88
+ ? app.status
89
+ : 'active'
90
+
91
+ const desktopX = Number(app.desktopX)
92
+ const desktopY = Number(app.desktopY)
93
+
94
+ return {
95
+ id: `user-${slug}`,
96
+ slug,
97
+ installedVersion: typeof app.installedVersion === 'string'
98
+ ? clampString(app.installedVersion, 64) || null
99
+ : (typeof app.version === 'string' ? clampString(app.version, 64) || null : null),
100
+ version: typeof app.installedVersion === 'string'
101
+ ? clampString(app.installedVersion, 64) || null
102
+ : (typeof app.version === 'string' ? clampString(app.version, 64) || null : null),
103
+ name: clampString(app.name) || slug,
104
+ icon: clampString(app.icon, MAX_ICON) || '📦',
105
+ hint: clampString(app.hint ?? app.description, MAX_HINT),
106
+ status,
107
+ runtime_type: app.runtime_type === 'iframe' || app.runtime_type === 'hosted' || app.runtime_type === 'connected' || app.runtime_type === 'native'
108
+ ? app.runtime_type
109
+ : 'iframe',
110
+ entry_url: typeof app.entry_url === 'string' ? clampString(app.entry_url, 2048) || null : null,
111
+ healthcheck_url: typeof app.healthcheck_url === 'string' ? clampString(app.healthcheck_url, 2048) || null : null,
112
+ builtin: false,
113
+ local: installMethod === 'local',
114
+ installMethod,
115
+ createdAt: clampString(app.createdAt, 40) || new Date().toISOString(),
116
+ windowTitle: clampString(app.windowTitle ?? app.name) || slug,
117
+ width: clampNumber(app.width, 320, 4096, 640),
118
+ height: clampNumber(app.height, 240, 4096, 420),
119
+ desktopX: Number.isFinite(desktopX) ? Math.round(desktopX) : null,
120
+ desktopY: Number.isFinite(desktopY) ? Math.round(desktopY) : null,
121
+ }
122
+ }
123
+
124
+ function clampNumber(value, min, max, fallback) {
125
+ const n = Number(value)
126
+ if (!Number.isFinite(n)) return fallback
127
+ return Math.min(max, Math.max(min, Math.round(n)))
128
+ }
129
+
130
+ export function sanitizeWindowState(win) {
131
+ if (!win || typeof win !== 'object') return null
132
+ const appId = clampString(win.appId, 80)
133
+ if (!appId) return null
134
+ const display = win.display === 'mini' || win.display === 'fullscreen' ? win.display : 'mini'
135
+ return {
136
+ appId,
137
+ minimized: !!win.minimized,
138
+ display,
139
+ x: clampNumber(win.x, 0, 100000, 0),
140
+ y: clampNumber(win.y, 0, 100000, 0),
141
+ width: clampNumber(win.width, 200, 10000, 720),
142
+ height: clampNumber(win.height, 200, 10000, 480),
143
+ zIndex: clampNumber(win.zIndex, 0, 100000, 1),
144
+ }
145
+ }
146
+
147
+ export function sanitizeDesktopSession(parsed) {
148
+ if (!parsed || typeof parsed !== 'object') return null
149
+
150
+ const userApps = Array.isArray(parsed.userApps)
151
+ ? parsed.userApps.slice(0, MAX_LIST).map(sanitizeUserApp).filter(Boolean)
152
+ : []
153
+
154
+ const installedSlugs = Array.isArray(parsed.installedSlugs)
155
+ ? [...new Set(parsed.installedSlugs.filter(isValidSlug))].slice(0, MAX_LIST)
156
+ : []
157
+
158
+ const windows = Array.isArray(parsed.windows)
159
+ ? parsed.windows.slice(0, MAX_LIST).map(sanitizeWindowState).filter(Boolean)
160
+ : []
161
+
162
+ const settings = sanitizeDesktopSettings(parsed.settings)
163
+ const activeId = typeof parsed.activeId === 'string' ? clampString(parsed.activeId, 80) : null
164
+
165
+ return {
166
+ userApps,
167
+ installedSlugs,
168
+ windows,
169
+ activeId: activeId || null,
170
+ settings: settings ?? undefined,
171
+ }
172
+ }
173
+
174
+ export function sanitizeWindowLayout(parsed) {
175
+ if (!parsed || typeof parsed !== 'object') return null
176
+ const display = parsed.display === 'mini' || parsed.display === 'fullscreen' ? parsed.display : null
177
+ if (!display) return null
178
+
179
+ const mini = parsed.mini ?? parsed
180
+ if (!mini || typeof mini !== 'object') return { display }
181
+
182
+ return {
183
+ display,
184
+ mini: {
185
+ x: mini.x == null ? null : clampNumber(mini.x, 0, 100000, 0),
186
+ y: mini.y == null ? null : clampNumber(mini.y, 0, 100000, 0),
187
+ width: clampNumber(mini.width, 200, 10000, 720),
188
+ height: clampNumber(mini.height, 200, 10000, 480),
189
+ },
190
+ }
191
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @param {string|null|undefined} version
3
+ * @returns {number[]}
4
+ */
5
+ function parseParts(version) {
6
+ if (!version || typeof version !== 'string') return [0, 0, 0]
7
+ return version.trim().split('.').map((part) => {
8
+ const n = Number.parseInt(part, 10)
9
+ return Number.isFinite(n) ? n : 0
10
+ })
11
+ }
12
+
13
+ /**
14
+ * @param {string|null|undefined} next
15
+ * @param {string|null|undefined} current
16
+ */
17
+ export function isSemverGreaterThan(next, current) {
18
+ const a = parseParts(next)
19
+ const b = parseParts(current)
20
+ const len = Math.max(a.length, b.length, 3)
21
+
22
+ for (let i = 0; i < len; i += 1) {
23
+ const da = a[i] ?? 0
24
+ const db = b[i] ?? 0
25
+ if (da > db) return true
26
+ if (da < db) return false
27
+ }
28
+
29
+ return false
30
+ }
@@ -0,0 +1,38 @@
1
+ /** Shared with other KNF packages on the same host (e.g. workpoint). */
2
+ export const ZONE_STORAGE_KEY = 'selected_zone'
3
+
4
+ export function parseZonesFromResponse(resp) {
5
+ const data = resp?.data?.datas ?? resp?.data?.data ?? resp?.data ?? {}
6
+ if (Array.isArray(data.zones)) return data.zones
7
+ if (Array.isArray(data)) return data
8
+ return []
9
+ }
10
+
11
+ export function parseZonesMeta(resp) {
12
+ const data = resp?.data?.datas ?? resp?.data?.data ?? resp?.data ?? {}
13
+ return {
14
+ timezone: data.timezone ?? null,
15
+ isManager: !!data.is_manager,
16
+ }
17
+ }
18
+
19
+ export function loadStoredZone() {
20
+ try {
21
+ const raw = localStorage.getItem(ZONE_STORAGE_KEY)
22
+ if (!raw) return null
23
+ const zone = JSON.parse(raw)
24
+ if (zone && zone.id != null) return zone
25
+ } catch {
26
+ /* ignore */
27
+ }
28
+ return null
29
+ }
30
+
31
+ export function saveStoredZone(zone) {
32
+ if (!zone?.id) return
33
+ try {
34
+ localStorage.setItem(ZONE_STORAGE_KEY, JSON.stringify({ id: zone.id, name: zone.name ?? '' }))
35
+ } catch {
36
+ /* ignore */
37
+ }
38
+ }