@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.
Files changed (46) hide show
  1. package/dist/ChangelogMenu.d.ts +17 -1
  2. package/dist/ChangelogMenu.d.ts.map +1 -1
  3. package/dist/ChangelogMenu.js +17 -3
  4. package/dist/ChangelogMenu.js.map +1 -1
  5. package/dist/CompanyMenu.d.ts +12 -1
  6. package/dist/CompanyMenu.d.ts.map +1 -1
  7. package/dist/CompanyMenu.js +25 -16
  8. package/dist/CompanyMenu.js.map +1 -1
  9. package/dist/KnowledgeBaseMenu.d.ts +11 -1
  10. package/dist/KnowledgeBaseMenu.d.ts.map +1 -1
  11. package/dist/KnowledgeBaseMenu.js +63 -33
  12. package/dist/KnowledgeBaseMenu.js.map +1 -1
  13. package/dist/ProfileDialog.d.ts +14 -0
  14. package/dist/ProfileDialog.d.ts.map +1 -0
  15. package/dist/ProfileDialog.js +250 -0
  16. package/dist/ProfileDialog.js.map +1 -0
  17. package/dist/UniversalAppsNavBar.d.ts +5 -38
  18. package/dist/UniversalAppsNavBar.d.ts.map +1 -1
  19. package/dist/UniversalAppsNavBar.js +6 -33
  20. package/dist/UniversalAppsNavBar.js.map +1 -1
  21. package/dist/UniversalNavBar.d.ts +24 -1
  22. package/dist/UniversalNavBar.d.ts.map +1 -1
  23. package/dist/UniversalNavBar.js +49 -46
  24. package/dist/UniversalNavBar.js.map +1 -1
  25. package/dist/UserProfile.d.ts.map +1 -1
  26. package/dist/UserProfile.js +30 -2
  27. package/dist/UserProfile.js.map +1 -1
  28. package/dist/barTheme.d.ts +46 -0
  29. package/dist/barTheme.d.ts.map +1 -0
  30. package/dist/barTheme.js +90 -0
  31. package/dist/barTheme.js.map +1 -0
  32. package/dist/desktop.d.ts +60 -0
  33. package/dist/desktop.d.ts.map +1 -0
  34. package/dist/desktop.js +73 -0
  35. package/dist/desktop.js.map +1 -0
  36. package/dist/i18n.d.ts.map +1 -1
  37. package/dist/i18n.js +42 -0
  38. package/dist/i18n.js.map +1 -1
  39. package/dist/index.d.ts +6 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +12 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/menuTheme.js +1 -1
  44. package/dist/menuTheme.js.map +1 -1
  45. package/electron/handoff.cjs +202 -0
  46. 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.110.1",
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": {