@unisim/sdk 0.122.2 → 0.123.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/Package.swift +24 -0
- package/README.md +44 -0
- package/UnisimSdk.podspec +28 -0
- package/dist/UniversalAppsNavBar.d.ts +7 -1
- package/dist/UniversalAppsNavBar.d.ts.map +1 -1
- package/dist/UniversalAppsNavBar.js +36 -2
- package/dist/UniversalAppsNavBar.js.map +1 -1
- package/dist/UserProfile.js +11 -3
- package/dist/UserProfile.js.map +1 -1
- package/dist/appMarks.d.ts +1 -1
- package/dist/appMarks.d.ts.map +1 -1
- package/dist/appMarks.js +20 -24
- package/dist/appMarks.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +22 -69
- package/dist/provider.js.map +1 -1
- package/dist/sessionStorage.d.ts +91 -0
- package/dist/sessionStorage.d.ts.map +1 -0
- package/dist/sessionStorage.js +239 -0
- package/dist/sessionStorage.js.map +1 -0
- package/ios/Sources/UnisimSuiteAuthPlugin/UnisimSuiteAuthPlugin.swift +241 -0
- package/package.json +12 -2
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape supabase-js wants for `auth.storage`. Every method may be async —
|
|
3
|
+
* the Keychain bridge is, and supabase-js awaits all three.
|
|
4
|
+
*/
|
|
5
|
+
export interface SessionStorageAdapter {
|
|
6
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
7
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
8
|
+
removeItem(key: string): void | Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The synchronous subset — what the cookie adapter actually is.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ Keep this distinction. The active-org and language stores read their
|
|
14
|
+
* value inside a `useState` INITIALISER, which cannot await; typing the cookie
|
|
15
|
+
* adapter as merely "possibly async" made those reads compile as
|
|
16
|
+
* `Promise<string> | string | null` and the org id would have been a pending
|
|
17
|
+
* promise stringified into a cookie. The compiler caught it; the runtime would
|
|
18
|
+
* not have said anything.
|
|
19
|
+
*/
|
|
20
|
+
export interface SyncSessionStorageAdapter {
|
|
21
|
+
getItem(key: string): string | null;
|
|
22
|
+
setItem(key: string, value: string): void;
|
|
23
|
+
removeItem(key: string): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* True inside a Capacitor container (iOS/Android), false in any browser.
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ Deliberately does NOT trust `cookieDomain` or the build mode to tell it
|
|
29
|
+
* apart. Every product computes `cookieDomain` from `import.meta.env.PROD`,
|
|
30
|
+
* which is true in the native bundle as well as the web one — that shared
|
|
31
|
+
* expression is exactly what made the native session storage silently wrong.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isNativeShell(): boolean;
|
|
34
|
+
interface SuiteAuthPlugin {
|
|
35
|
+
get(options: {
|
|
36
|
+
key: string;
|
|
37
|
+
}): Promise<{
|
|
38
|
+
value: string | null;
|
|
39
|
+
}>;
|
|
40
|
+
set(options: {
|
|
41
|
+
key: string;
|
|
42
|
+
value: string;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
remove(options: {
|
|
45
|
+
key: string;
|
|
46
|
+
}): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** True when this native build can reach the shared-Keychain plugin. */
|
|
49
|
+
export declare function hasSharedKeychain(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Keychain-backed storage, shared across every suite app signed by the same
|
|
52
|
+
* team. Async, which supabase-js supports.
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ On a plugin ERROR this falls back to localStorage for that call, so a
|
|
55
|
+
* Keychain hiccup degrades to an app-local session rather than signing the
|
|
56
|
+
* user out. A plugin returning `null` is NOT an error — it means "no session
|
|
57
|
+
* in the shared store", and must be reported as such, or a sign-out in one app
|
|
58
|
+
* would never be seen by the others.
|
|
59
|
+
*/
|
|
60
|
+
export declare function createSharedKeychainStorage(plugin: SuiteAuthPlugin): SessionStorageAdapter;
|
|
61
|
+
/**
|
|
62
|
+
* Cookie-backed storage — the load-bearing piece for cross-subdomain SSO.
|
|
63
|
+
* supabase-js defaults to localStorage which is origin-scoped, so a session
|
|
64
|
+
* created at app.unisim.co.uk wouldn't be visible to a product at
|
|
65
|
+
* assess.unisim.co.uk. Writing the session JSON into a cookie scoped to the
|
|
66
|
+
* parent zone (.unisim.co.uk) means every subdomain reads the same auth.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ Only usable from an origin that actually sits under `domain`. From
|
|
69
|
+
* anywhere else — `capacitor://localhost`, a dev server on localhost, a
|
|
70
|
+
* file:// Electron renderer — the browser rejects the write outright and every
|
|
71
|
+
* read returns null. `chooseSessionStorage()` is what keeps that from
|
|
72
|
+
* happening; don't reach for this adapter directly.
|
|
73
|
+
*/
|
|
74
|
+
export declare function createCookieStorage(domain: string): SyncSessionStorageAdapter;
|
|
75
|
+
export type SessionStorageKind = 'shared-keychain' | 'native-local' | 'shared-cookie' | 'local';
|
|
76
|
+
export interface ChosenSessionStorage {
|
|
77
|
+
kind: SessionStorageKind;
|
|
78
|
+
/** `undefined` means "let supabase-js use its own localStorage adapter". */
|
|
79
|
+
storage: SessionStorageAdapter | undefined;
|
|
80
|
+
/** Whether sibling suite state (active org) may ride the same cookie. */
|
|
81
|
+
cookiesUsable: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Pick the session store for the platform this code is running on.
|
|
85
|
+
*
|
|
86
|
+
* The native check comes FIRST and overrides `cookieDomain`, because every
|
|
87
|
+
* native build sets `cookieDomain` and none of them can use it.
|
|
88
|
+
*/
|
|
89
|
+
export declare function chooseSessionStorage(cookieDomain?: string): ChosenSessionStorage;
|
|
90
|
+
export {};
|
|
91
|
+
//# sourceMappingURL=sessionStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionStorage.d.ts","sourceRoot":"","sources":["../src/sessionStorage.ts"],"names":[],"mappings":"AAoCA;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC5D,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9C;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACnC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACzC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAgBD;;;;;;;GAOG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAcvC;AAID,UAAU,eAAe;IACvB,GAAG,CAAC,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAA;IAChE,GAAG,CAAC,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3D,MAAM,CAAC,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAChD;AAOD,wEAAwE;AACxE,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAQD;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,eAAe,GAAG,qBAAqB,CAmD1F;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,yBAAyB,CA0C7E;AAID,MAAM,MAAM,kBAAkB,GAC1B,iBAAiB,GACjB,cAAc,GACd,eAAe,GACf,OAAO,CAAA;AAEX,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,kBAAkB,CAAA;IACxB,4EAA4E;IAC5E,OAAO,EAAE,qBAAqB,GAAG,SAAS,CAAA;IAC1C,yEAAyE;IACzE,aAAa,EAAE,OAAO,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,oBAAoB,CAsBhF"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Where the Supabase session lives, per platform.
|
|
3
|
+
//
|
|
4
|
+
// supabase-js keeps the session in whatever `auth.storage` it is handed. The
|
|
5
|
+
// suite needs three different answers, and getting the wrong one is silent
|
|
6
|
+
// every time — a session that fails to persist looks exactly like a session
|
|
7
|
+
// that was never created:
|
|
8
|
+
//
|
|
9
|
+
// • **Browser, production** — a cookie scoped to the parent zone
|
|
10
|
+
// (.unisim.co.uk), so app.unisim.co.uk and assess.unisim.co.uk read the
|
|
11
|
+
// same auth. This is the cross-subdomain SSO mechanism and predates this
|
|
12
|
+
// module; it moved here unchanged.
|
|
13
|
+
//
|
|
14
|
+
// • **Browser, local dev / Electron** — localStorage. There is no parent
|
|
15
|
+
// zone to scope a cookie to on localhost or file://.
|
|
16
|
+
//
|
|
17
|
+
// • **Native (Capacitor)** — the iOS Keychain, in a group shared by every
|
|
18
|
+
// suite app, via the UnisimSuiteAuth plugin. Falls back to localStorage
|
|
19
|
+
// when the plugin is absent.
|
|
20
|
+
//
|
|
21
|
+
// ⚠️ **The native case is why this module exists.** A Capacitor app runs at
|
|
22
|
+
// `capacitor://localhost`, and a cookie written from there with
|
|
23
|
+
// `Domain=.unisim.co.uk` is REJECTED by the cookie domain-match rule — the
|
|
24
|
+
// write silently does nothing and the read comes back null. Every native build
|
|
25
|
+
// sets `cookieDomain` (it is gated on `import.meta.env.PROD`, which is true in
|
|
26
|
+
// the native bundle), so until this module landed **the native apps could not
|
|
27
|
+
// persist a session at all**: sign in, force-quit, and you were signed out
|
|
28
|
+
// again, with nothing logged. `sessionStorage.browser.test.mjs` pins that
|
|
29
|
+
// behaviour so it cannot come back.
|
|
30
|
+
//
|
|
31
|
+
// The shared Keychain then buys the thing that was actually asked for: sign in
|
|
32
|
+
// on Universal Images on the phone and Universal PDF is already signed in.
|
|
33
|
+
// Sign-out is suite-wide too, which matches how the shared cookie already
|
|
34
|
+
// behaves on the web.
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
function capacitorGlobal() {
|
|
37
|
+
if (typeof window === 'undefined')
|
|
38
|
+
return null;
|
|
39
|
+
const c = window.Capacitor;
|
|
40
|
+
return c ?? null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* True inside a Capacitor container (iOS/Android), false in any browser.
|
|
44
|
+
*
|
|
45
|
+
* ⚠️ Deliberately does NOT trust `cookieDomain` or the build mode to tell it
|
|
46
|
+
* apart. Every product computes `cookieDomain` from `import.meta.env.PROD`,
|
|
47
|
+
* which is true in the native bundle as well as the web one — that shared
|
|
48
|
+
* expression is exactly what made the native session storage silently wrong.
|
|
49
|
+
*/
|
|
50
|
+
export function isNativeShell() {
|
|
51
|
+
const c = capacitorGlobal();
|
|
52
|
+
if (c && typeof c.isNativePlatform === 'function') {
|
|
53
|
+
try {
|
|
54
|
+
return c.isNativePlatform();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* fall through to the protocol check */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// A partially-initialised bridge still serves the app from the container's
|
|
61
|
+
// own scheme, which no browser uses.
|
|
62
|
+
if (typeof window === 'undefined')
|
|
63
|
+
return false;
|
|
64
|
+
const protocol = window.location?.protocol;
|
|
65
|
+
return protocol === 'capacitor:' || protocol === 'ionic:';
|
|
66
|
+
}
|
|
67
|
+
function suiteAuthPlugin() {
|
|
68
|
+
const plugin = capacitorGlobal()?.Plugins?.UnisimSuiteAuth;
|
|
69
|
+
return plugin ? plugin : null;
|
|
70
|
+
}
|
|
71
|
+
/** True when this native build can reach the shared-Keychain plugin. */
|
|
72
|
+
export function hasSharedKeychain() {
|
|
73
|
+
return suiteAuthPlugin() !== null;
|
|
74
|
+
}
|
|
75
|
+
function localStorageAdapter() {
|
|
76
|
+
// `undefined` lets supabase-js install its own localStorage adapter, which
|
|
77
|
+
// already handles the private-mode / storage-disabled throw.
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Keychain-backed storage, shared across every suite app signed by the same
|
|
82
|
+
* team. Async, which supabase-js supports.
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ On a plugin ERROR this falls back to localStorage for that call, so a
|
|
85
|
+
* Keychain hiccup degrades to an app-local session rather than signing the
|
|
86
|
+
* user out. A plugin returning `null` is NOT an error — it means "no session
|
|
87
|
+
* in the shared store", and must be reported as such, or a sign-out in one app
|
|
88
|
+
* would never be seen by the others.
|
|
89
|
+
*/
|
|
90
|
+
export function createSharedKeychainStorage(plugin) {
|
|
91
|
+
let warned = false;
|
|
92
|
+
const warn = (op, err) => {
|
|
93
|
+
if (warned)
|
|
94
|
+
return;
|
|
95
|
+
warned = true;
|
|
96
|
+
console.warn(`[unisim/sdk] shared Keychain ${op} failed; falling back to app-local storage. ` +
|
|
97
|
+
'Sign-in will not carry across the suite apps on this device.', err);
|
|
98
|
+
};
|
|
99
|
+
const local = {
|
|
100
|
+
get: (key) => {
|
|
101
|
+
try {
|
|
102
|
+
return window.localStorage.getItem(key);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
set: (key, value) => {
|
|
109
|
+
try {
|
|
110
|
+
window.localStorage.setItem(key, value);
|
|
111
|
+
}
|
|
112
|
+
catch { /* private mode */ }
|
|
113
|
+
},
|
|
114
|
+
remove: (key) => {
|
|
115
|
+
try {
|
|
116
|
+
window.localStorage.removeItem(key);
|
|
117
|
+
}
|
|
118
|
+
catch { /* private mode */ }
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
async getItem(key) {
|
|
123
|
+
try {
|
|
124
|
+
const { value } = await plugin.get({ key });
|
|
125
|
+
return value ?? null;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
warn('read', err);
|
|
129
|
+
return local.get(key);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
async setItem(key, value) {
|
|
133
|
+
try {
|
|
134
|
+
await plugin.set({ key, value });
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
warn('write', err);
|
|
138
|
+
local.set(key, value);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async removeItem(key) {
|
|
142
|
+
try {
|
|
143
|
+
await plugin.remove({ key });
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
warn('delete', err);
|
|
147
|
+
local.remove(key);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// ── Browser: the cross-subdomain cookie ──────────────────────────────────────
|
|
153
|
+
/**
|
|
154
|
+
* Cookie-backed storage — the load-bearing piece for cross-subdomain SSO.
|
|
155
|
+
* supabase-js defaults to localStorage which is origin-scoped, so a session
|
|
156
|
+
* created at app.unisim.co.uk wouldn't be visible to a product at
|
|
157
|
+
* assess.unisim.co.uk. Writing the session JSON into a cookie scoped to the
|
|
158
|
+
* parent zone (.unisim.co.uk) means every subdomain reads the same auth.
|
|
159
|
+
*
|
|
160
|
+
* ⚠️ Only usable from an origin that actually sits under `domain`. From
|
|
161
|
+
* anywhere else — `capacitor://localhost`, a dev server on localhost, a
|
|
162
|
+
* file:// Electron renderer — the browser rejects the write outright and every
|
|
163
|
+
* read returns null. `chooseSessionStorage()` is what keeps that from
|
|
164
|
+
* happening; don't reach for this adapter directly.
|
|
165
|
+
*/
|
|
166
|
+
export function createCookieStorage(domain) {
|
|
167
|
+
const secure = typeof window !== 'undefined' && window.location.protocol === 'https:';
|
|
168
|
+
return {
|
|
169
|
+
getItem(key) {
|
|
170
|
+
if (typeof document === 'undefined')
|
|
171
|
+
return null;
|
|
172
|
+
const encoded = encodeURIComponent(key);
|
|
173
|
+
for (const cookie of document.cookie.split('; ')) {
|
|
174
|
+
const eq = cookie.indexOf('=');
|
|
175
|
+
const name = eq === -1 ? cookie : cookie.slice(0, eq);
|
|
176
|
+
if (name === encoded) {
|
|
177
|
+
return eq === -1 ? '' : decodeURIComponent(cookie.slice(eq + 1));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
},
|
|
182
|
+
setItem(key, value) {
|
|
183
|
+
if (typeof document === 'undefined')
|
|
184
|
+
return;
|
|
185
|
+
const parts = [
|
|
186
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
|
|
187
|
+
'Path=/',
|
|
188
|
+
`Domain=${domain}`,
|
|
189
|
+
`Max-Age=${60 * 60 * 24 * 365}`, // 1 year
|
|
190
|
+
'SameSite=Lax',
|
|
191
|
+
];
|
|
192
|
+
if (secure)
|
|
193
|
+
parts.push('Secure');
|
|
194
|
+
document.cookie = parts.join('; ');
|
|
195
|
+
},
|
|
196
|
+
removeItem(key) {
|
|
197
|
+
if (typeof document === 'undefined')
|
|
198
|
+
return;
|
|
199
|
+
const parts = [
|
|
200
|
+
`${encodeURIComponent(key)}=`,
|
|
201
|
+
'Path=/',
|
|
202
|
+
`Domain=${domain}`,
|
|
203
|
+
'Max-Age=0',
|
|
204
|
+
'SameSite=Lax',
|
|
205
|
+
];
|
|
206
|
+
if (secure)
|
|
207
|
+
parts.push('Secure');
|
|
208
|
+
document.cookie = parts.join('; ');
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Pick the session store for the platform this code is running on.
|
|
214
|
+
*
|
|
215
|
+
* The native check comes FIRST and overrides `cookieDomain`, because every
|
|
216
|
+
* native build sets `cookieDomain` and none of them can use it.
|
|
217
|
+
*/
|
|
218
|
+
export function chooseSessionStorage(cookieDomain) {
|
|
219
|
+
if (isNativeShell()) {
|
|
220
|
+
const plugin = suiteAuthPlugin();
|
|
221
|
+
if (plugin) {
|
|
222
|
+
return {
|
|
223
|
+
kind: 'shared-keychain',
|
|
224
|
+
storage: createSharedKeychainStorage(plugin),
|
|
225
|
+
cookiesUsable: false,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return { kind: 'native-local', storage: localStorageAdapter(), cookiesUsable: false };
|
|
229
|
+
}
|
|
230
|
+
if (cookieDomain) {
|
|
231
|
+
return {
|
|
232
|
+
kind: 'shared-cookie',
|
|
233
|
+
storage: createCookieStorage(cookieDomain),
|
|
234
|
+
cookiesUsable: true,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return { kind: 'local', storage: localStorageAdapter(), cookiesUsable: false };
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=sessionStorage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionStorage.js","sourceRoot":"","sources":["../src/sessionStorage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import Security
|
|
4
|
+
|
|
5
|
+
/// Stores the Universal Suite session in the iOS Keychain, in a group shared by
|
|
6
|
+
/// every suite app signed by the same team — so signing in on Universal Images
|
|
7
|
+
/// leaves you signed in when you open Universal PDF.
|
|
8
|
+
///
|
|
9
|
+
/// It exists because a Capacitor app runs at `capacitor://localhost`, where the
|
|
10
|
+
/// suite's cross-subdomain auth cookie cannot be written at all: the browser
|
|
11
|
+
/// rejects `Domain=.unisim.co.uk` from an origin outside that zone. Before this
|
|
12
|
+
/// plugin the native builds could not persist a session even to THEMSELVES —
|
|
13
|
+
/// sign in, force-quit, signed out again, nothing logged. See
|
|
14
|
+
/// `src/sessionStorage.ts` and its browser test.
|
|
15
|
+
///
|
|
16
|
+
/// ⚠️ **Sharing needs an entitlement the plugin cannot add for you.** The host
|
|
17
|
+
/// app must carry an `.entitlements` file declaring the shared group:
|
|
18
|
+
///
|
|
19
|
+
/// <key>keychain-access-groups</key>
|
|
20
|
+
/// <array><string>$(AppIdentifierPrefix)co.uk.unisim.suite</string></array>
|
|
21
|
+
///
|
|
22
|
+
/// and reference it from `CODE_SIGN_ENTITLEMENTS` in the Xcode project. No
|
|
23
|
+
/// `capacitor.config.json` change is needed — the team prefix is resolved at
|
|
24
|
+
/// runtime. An app with NO entitlements file cannot use the Keychain at all:
|
|
25
|
+
/// every call comes back `errSecMissingEntitlement` (-34018), which is also
|
|
26
|
+
/// what an unsigned simulator build gets.
|
|
27
|
+
///
|
|
28
|
+
/// Without a resolvable group the plugin still stores the session, app-locally:
|
|
29
|
+
/// it persists across launches but does NOT carry to the other suite apps.
|
|
30
|
+
/// `status()` reports which of the two you actually got, because nothing about
|
|
31
|
+
/// the behaviour makes that visible from the outside.
|
|
32
|
+
@objc(UnisimSuiteAuthPlugin)
|
|
33
|
+
public class UnisimSuiteAuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
34
|
+
public let identifier = "UnisimSuiteAuthPlugin"
|
|
35
|
+
public let jsName = "UnisimSuiteAuth"
|
|
36
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
37
|
+
CAPPluginMethod(name: "get", returnType: CAPPluginReturnPromise),
|
|
38
|
+
CAPPluginMethod(name: "set", returnType: CAPPluginReturnPromise),
|
|
39
|
+
CAPPluginMethod(name: "remove", returnType: CAPPluginReturnPromise),
|
|
40
|
+
CAPPluginMethod(name: "status", returnType: CAPPluginReturnPromise)
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
/// One service for the whole suite. The `key` the JS side passes (Supabase's
|
|
44
|
+
/// `storageKey`, i.e. `universal-suite-auth`) becomes the account, so other
|
|
45
|
+
/// suite keys can share the same group later without colliding.
|
|
46
|
+
private static let service = "co.uk.unisim.suite.auth"
|
|
47
|
+
|
|
48
|
+
/// The group every suite app shares. Prefixed with the team's app-identifier
|
|
49
|
+
/// prefix at runtime — see `appIdentifierPrefix()`.
|
|
50
|
+
private static let defaultGroupSuffix = "co.uk.unisim.suite"
|
|
51
|
+
|
|
52
|
+
private static var cachedPrefix: String??
|
|
53
|
+
|
|
54
|
+
/// The team prefix iOS stamps on this app's keychain items, e.g. the
|
|
55
|
+
/// `ABCDE12345` in `ABCDE12345.co.uk.unisim.pdf`.
|
|
56
|
+
///
|
|
57
|
+
/// ⚠️ Resolved at RUNTIME rather than hardcoded, and that is deliberate: the
|
|
58
|
+
/// entitlement is written as `$(AppIdentifierPrefix)co.uk.unisim.suite`, and
|
|
59
|
+
/// `kSecAttrAccessGroup` will only accept the expanded form. Baking a team
|
|
60
|
+
/// ID into the config of thirteen apps means thirteen places to be wrong,
|
|
61
|
+
/// and a wrong one fails as `errSecMissingEntitlement` at sign-in — far from
|
|
62
|
+
/// where the mistake was made.
|
|
63
|
+
///
|
|
64
|
+
/// The trick is the documented one: write an item with NO access group, and
|
|
65
|
+
/// read back the group iOS assigned it, which is always
|
|
66
|
+
/// `<prefix>.<bundle id>`.
|
|
67
|
+
private static func appIdentifierPrefix() -> String? {
|
|
68
|
+
if let cached = cachedPrefix { return cached }
|
|
69
|
+
|
|
70
|
+
let base: [String: Any] = [
|
|
71
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
72
|
+
kSecAttrService as String: service,
|
|
73
|
+
kSecAttrAccount as String: "unisim.suite.prefix-probe"
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
var query = base
|
|
77
|
+
query[kSecReturnAttributes as String] = true
|
|
78
|
+
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
79
|
+
|
|
80
|
+
var item: CFTypeRef?
|
|
81
|
+
var status = SecItemCopyMatching(query as CFDictionary, &item)
|
|
82
|
+
if status == errSecItemNotFound {
|
|
83
|
+
var insert = base
|
|
84
|
+
insert[kSecValueData as String] = Data()
|
|
85
|
+
insert[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
|
86
|
+
insert[kSecReturnAttributes as String] = true
|
|
87
|
+
status = SecItemAdd(insert as CFDictionary, &item)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
guard status == errSecSuccess,
|
|
91
|
+
let attributes = item as? [String: Any],
|
|
92
|
+
let group = attributes[kSecAttrAccessGroup as String] as? String,
|
|
93
|
+
let dot = group.firstIndex(of: ".") else {
|
|
94
|
+
cachedPrefix = .some(nil)
|
|
95
|
+
return nil
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let prefix = String(group[group.startIndex..<dot])
|
|
99
|
+
cachedPrefix = .some(prefix)
|
|
100
|
+
return prefix
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// The group to store under, or nil to fall back to an app-local item.
|
|
104
|
+
private var accessGroup: String? {
|
|
105
|
+
// An explicit group wins — an escape hatch for an app that needs to sit
|
|
106
|
+
// outside the suite group, and the only way to override the runtime
|
|
107
|
+
// resolution if it ever gets this wrong.
|
|
108
|
+
if let explicit = getConfig().getString("accessGroup"), !explicit.isEmpty {
|
|
109
|
+
return explicit
|
|
110
|
+
}
|
|
111
|
+
let suffix = getConfig().getString("accessGroupSuffix") ?? Self.defaultGroupSuffix
|
|
112
|
+
guard let prefix = Self.appIdentifierPrefix() else { return nil }
|
|
113
|
+
return "\(prefix).\(suffix)"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func baseQuery(for key: String) -> [String: Any] {
|
|
117
|
+
var query: [String: Any] = [
|
|
118
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
119
|
+
kSecAttrService as String: Self.service,
|
|
120
|
+
kSecAttrAccount as String: key
|
|
121
|
+
]
|
|
122
|
+
if let group = accessGroup {
|
|
123
|
+
query[kSecAttrAccessGroup as String] = group
|
|
124
|
+
}
|
|
125
|
+
return query
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// ⚠️ Put the OSStatus in the MESSAGE, not just the code. Every one of these
|
|
129
|
+
/// failures is indistinguishable from the JS side otherwise — "Keychain
|
|
130
|
+
/// update failed" was all that came back from the first simulator run, and
|
|
131
|
+
/// the number was the only thing that would have identified it.
|
|
132
|
+
///
|
|
133
|
+
/// The one worth recognising is **-34018 `errSecMissingEntitlement`**, which
|
|
134
|
+
/// means the process has no keychain entitlement to work with at all. Two
|
|
135
|
+
/// ways to get there: an app built with code signing disabled (an unsigned
|
|
136
|
+
/// simulator build has no `application-identifier`, so the Keychain refuses
|
|
137
|
+
/// everything), or a configured `accessGroup` that is not listed in the
|
|
138
|
+
/// app's `keychain-access-groups` — including one missing the team prefix.
|
|
139
|
+
private static func describe(_ operation: String, _ status: OSStatus) -> String {
|
|
140
|
+
let detail = SecCopyErrorMessageString(status, nil) as String? ?? "unknown error"
|
|
141
|
+
let hint = status == errSecMissingEntitlement
|
|
142
|
+
? " — the app has no usable keychain entitlement (unsigned build, or accessGroup not in keychain-access-groups)"
|
|
143
|
+
: ""
|
|
144
|
+
return "Keychain \(operation) failed: OSStatus \(status) (\(detail))\(hint)"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@objc func status(_ call: CAPPluginCall) {
|
|
148
|
+
let group = accessGroup
|
|
149
|
+
call.resolve([
|
|
150
|
+
"shared": group != nil,
|
|
151
|
+
"accessGroup": group ?? NSNull(),
|
|
152
|
+
"appIdentifierPrefix": Self.appIdentifierPrefix() ?? NSNull()
|
|
153
|
+
])
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@objc func get(_ call: CAPPluginCall) {
|
|
157
|
+
guard let key = call.getString("key") else {
|
|
158
|
+
call.reject("key is required")
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
var query = baseQuery(for: key)
|
|
163
|
+
query[kSecReturnData as String] = true
|
|
164
|
+
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
165
|
+
|
|
166
|
+
var item: CFTypeRef?
|
|
167
|
+
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
|
168
|
+
|
|
169
|
+
switch status {
|
|
170
|
+
case errSecSuccess:
|
|
171
|
+
guard let data = item as? Data, let value = String(data: data, encoding: .utf8) else {
|
|
172
|
+
// Present but unreadable — treat as absent rather than throwing,
|
|
173
|
+
// so a corrupt item shows as "signed out" and can be overwritten
|
|
174
|
+
// by the next sign-in instead of wedging the app.
|
|
175
|
+
call.resolve(["value": NSNull()])
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
call.resolve(["value": value])
|
|
179
|
+
case errSecItemNotFound:
|
|
180
|
+
// ⚠️ NOT an error, and the JS side must not treat it as one. "No
|
|
181
|
+
// session in the shared store" is exactly what another suite app
|
|
182
|
+
// signing out looks like.
|
|
183
|
+
call.resolve(["value": NSNull()])
|
|
184
|
+
default:
|
|
185
|
+
call.reject(Self.describe("read", status), String(status))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@objc func set(_ call: CAPPluginCall) {
|
|
190
|
+
guard let key = call.getString("key") else {
|
|
191
|
+
call.reject("key is required")
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
guard let value = call.getString("value"), let data = value.data(using: .utf8) else {
|
|
195
|
+
call.reject("value is required")
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let query = baseQuery(for: key)
|
|
200
|
+
let attributes: [String: Any] = [
|
|
201
|
+
kSecValueData as String: data,
|
|
202
|
+
// Readable once the device has been unlocked after a reboot, and
|
|
203
|
+
// never synced to iCloud — a bearer token has no business on
|
|
204
|
+
// another device.
|
|
205
|
+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
|
|
209
|
+
if updateStatus == errSecSuccess {
|
|
210
|
+
call.resolve()
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
if updateStatus != errSecItemNotFound {
|
|
214
|
+
call.reject(Self.describe("update", updateStatus), String(updateStatus))
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
var insert = query
|
|
219
|
+
insert.merge(attributes) { current, _ in current }
|
|
220
|
+
let addStatus = SecItemAdd(insert as CFDictionary, nil)
|
|
221
|
+
if addStatus == errSecSuccess {
|
|
222
|
+
call.resolve()
|
|
223
|
+
} else {
|
|
224
|
+
call.reject(Self.describe("write", addStatus), String(addStatus))
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@objc func remove(_ call: CAPPluginCall) {
|
|
229
|
+
guard let key = call.getString("key") else {
|
|
230
|
+
call.reject("key is required")
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let status = SecItemDelete(baseQuery(for: key) as CFDictionary)
|
|
235
|
+
if status == errSecSuccess || status == errSecItemNotFound {
|
|
236
|
+
call.resolve()
|
|
237
|
+
} else {
|
|
238
|
+
call.reject(Self.describe("delete", status), String(status))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unisim/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared React SDK for the Universal Suite
|
|
3
|
+
"version": "0.123.0",
|
|
4
|
+
"description": "Shared React SDK for the Universal Suite \u2014 auth, entitlements, usage telemetry, changelog, org admin.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Universal Simulation Ltd",
|
|
7
7
|
"keywords": [
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"module": "./dist/index.js",
|
|
29
29
|
"types": "./dist/index.d.ts",
|
|
30
30
|
"sideEffects": false,
|
|
31
|
+
"capacitor": {
|
|
32
|
+
"ios": {
|
|
33
|
+
"src": "ios"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
31
36
|
"exports": {
|
|
32
37
|
".": {
|
|
33
38
|
"types": "./dist/index.d.ts",
|
|
@@ -40,12 +45,16 @@
|
|
|
40
45
|
"files": [
|
|
41
46
|
"dist",
|
|
42
47
|
"electron",
|
|
48
|
+
"ios/Sources",
|
|
49
|
+
"Package.swift",
|
|
50
|
+
"UnisimSdk.podspec",
|
|
43
51
|
"README.md"
|
|
44
52
|
],
|
|
45
53
|
"scripts": {
|
|
46
54
|
"build": "tsc -p tsconfig.build.json",
|
|
47
55
|
"dev": "tsc -p tsconfig.build.json --watch",
|
|
48
56
|
"typecheck": "tsc --noEmit",
|
|
57
|
+
"test:session-storage": "npm run build && node tests/session-storage.browser.mjs",
|
|
49
58
|
"prepublishOnly": "npm run typecheck && npm run build"
|
|
50
59
|
},
|
|
51
60
|
"dependencies": {
|
|
@@ -62,6 +71,7 @@
|
|
|
62
71
|
"@types/react": "^18.3.0",
|
|
63
72
|
"@types/react-dom": "^18.3.7",
|
|
64
73
|
"pdf-lib": "^1.17.1",
|
|
74
|
+
"playwright": "^1.62.1",
|
|
65
75
|
"react": "^18.3.0",
|
|
66
76
|
"react-dom": "^18.3.1",
|
|
67
77
|
"typescript": "^5.7.2"
|