@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.
- package/README.md +84 -0
- package/package.json +31 -0
- package/src/api/coreApi.js +25 -0
- package/src/api/index.js +80 -0
- package/src/composables/createZoneContext.js +156 -0
- package/src/composables/useAppHubHostApi.js +24 -0
- package/src/composables/useAppHubZoneContext.js +11 -0
- package/src/composables/useDevOriginToggle.js +40 -0
- package/src/i18n/index.js +16 -0
- package/src/i18n/resolveLang.js +6 -0
- package/src/i18n/resolveTheme.js +30 -0
- package/src/i18n/translations/en.js +303 -0
- package/src/i18n/translations/vi.js +302 -0
- package/src/index.js +427 -0
- package/src/moduleStore.js +10 -0
- package/src/modules/app-store/components/AppHubAppStoreApp.vue +210 -0
- package/src/modules/app-store/components/AppHubAppStoreCard.vue +88 -0
- package/src/modules/app-store/components/AppHubAppStoreSettingsPanel.vue +266 -0
- package/src/modules/app-store/components/AppHubAppVersionHistory.vue +77 -0
- package/src/modules/app-store/components/AppHubDevReviewPanel.vue +206 -0
- package/src/modules/app-store/components/AppHubDraftStoreApp.vue +184 -0
- package/src/modules/app-store/components/AppHubDraftStoreCard.vue +116 -0
- package/src/modules/app-store/composables/useAppStore.js +206 -0
- package/src/modules/app-store/composables/useCatalogInfiniteScroll.js +47 -0
- package/src/modules/app-store/constants/catalogModes.js +2 -0
- package/src/modules/app-store/data/defaultCatalog.js +19 -0
- package/src/modules/app-store/index.js +9 -0
- package/src/modules/app-store/utils/normalizeCatalogApp.js +37 -0
- package/src/modules/desktop/components/AppHubDesktop.vue +1510 -0
- package/src/modules/desktop/components/AppHubDesktopDevOriginBar.vue +57 -0
- package/src/modules/desktop/components/AppHubDesktopDropLayer.vue +15 -0
- package/src/modules/desktop/components/AppHubDesktopDropTarget.vue +32 -0
- package/src/modules/desktop/components/AppHubDesktopIconContextMenu.vue +74 -0
- package/src/modules/desktop/components/AppHubDesktopIconFolder.vue +60 -0
- package/src/modules/desktop/components/AppHubDesktopIconGroup.vue +58 -0
- package/src/modules/desktop/components/AppHubDesktopIconInfoDialog.vue +33 -0
- package/src/modules/desktop/components/AppHubDesktopIconRenameDialog.vue +62 -0
- package/src/modules/desktop/components/AppHubDesktopSettings.vue +28 -0
- package/src/modules/desktop/components/AppHubDropInstallBadge.vue +65 -0
- package/src/modules/desktop/components/AppHubDuplicateAppDialog.vue +38 -0
- package/src/modules/desktop/components/AppHubGuideApp.vue +278 -0
- package/src/modules/desktop/components/AppHubOriginBlockScreen.vue +105 -0
- package/src/modules/desktop/components/AppHubOriginLoadingScreen.vue +23 -0
- package/src/modules/desktop/components/AppHubPlaceholderApp.vue +14 -0
- package/src/modules/desktop/components/AppHubSettingsApp.vue +319 -0
- package/src/modules/desktop/components/AppHubStartButton.vue +24 -0
- package/src/modules/desktop/components/AppHubStartMenu.vue +182 -0
- package/src/modules/desktop/components/AppHubTaskbarPins.vue +23 -0
- package/src/modules/desktop/components/settings/AppHubSettingsKeyboardPanel.vue +82 -0
- package/src/modules/desktop/components/settings/AppHubSettingsScreenPanel.vue +41 -0
- package/src/modules/desktop/components/settings/AppHubSettingsStartMenuPanel.vue +95 -0
- package/src/modules/desktop/composables/simulateInstallProgress.js +15 -0
- package/src/modules/desktop/composables/useDesktopDropInstall.js +272 -0
- package/src/modules/desktop/composables/useDesktopHubSettings.js +51 -0
- package/src/modules/desktop/composables/useDesktopIconDrag.js +207 -0
- package/src/modules/desktop/composables/useDesktopShell.js +335 -0
- package/src/modules/desktop/data/builtinApps.js +77 -0
- package/src/modules/desktop/index.js +12 -0
- package/src/modules/desktop/styles/desktop.css +3104 -0
- package/src/modules/desktop/styles/theme.css +616 -0
- package/src/modules/desktop/utils/desktopGrid.js +43 -0
- package/src/modules/desktop/utils/desktopIconGroups.js +103 -0
- package/src/modules/desktop/utils/desktopSession.js +40 -0
- package/src/modules/desktop/utils/desktopSettings.js +37 -0
- package/src/modules/desktop/utils/dropPackageParser.js +140 -0
- package/src/modules/desktop/utils/duplicateAppUtils.js +28 -0
- package/src/modules/desktop/utils/hubKeyboardSettings.js +63 -0
- package/src/modules/desktop/utils/recentApps.js +148 -0
- package/src/modules/desktop/utils/startMenuFavorites.js +100 -0
- package/src/modules/desktop/utils/startMenuPins.js +90 -0
- package/src/modules/notifications/components/AppHubDesktopNotifications.vue +54 -0
- package/src/modules/notifications/composables/createDesktopNotifications.js +86 -0
- package/src/modules/notifications/index.js +9 -0
- package/src/modules/notifications/styles/notifications.css +118 -0
- package/src/modules/notifications/utils/parseApiError.js +29 -0
- package/src/modules/runner/components/AppHubRunner.vue +292 -0
- package/src/modules/runner/index.js +1 -0
- package/src/modules/window-manager/components/AppHubWindowFrame.vue +224 -0
- package/src/modules/window-manager/composables/useWindowManager.js +652 -0
- package/src/modules/window-manager/index.js +7 -0
- package/src/modules/window-manager/utils/sessionLayout.js +28 -0
- package/src/modules/window-manager/utils/windowLayout.js +236 -0
- package/src/modules/window-manager/utils/windowSnap.js +146 -0
- package/src/utils/bootstrapCache.js +47 -0
- package/src/utils/devOriginSettings.js +22 -0
- package/src/utils/launchUrl.js +111 -0
- package/src/utils/originSafety.js +267 -0
- package/src/utils/safeStorage.js +191 -0
- package/src/utils/semver.js +30 -0
- 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
|
+
}
|