@unisim/sdk 0.110.1 → 0.112.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/dist/ChangelogMenu.d.ts +17 -1
- package/dist/ChangelogMenu.d.ts.map +1 -1
- package/dist/ChangelogMenu.js +17 -3
- package/dist/ChangelogMenu.js.map +1 -1
- package/dist/CompanyMenu.d.ts +12 -1
- package/dist/CompanyMenu.d.ts.map +1 -1
- package/dist/CompanyMenu.js +25 -16
- package/dist/CompanyMenu.js.map +1 -1
- package/dist/KnowledgeBaseMenu.d.ts +11 -1
- package/dist/KnowledgeBaseMenu.d.ts.map +1 -1
- package/dist/KnowledgeBaseMenu.js +63 -33
- package/dist/KnowledgeBaseMenu.js.map +1 -1
- package/dist/ProfileDialog.d.ts +14 -0
- package/dist/ProfileDialog.d.ts.map +1 -0
- package/dist/ProfileDialog.js +250 -0
- package/dist/ProfileDialog.js.map +1 -0
- package/dist/UniversalAppsNavBar.d.ts +5 -38
- package/dist/UniversalAppsNavBar.d.ts.map +1 -1
- package/dist/UniversalAppsNavBar.js +6 -33
- package/dist/UniversalAppsNavBar.js.map +1 -1
- package/dist/UniversalNavBar.d.ts +24 -1
- package/dist/UniversalNavBar.d.ts.map +1 -1
- package/dist/UniversalNavBar.js +49 -46
- package/dist/UniversalNavBar.js.map +1 -1
- package/dist/UserProfile.d.ts.map +1 -1
- package/dist/UserProfile.js +30 -2
- package/dist/UserProfile.js.map +1 -1
- package/dist/barTheme.d.ts +46 -0
- package/dist/barTheme.d.ts.map +1 -0
- package/dist/barTheme.js +90 -0
- package/dist/barTheme.js.map +1 -0
- package/dist/desktop.d.ts +60 -0
- package/dist/desktop.d.ts.map +1 -0
- package/dist/desktop.js +73 -0
- package/dist/desktop.js.map +1 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +42 -0
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/menuTheme.js +1 -1
- package/dist/menuTheme.js.map +1 -1
- package/electron/handoff.cjs +202 -0
- package/package.json +5 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Hub handoff — the MAIN-PROCESS half of the desktop bridge.
|
|
2
|
+
//
|
|
3
|
+
// A Universal App's Electron shell sends every http(s) navigation to the
|
|
4
|
+
// system browser, which is right for arbitrary links and wrong for the hub:
|
|
5
|
+
// sign-in on desktop is the SDK's in-app dialog, so the session lives in this
|
|
6
|
+
// app and the browser has never seen it. "View profile" therefore landed a
|
|
7
|
+
// signed-in user on a signed-out page.
|
|
8
|
+
//
|
|
9
|
+
// This opens hub pages in a window the app owns, with the session already
|
|
10
|
+
// installed for the hub's origin — so the page is signed in before its first
|
|
11
|
+
// paint, and no credential is ever written into a URL.
|
|
12
|
+
//
|
|
13
|
+
// // main.cjs
|
|
14
|
+
// const { installHubHandoff } = require('@unisim/sdk/electron')
|
|
15
|
+
// installHubHandoff({ icon: ICON_PATH })
|
|
16
|
+
//
|
|
17
|
+
// ⚠️ The preload half CANNOT require this package. Electron sandboxes preload
|
|
18
|
+
// scripts by default (since 20), and a sandboxed preload's `require` resolves
|
|
19
|
+
// only a handful of built-ins — anything else throws at load and takes the
|
|
20
|
+
// whole bridge with it. Each app's own preload exposes the bridge in five
|
|
21
|
+
// lines using nothing but `require('electron')`; see PRELOAD_SNIPPET below.
|
|
22
|
+
//
|
|
23
|
+
// ⚠️ The window this opens gets NO preload and NO node integration. It renders
|
|
24
|
+
// a remote origin; handing it any bridge would hand the web page the app.
|
|
25
|
+
|
|
26
|
+
const { BrowserWindow, ipcMain, shell, session: electronSession } = require('electron')
|
|
27
|
+
|
|
28
|
+
/** IPC channels. Preloads must use these exact strings — see PRELOAD_SNIPPET. */
|
|
29
|
+
const CHANNEL_OPEN = 'unisim:open-hub'
|
|
30
|
+
const CHANNEL_CLEAR = 'unisim:clear-hub'
|
|
31
|
+
|
|
32
|
+
/** Must match the provider's `storageKey` and its cookie domain in the SDK. */
|
|
33
|
+
const SUITE_SESSION_KEY = 'universal-suite-auth'
|
|
34
|
+
const SUITE_COOKIE_DOMAIN = '.unisim.co.uk'
|
|
35
|
+
|
|
36
|
+
/** A year, matching what the SDK's own cookie storage writes. */
|
|
37
|
+
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 365
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The five lines an app's preload needs. Kept here so the channel names have
|
|
41
|
+
* exactly one home, even though the preload has to spell them out itself.
|
|
42
|
+
*/
|
|
43
|
+
const PRELOAD_SNIPPET = `
|
|
44
|
+
contextBridge.exposeInMainWorld('unisimDesktop', {
|
|
45
|
+
openHub: (url, session) => ipcRenderer.invoke('${CHANNEL_OPEN}', { url, session }),
|
|
46
|
+
clearHub: () => ipcRenderer.invoke('${CHANNEL_CLEAR}'),
|
|
47
|
+
})
|
|
48
|
+
`.trim()
|
|
49
|
+
|
|
50
|
+
function isSuiteUrl(url, cookieDomain) {
|
|
51
|
+
let parsed
|
|
52
|
+
try {
|
|
53
|
+
parsed = new URL(url)
|
|
54
|
+
} catch {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
if (parsed.protocol !== 'https:') return false
|
|
58
|
+
const bare = cookieDomain.replace(/^\./, '')
|
|
59
|
+
return parsed.hostname === bare || parsed.hostname.endsWith(`.${bare}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Wire up the hub-handoff IPC. Call once, after `app.whenReady()`.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} [options]
|
|
66
|
+
* @param {string} [options.cookieDomain] Zone the session cookie is scoped to.
|
|
67
|
+
* @param {string} [options.icon] Window icon path (Windows/Linux).
|
|
68
|
+
* @param {object} [options.windowOptions] Merged into the BrowserWindow opts.
|
|
69
|
+
* @returns {() => void} Removes the handlers again (tests, teardown).
|
|
70
|
+
*/
|
|
71
|
+
function installHubHandoff(options = {}) {
|
|
72
|
+
const {
|
|
73
|
+
cookieDomain = SUITE_COOKIE_DOMAIN,
|
|
74
|
+
icon,
|
|
75
|
+
windowOptions = {},
|
|
76
|
+
} = options
|
|
77
|
+
|
|
78
|
+
/** The one hub window, reused so repeat clicks don't stack windows up. */
|
|
79
|
+
let hubWindow = null
|
|
80
|
+
|
|
81
|
+
async function installSession(ses, url, session) {
|
|
82
|
+
const { origin } = new URL(url)
|
|
83
|
+
await ses.cookies.set({
|
|
84
|
+
url: origin,
|
|
85
|
+
name: SUITE_SESSION_KEY,
|
|
86
|
+
// The SDK's cookie storage percent-encodes the value it writes, and
|
|
87
|
+
// percent-decodes what it reads. Hand it back what it expects.
|
|
88
|
+
value: encodeURIComponent(session),
|
|
89
|
+
domain: cookieDomain,
|
|
90
|
+
path: '/',
|
|
91
|
+
secure: true,
|
|
92
|
+
sameSite: 'lax',
|
|
93
|
+
expirationDate: Math.floor(Date.now() / 1000) + SESSION_MAX_AGE_SECONDS,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
ipcMain.handle(CHANNEL_OPEN, async (_event, payload) => {
|
|
98
|
+
const url = payload && typeof payload.url === 'string' ? payload.url : ''
|
|
99
|
+
const session =
|
|
100
|
+
payload && typeof payload.session === 'string' && payload.session ? payload.session : null
|
|
101
|
+
|
|
102
|
+
// Anything that is not a suite page is an ordinary external link and keeps
|
|
103
|
+
// its ordinary behaviour. This is also the guard that stops a compromised
|
|
104
|
+
// renderer from asking us to plant the session cookie on someone else's
|
|
105
|
+
// origin: the cookie is only ever set for a URL that passes here.
|
|
106
|
+
if (!isSuiteUrl(url, cookieDomain)) {
|
|
107
|
+
await shell.openExternal(url)
|
|
108
|
+
return { ok: true, external: true }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Signed out (or a mock session that means nothing to the hub): there is
|
|
112
|
+
// nothing to hand over, so the browser is the better window — it may have
|
|
113
|
+
// a real session of its own, and it has the user's password manager.
|
|
114
|
+
if (!session) {
|
|
115
|
+
await shell.openExternal(url)
|
|
116
|
+
return { ok: true, external: true }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (hubWindow && !hubWindow.isDestroyed()) {
|
|
121
|
+
await installSession(hubWindow.webContents.session, url, session)
|
|
122
|
+
hubWindow.loadURL(url)
|
|
123
|
+
hubWindow.show()
|
|
124
|
+
hubWindow.focus()
|
|
125
|
+
return { ok: true }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const win = new BrowserWindow({
|
|
129
|
+
width: 1100,
|
|
130
|
+
height: 820,
|
|
131
|
+
// Painted only once the hub has rendered — the session is installed
|
|
132
|
+
// before the first load, so there is no signed-out flash to hide,
|
|
133
|
+
// but an empty white window while the page fetches still looks broken.
|
|
134
|
+
show: false,
|
|
135
|
+
title: 'Universal ID',
|
|
136
|
+
...(icon ? { icon } : {}),
|
|
137
|
+
...windowOptions,
|
|
138
|
+
webPreferences: {
|
|
139
|
+
contextIsolation: true,
|
|
140
|
+
nodeIntegration: false,
|
|
141
|
+
...(windowOptions.webPreferences || {}),
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
hubWindow = win
|
|
145
|
+
|
|
146
|
+
await installSession(win.webContents.session, url, session)
|
|
147
|
+
|
|
148
|
+
win.once('ready-to-show', () => win.show())
|
|
149
|
+
win.on('closed', () => {
|
|
150
|
+
if (hubWindow === win) hubWindow = null
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Links the hub opens in a new tab belong in the real browser — this
|
|
154
|
+
// window is an account panel, not a general-purpose browser.
|
|
155
|
+
win.webContents.setWindowOpenHandler(({ url: target }) => {
|
|
156
|
+
if (/^https?:\/\//.test(target)) {
|
|
157
|
+
void shell.openExternal(target)
|
|
158
|
+
return { action: 'deny' }
|
|
159
|
+
}
|
|
160
|
+
return { action: 'deny' }
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
await win.loadURL(url)
|
|
164
|
+
return { ok: true }
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return { ok: false, error: String((err && err.message) || err) }
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
ipcMain.handle(CHANNEL_CLEAR, async () => {
|
|
171
|
+
// Signing out of the app has to sign out the handed-over session too —
|
|
172
|
+
// otherwise the next "View profile" opens the account of the person who
|
|
173
|
+
// just left.
|
|
174
|
+
const ses = electronSession.defaultSession
|
|
175
|
+
const cookies = await ses.cookies.get({ name: SUITE_SESSION_KEY })
|
|
176
|
+
await Promise.all(
|
|
177
|
+
cookies.map((cookie) => {
|
|
178
|
+
const scheme = cookie.secure ? 'https' : 'http'
|
|
179
|
+
const host = cookie.domain && cookie.domain.startsWith('.')
|
|
180
|
+
? cookie.domain.slice(1)
|
|
181
|
+
: cookie.domain
|
|
182
|
+
return ses.cookies.remove(`${scheme}://${host}${cookie.path || '/'}`, cookie.name)
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
if (hubWindow && !hubWindow.isDestroyed()) hubWindow.close()
|
|
186
|
+
return { ok: true }
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
return function uninstall() {
|
|
190
|
+
ipcMain.removeHandler(CHANNEL_OPEN)
|
|
191
|
+
ipcMain.removeHandler(CHANNEL_CLEAR)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
installHubHandoff,
|
|
197
|
+
CHANNEL_OPEN,
|
|
198
|
+
CHANNEL_CLEAR,
|
|
199
|
+
SUITE_SESSION_KEY,
|
|
200
|
+
SUITE_COOKIE_DOMAIN,
|
|
201
|
+
PRELOAD_SNIPPET,
|
|
202
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unisim/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.112.0",
|
|
4
4
|
"description": "Shared React SDK for the Universal Suite — auth, entitlements, usage telemetry, changelog, org admin.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Universal Simulation Ltd",
|
|
@@ -32,10 +32,14 @@
|
|
|
32
32
|
".": {
|
|
33
33
|
"types": "./dist/index.d.ts",
|
|
34
34
|
"import": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./electron": {
|
|
37
|
+
"require": "./electron/handoff.cjs"
|
|
35
38
|
}
|
|
36
39
|
},
|
|
37
40
|
"files": [
|
|
38
41
|
"dist",
|
|
42
|
+
"electron",
|
|
39
43
|
"README.md"
|
|
40
44
|
],
|
|
41
45
|
"scripts": {
|