@oxyhq/core 12.10.6 → 12.11.1
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/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +66 -6
- package/dist/cjs/boot/sessionColdBoot.js +142 -37
- package/dist/cjs/i18n/locales/ar-SA.json +13 -1
- package/dist/cjs/i18n/locales/ca-ES.json +13 -1
- package/dist/cjs/i18n/locales/de-DE.json +13 -1
- package/dist/cjs/i18n/locales/en-US.json +13 -4
- package/dist/cjs/i18n/locales/es-ES.json +13 -4
- package/dist/cjs/i18n/locales/fr-FR.json +13 -1
- package/dist/cjs/i18n/locales/it-IT.json +13 -1
- package/dist/cjs/i18n/locales/ja-JP.json +13 -1
- package/dist/cjs/i18n/locales/ko-KR.json +13 -1
- package/dist/cjs/i18n/locales/locales/ar-SA.json +13 -1
- package/dist/cjs/i18n/locales/locales/ca-ES.json +13 -1
- package/dist/cjs/i18n/locales/locales/de-DE.json +13 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +13 -4
- package/dist/cjs/i18n/locales/locales/es-ES.json +13 -4
- package/dist/cjs/i18n/locales/locales/fr-FR.json +13 -1
- package/dist/cjs/i18n/locales/locales/it-IT.json +13 -1
- package/dist/cjs/i18n/locales/locales/ja-JP.json +13 -1
- package/dist/cjs/i18n/locales/locales/ko-KR.json +13 -1
- package/dist/cjs/i18n/locales/locales/pt-PT.json +13 -1
- package/dist/cjs/i18n/locales/locales/zh-CN.json +13 -1
- package/dist/cjs/i18n/locales/pt-PT.json +13 -1
- package/dist/cjs/i18n/locales/zh-CN.json +13 -1
- package/dist/cjs/index.js +31 -5
- package/dist/cjs/mixins/OxyServices.auth.js +257 -17
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +54 -3
- package/dist/cjs/mixins/OxyServices.notifications.js +63 -0
- package/dist/cjs/mixins/OxyServices.privacy.js +6 -0
- package/dist/cjs/mixins/OxyServices.user.js +1 -0
- package/dist/cjs/mixins/index.js +5 -0
- package/dist/cjs/session/SessionClient.js +40 -4
- package/dist/cjs/session/accountDialogController.js +288 -41
- package/dist/cjs/session/identityPin.js +256 -0
- package/dist/cjs/session/identitySession.js +135 -0
- package/dist/cjs/session/projectSessionState.js +54 -15
- package/dist/cjs/session/refresh.js +73 -8
- package/dist/cjs/utils/commonsDelivery.js +59 -0
- package/dist/cjs/utils/oauthPkce.js +33 -3
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +66 -6
- package/dist/esm/boot/sessionColdBoot.js +142 -37
- package/dist/esm/i18n/locales/ar-SA.json +13 -1
- package/dist/esm/i18n/locales/ca-ES.json +13 -1
- package/dist/esm/i18n/locales/de-DE.json +13 -1
- package/dist/esm/i18n/locales/en-US.json +13 -4
- package/dist/esm/i18n/locales/es-ES.json +13 -4
- package/dist/esm/i18n/locales/fr-FR.json +13 -1
- package/dist/esm/i18n/locales/it-IT.json +13 -1
- package/dist/esm/i18n/locales/ja-JP.json +13 -1
- package/dist/esm/i18n/locales/ko-KR.json +13 -1
- package/dist/esm/i18n/locales/locales/ar-SA.json +13 -1
- package/dist/esm/i18n/locales/locales/ca-ES.json +13 -1
- package/dist/esm/i18n/locales/locales/de-DE.json +13 -1
- package/dist/esm/i18n/locales/locales/en-US.json +13 -4
- package/dist/esm/i18n/locales/locales/es-ES.json +13 -4
- package/dist/esm/i18n/locales/locales/fr-FR.json +13 -1
- package/dist/esm/i18n/locales/locales/it-IT.json +13 -1
- package/dist/esm/i18n/locales/locales/ja-JP.json +13 -1
- package/dist/esm/i18n/locales/locales/ko-KR.json +13 -1
- package/dist/esm/i18n/locales/locales/pt-PT.json +13 -1
- package/dist/esm/i18n/locales/locales/zh-CN.json +13 -1
- package/dist/esm/i18n/locales/pt-PT.json +13 -1
- package/dist/esm/i18n/locales/zh-CN.json +13 -1
- package/dist/esm/index.js +14 -1
- package/dist/esm/mixins/OxyServices.auth.js +257 -17
- package/dist/esm/mixins/OxyServices.deviceBoot.js +52 -3
- package/dist/esm/mixins/OxyServices.notifications.js +60 -0
- package/dist/esm/mixins/OxyServices.privacy.js +6 -0
- package/dist/esm/mixins/OxyServices.user.js +1 -0
- package/dist/esm/mixins/index.js +5 -0
- package/dist/esm/session/SessionClient.js +40 -4
- package/dist/esm/session/accountDialogController.js +288 -41
- package/dist/esm/session/identityPin.js +249 -0
- package/dist/esm/session/identitySession.js +131 -0
- package/dist/esm/session/projectSessionState.js +54 -15
- package/dist/esm/session/refresh.js +73 -8
- package/dist/esm/utils/commonsDelivery.js +54 -0
- package/dist/esm/utils/oauthPkce.js +31 -2
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +18 -1
- package/dist/types/boot/sessionColdBoot.d.ts +38 -1
- package/dist/types/index.d.ts +12 -4
- package/dist/types/mixins/OxyServices.auth.d.ts +262 -11
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +31 -1
- package/dist/types/mixins/OxyServices.notifications.d.ts +143 -0
- package/dist/types/mixins/OxyServices.user.d.ts +2 -0
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/session/SessionClient.d.ts +28 -0
- package/dist/types/session/accountDialogController.d.ts +187 -9
- package/dist/types/session/identityPin.d.ts +87 -0
- package/dist/types/session/identitySession.d.ts +105 -0
- package/dist/types/session/projectSessionState.d.ts +21 -19
- package/dist/types/session/refresh.d.ts +32 -1
- package/dist/types/utils/commonsDelivery.d.ts +105 -0
- package/dist/types/utils/oauthPkce.d.ts +21 -1
- package/package.json +3 -3
- package/src/HttpService.ts +67 -6
- package/src/__tests__/inSessionRefresh.test.ts +67 -0
- package/src/boot/__tests__/sessionColdBoot.identity.test.ts +393 -0
- package/src/boot/sessionColdBoot.ts +183 -37
- package/src/i18n/locales/ar-SA.json +13 -1
- package/src/i18n/locales/ca-ES.json +13 -1
- package/src/i18n/locales/de-DE.json +13 -1
- package/src/i18n/locales/en-US.json +13 -4
- package/src/i18n/locales/es-ES.json +13 -4
- package/src/i18n/locales/fr-FR.json +13 -1
- package/src/i18n/locales/it-IT.json +13 -1
- package/src/i18n/locales/ja-JP.json +13 -1
- package/src/i18n/locales/ko-KR.json +13 -1
- package/src/i18n/locales/pt-PT.json +13 -1
- package/src/i18n/locales/zh-CN.json +13 -1
- package/src/index.ts +49 -0
- package/src/mixins/OxyServices.auth.ts +464 -24
- package/src/mixins/OxyServices.deviceBoot.ts +54 -2
- package/src/mixins/OxyServices.notifications.ts +142 -0
- package/src/mixins/OxyServices.privacy.ts +6 -0
- package/src/mixins/OxyServices.user.ts +3 -0
- package/src/mixins/__tests__/commonsSignIn.test.ts +754 -2
- package/src/mixins/__tests__/deviceTokenMintPinned.test.ts +113 -0
- package/src/mixins/__tests__/preSessionSkipAuth.test.ts +18 -0
- package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +2 -0
- package/src/mixins/index.ts +6 -0
- package/src/session/SessionClient.ts +57 -4
- package/src/session/__tests__/SessionClient.identityPin.test.ts +175 -0
- package/src/session/__tests__/accountDialogController.test.ts +812 -1
- package/src/session/__tests__/identityPin.test.ts +131 -0
- package/src/session/__tests__/identitySession.test.ts +194 -0
- package/src/session/__tests__/projectSessionState.test.ts +88 -0
- package/src/session/__tests__/refresh.identityPin.test.ts +306 -0
- package/src/session/accountDialogController.ts +435 -44
- package/src/session/identityPin.ts +310 -0
- package/src/session/identitySession.ts +206 -0
- package/src/session/projectSessionState.ts +61 -13
- package/src/session/refresh.ts +103 -9
- package/src/utils/__tests__/oauthPkce.test.ts +51 -0
- package/src/utils/commonsDelivery.ts +132 -0
- package/src/utils/oauthPkce.ts +56 -3
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Identity pin — the durable binding between THIS device's PRIMARY identity key
|
|
4
|
+
* and the account that key authenticates as.
|
|
5
|
+
*
|
|
6
|
+
* An identity-bound client (Commons, the identity vault) must always act as the
|
|
7
|
+
* owner of the local signing key — permanently. The `DeviceSession` it shares
|
|
8
|
+
* with every other Oxy app on the same device tracks a MUTABLE `activeAccountId`
|
|
9
|
+
* that any sibling app can switch; following it silently changes both the user
|
|
10
|
+
* such a client renders and the bearer it sends. The pin is the client-side
|
|
11
|
+
* record that breaks that coupling: `{ publicKey, accountId }`, written when an
|
|
12
|
+
* identity-mode session is established (the only moment both facts are known
|
|
13
|
+
* first-hand), read on every boot, and reconciled against the live
|
|
14
|
+
* `KeyManager.getPublicKey()` before it is trusted (see
|
|
15
|
+
* `resolveIdentityPin` in `identitySession.ts`).
|
|
16
|
+
*
|
|
17
|
+
* It holds NO secret — a public key and an account id, both already known to the
|
|
18
|
+
* server — so it lives in the same per-origin storage as {@link PersistedAuthState}
|
|
19
|
+
* (`authStateStore.ts`) rather than the keychain, and mirrors that module's
|
|
20
|
+
* store shape: `load / save / clear`, an in-memory mirror, a read-back-verified
|
|
21
|
+
* durable write, and graceful degradation when storage is unavailable.
|
|
22
|
+
*
|
|
23
|
+
* ESM-safe (no `require()`); no react/react-native/expo imports.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.IDENTITY_PIN_STORAGE_KEY = void 0;
|
|
27
|
+
exports.identityPinMatches = identityPinMatches;
|
|
28
|
+
exports.createMemoryIdentityPinStore = createMemoryIdentityPinStore;
|
|
29
|
+
exports.createWebIdentityPinStore = createWebIdentityPinStore;
|
|
30
|
+
exports.createNativeIdentityPinStore = createNativeIdentityPinStore;
|
|
31
|
+
const logger_1 = require("../logger");
|
|
32
|
+
/**
|
|
33
|
+
* Versioned storage key. Deliberately distinct from the `oxy.auth.*` keys so a
|
|
34
|
+
* session sign-out (which clears the auth blob) and an identity change (which
|
|
35
|
+
* clears the pin) can never take each other down.
|
|
36
|
+
*/
|
|
37
|
+
exports.IDENTITY_PIN_STORAGE_KEY = 'oxy.identity.pin.v1';
|
|
38
|
+
/**
|
|
39
|
+
* Structural check for a storable identity public key: `KeyManager` only ever
|
|
40
|
+
* persists secp256k1 hex — compressed (66 chars) or uncompressed (130). A stored
|
|
41
|
+
* value of any other shape is corrupt, so it is treated as "no pin" (fail
|
|
42
|
+
* closed) rather than compared. Full curve validation belongs to `KeyManager`;
|
|
43
|
+
* the pin's real authority is the reconcile against the live local key.
|
|
44
|
+
*/
|
|
45
|
+
function isStorablePublicKey(value) {
|
|
46
|
+
if (typeof value !== 'string') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (value.length !== 66 && value.length !== 130) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return /^[0-9a-fA-F]+$/.test(value);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse + shape-validate a stored blob. Returns `null` for anything that is not
|
|
56
|
+
* a well-formed {@link IdentityPin} (absent, malformed JSON, wrong types, junk
|
|
57
|
+
* public key) so a corrupt entry degrades to "not pinned" rather than throwing —
|
|
58
|
+
* and, critically, never yields a pin that could bind a client to the wrong
|
|
59
|
+
* account.
|
|
60
|
+
*/
|
|
61
|
+
function deserialize(raw) {
|
|
62
|
+
if (!raw) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
let parsed;
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(raw);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const candidate = parsed;
|
|
76
|
+
if (!isStorablePublicKey(candidate.publicKey)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (typeof candidate.accountId !== 'string' || candidate.accountId.length === 0) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
publicKey: candidate.publicKey.toLowerCase(),
|
|
84
|
+
accountId: candidate.accountId,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Serialize a pin, or `null` when the caller handed over a value that would not
|
|
89
|
+
* survive {@link deserialize} — writing it would produce an entry that silently
|
|
90
|
+
* reads back as "not pinned".
|
|
91
|
+
*/
|
|
92
|
+
function serialize(pin) {
|
|
93
|
+
if (!isStorablePublicKey(pin.publicKey) || pin.accountId.length === 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return JSON.stringify({ publicKey: pin.publicKey.toLowerCase(), accountId: pin.accountId });
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Whether `pin` still describes the identity currently on this device.
|
|
100
|
+
*
|
|
101
|
+
* `publicKey` comparison is case-insensitive (hex). A `null` local key is a
|
|
102
|
+
* definitive "no identity here" and therefore never a match — the caller clears
|
|
103
|
+
* the pin rather than binding to an account whose key is gone.
|
|
104
|
+
*/
|
|
105
|
+
function identityPinMatches(pin, localPublicKey) {
|
|
106
|
+
if (!pin || !localPublicKey) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return pin.publicKey.toLowerCase() === localPublicKey.toLowerCase();
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A process-lifetime, in-memory {@link IdentityPinStore}. Used directly for
|
|
113
|
+
* tests/SSR and as the degraded fallback of the web store.
|
|
114
|
+
*/
|
|
115
|
+
function createMemoryIdentityPinStore() {
|
|
116
|
+
let current = null;
|
|
117
|
+
return {
|
|
118
|
+
load: async () => current,
|
|
119
|
+
save: async (pin) => {
|
|
120
|
+
if (!serialize(pin)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
// Memory IS this store's durability backing — the write always lands.
|
|
124
|
+
current = { publicKey: pin.publicKey.toLowerCase(), accountId: pin.accountId };
|
|
125
|
+
return true;
|
|
126
|
+
},
|
|
127
|
+
clear: async () => {
|
|
128
|
+
current = null;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Read the ambient `localStorage`, tolerating the case where merely ACCESSING
|
|
134
|
+
* `window.localStorage` throws (sandboxed iframe `SecurityError`). Returns
|
|
135
|
+
* `null` when storage is unavailable.
|
|
136
|
+
*/
|
|
137
|
+
function safeGetLocalStorage() {
|
|
138
|
+
try {
|
|
139
|
+
if (typeof globalThis === 'undefined') {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const store = globalThis.localStorage;
|
|
143
|
+
return store ?? null;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A `localStorage`-backed {@link IdentityPinStore}. Degrades to an in-memory
|
|
151
|
+
* store when `localStorage` is unreachable, and keeps an in-memory mirror so a
|
|
152
|
+
* failed persist still pins THIS page's lifetime.
|
|
153
|
+
*/
|
|
154
|
+
function createWebIdentityPinStore() {
|
|
155
|
+
const storage = safeGetLocalStorage();
|
|
156
|
+
if (!storage) {
|
|
157
|
+
return createMemoryIdentityPinStore();
|
|
158
|
+
}
|
|
159
|
+
// `undefined` = never written this session → fall back to storage; any set
|
|
160
|
+
// value (including `null` after clear) is authoritative.
|
|
161
|
+
let sessionMirror;
|
|
162
|
+
return {
|
|
163
|
+
load: async () => {
|
|
164
|
+
if (sessionMirror !== undefined) {
|
|
165
|
+
return sessionMirror;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
return deserialize(storage.getItem(exports.IDENTITY_PIN_STORAGE_KEY));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
save: async (pin) => {
|
|
175
|
+
const json = serialize(pin);
|
|
176
|
+
if (!json) {
|
|
177
|
+
logger_1.logger.error('[identityPin] refusing to persist a malformed identity pin', undefined, { component: 'identityPin' });
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
sessionMirror = { publicKey: pin.publicKey.toLowerCase(), accountId: pin.accountId };
|
|
181
|
+
try {
|
|
182
|
+
storage.setItem(exports.IDENTITY_PIN_STORAGE_KEY, json);
|
|
183
|
+
if (storage.getItem(exports.IDENTITY_PIN_STORAGE_KEY) === json) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
logger_1.logger.error('[identityPin] read-back mismatch after save — the identity pin did not persist; this process stays pinned via the in-memory mirror but the next boot must re-establish the identity session', undefined, { component: 'identityPin' });
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
logger_1.logger.error('[identityPin] persist threw — the identity pin did not persist; this process stays pinned via the in-memory mirror but the next boot must re-establish the identity session', error, { component: 'identityPin' });
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
clear: async () => {
|
|
195
|
+
sessionMirror = null;
|
|
196
|
+
try {
|
|
197
|
+
storage.removeItem(exports.IDENTITY_PIN_STORAGE_KEY);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
logger_1.logger.debug('[identityPin] clear failed', { component: 'identityPin' }, error);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* A native {@link IdentityPinStore} over an injected async key/value store —
|
|
207
|
+
* the same seam `createNativeAuthStateStore` uses, so `@oxyhq/core` never
|
|
208
|
+
* imports `expo-secure-store`.
|
|
209
|
+
*/
|
|
210
|
+
function createNativeIdentityPinStore(storage) {
|
|
211
|
+
let sessionMirror;
|
|
212
|
+
return {
|
|
213
|
+
load: async () => {
|
|
214
|
+
if (sessionMirror !== undefined) {
|
|
215
|
+
return sessionMirror;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
return deserialize(await storage.getItem(exports.IDENTITY_PIN_STORAGE_KEY));
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
save: async (pin) => {
|
|
225
|
+
const json = serialize(pin);
|
|
226
|
+
if (!json) {
|
|
227
|
+
logger_1.logger.error('[identityPin] refusing to persist a malformed identity pin', undefined, { component: 'identityPin' });
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
sessionMirror = { publicKey: pin.publicKey.toLowerCase(), accountId: pin.accountId };
|
|
231
|
+
try {
|
|
232
|
+
await storage.setItem(exports.IDENTITY_PIN_STORAGE_KEY, json);
|
|
233
|
+
// A native write can resolve WITHOUT throwing yet not land, so a
|
|
234
|
+
// read-back is the only reliable proof.
|
|
235
|
+
if ((await storage.getItem(exports.IDENTITY_PIN_STORAGE_KEY)) === json) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
logger_1.logger.error('[identityPin] read-back mismatch after save — the identity pin did not persist; this app run stays pinned via the in-memory mirror but the next cold start must re-establish the identity session', undefined, { component: 'identityPin' });
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
logger_1.logger.error('[identityPin] persist threw — the identity pin did not persist; this app run stays pinned via the in-memory mirror but the next cold start must re-establish the identity session', error, { component: 'identityPin' });
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
clear: async () => {
|
|
247
|
+
sessionMirror = null;
|
|
248
|
+
try {
|
|
249
|
+
await storage.removeItem(exports.IDENTITY_PIN_STORAGE_KEY);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
logger_1.logger.debug('[identityPin] clear failed', { component: 'identityPin' }, error);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Identity-bound session — pin reconciliation + establishment.
|
|
4
|
+
*
|
|
5
|
+
* This is the SDK half of "an identity vault's authenticated user is whoever
|
|
6
|
+
* owns the local primary key". Two operations, both consumed by the cold boot
|
|
7
|
+
* (`boot/sessionColdBoot.ts`) and the re-mint lane (`session/refresh.ts`):
|
|
8
|
+
*
|
|
9
|
+
* - {@link resolveIdentityPin} — read the persisted `{publicKey, accountId}`
|
|
10
|
+
* pin and reconcile it against the identity currently on this device. A
|
|
11
|
+
* definitive mismatch (the key was replaced, or is gone) CLEARS the pin; an
|
|
12
|
+
* INDETERMINATE read (keychain locked / storage threw) leaves it untouched.
|
|
13
|
+
* - {@link establishIdentitySession} — mint a session from the PRIMARY local
|
|
14
|
+
* key (`getPublicKey` → `requestChallenge` → `signChallenge` →
|
|
15
|
+
* `verifyChallenge`), persist the resulting device credential, and write the
|
|
16
|
+
* pin. This is the identity-mode replacement for the shared-keychain lane,
|
|
17
|
+
* which reads the CROSS-APP shared slot and may therefore hold a different
|
|
18
|
+
* identity than this device's primary.
|
|
19
|
+
*
|
|
20
|
+
* Both take their key/signature functions from an injectable {@link IdentityBinding}
|
|
21
|
+
* (defaulting to `KeyManager` / `SignatureService`) so the lanes above stay
|
|
22
|
+
* testable without a keychain.
|
|
23
|
+
*
|
|
24
|
+
* ESM-safe (no `require()`); no react/react-native/expo imports.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.resolveIdentityPin = resolveIdentityPin;
|
|
28
|
+
exports.establishIdentitySession = establishIdentitySession;
|
|
29
|
+
const keyManager_1 = require("../crypto/keyManager");
|
|
30
|
+
const signatureService_1 = require("../crypto/signatureService");
|
|
31
|
+
const identityPin_1 = require("./identityPin");
|
|
32
|
+
const logger_1 = require("../logger");
|
|
33
|
+
function readPublicKeyOf(binding) {
|
|
34
|
+
return binding.readPublicKey ?? (() => keyManager_1.KeyManager.getPublicKey());
|
|
35
|
+
}
|
|
36
|
+
function signChallengeOf(binding) {
|
|
37
|
+
return binding.signChallenge ?? ((challenge) => signatureService_1.SignatureService.signChallenge(challenge));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read the persisted pin and reconcile it against the identity on this device.
|
|
41
|
+
*
|
|
42
|
+
* Returns the pin ONLY when the local primary public key still matches it — that
|
|
43
|
+
* is the single condition under which a client may bind its token and its
|
|
44
|
+
* rendered user to that account.
|
|
45
|
+
*
|
|
46
|
+
* Outcomes:
|
|
47
|
+
* - no pin stored → `null` (nothing to clear).
|
|
48
|
+
* - local key MATCHES → the pin (trusted).
|
|
49
|
+
* - local key read succeeded and differs / is absent → the identity was
|
|
50
|
+
* replaced or lost: CLEAR the pin and return `null`. Keeping it would pin the
|
|
51
|
+
* client to an account it can no longer prove control of.
|
|
52
|
+
* - the local key read THREW (keychain locked, storage unavailable) → return
|
|
53
|
+
* `null` WITHOUT clearing. A read that produced no verdict is never evidence
|
|
54
|
+
* of an identity change (the same rule `KeyManager` applies to its own
|
|
55
|
+
* transient errors); the caller falls through to the identity sign-in lane,
|
|
56
|
+
* which fails closed on a locked keychain rather than adopting a foreign
|
|
57
|
+
* account.
|
|
58
|
+
*/
|
|
59
|
+
async function resolveIdentityPin(binding) {
|
|
60
|
+
const pin = await binding.pinStore.load();
|
|
61
|
+
if (!pin) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
let localPublicKey;
|
|
65
|
+
try {
|
|
66
|
+
localPublicKey = await readPublicKeyOf(binding)();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
logger_1.logger.debug('Identity key read did not produce a verdict — keeping the pin untouched', { component: 'identitySession', method: 'resolveIdentityPin' }, error);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if ((0, identityPin_1.identityPinMatches)(pin, localPublicKey)) {
|
|
73
|
+
return pin;
|
|
74
|
+
}
|
|
75
|
+
logger_1.logger.warn('Identity pin no longer matches the device identity key — clearing it (the identity was replaced or removed)', { component: 'identitySession', method: 'resolveIdentityPin' });
|
|
76
|
+
await binding.pinStore.clear();
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Establish a session for the device's PRIMARY identity key and pin it.
|
|
81
|
+
*
|
|
82
|
+
* `requestChallenge` → `signChallenge` → `verifyChallenge`; `verifyChallenge`
|
|
83
|
+
* plants the access token itself. The server resolves the account from the
|
|
84
|
+
* VERIFIED SIGNER, so the returned session is identity-authoritative — it
|
|
85
|
+
* ignores the device's `activeAccountId` entirely.
|
|
86
|
+
*
|
|
87
|
+
* Side effects on success: the durable device credential is persisted (so the
|
|
88
|
+
* next boot can take the fast pinned-mint lane) and the pin is written.
|
|
89
|
+
*
|
|
90
|
+
* Returns `null` — never throws — for the two "nothing to do" cases: no local
|
|
91
|
+
* identity (including every web caller, where `KeyManager` has no key), and a
|
|
92
|
+
* verify that produced no access token. Network/crypto failures propagate so the
|
|
93
|
+
* caller can classify them.
|
|
94
|
+
*/
|
|
95
|
+
async function establishIdentitySession(args) {
|
|
96
|
+
const { oxy, store, binding, requestOptions } = args;
|
|
97
|
+
const publicKey = await readPublicKeyOf(binding)();
|
|
98
|
+
if (!publicKey) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const { challenge } = await oxy.requestChallenge(publicKey, requestOptions);
|
|
102
|
+
const signed = await signChallengeOf(binding)(challenge);
|
|
103
|
+
if (signed.publicKey.toLowerCase() !== publicKey.toLowerCase()) {
|
|
104
|
+
// The signer disagrees with the key we resolved a challenge for — the
|
|
105
|
+
// identity changed mid-flight, or a custom signer was misconfigured. Refuse:
|
|
106
|
+
// verifying under a different key would bind this client to another account.
|
|
107
|
+
throw new Error('Identity sign-in aborted: the signing key does not match the device identity key');
|
|
108
|
+
}
|
|
109
|
+
// `signed.challenge` carries the SIGNATURE (mirrors `signChallengeWithSharedKey`).
|
|
110
|
+
const session = await oxy.verifyChallenge(signed.publicKey, challenge, signed.challenge, signed.timestamp, binding.deviceName, binding.deviceFingerprint, requestOptions);
|
|
111
|
+
if (!session?.accessToken) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
// Persist the rotating device credential so later boots re-mint through the
|
|
115
|
+
// fast `POST /session/device/token` lane instead of re-signing a challenge.
|
|
116
|
+
if (session.deviceId && session.deviceSecret) {
|
|
117
|
+
await store.save({
|
|
118
|
+
sessionId: session.sessionId,
|
|
119
|
+
userId: session.user.id,
|
|
120
|
+
deviceId: session.deviceId,
|
|
121
|
+
deviceSecret: session.deviceSecret,
|
|
122
|
+
accessToken: session.accessToken,
|
|
123
|
+
expiresAt: session.expiresAt,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const pin = { publicKey: publicKey.toLowerCase(), accountId: session.user.id };
|
|
127
|
+
// A failed pin persist is NOT fatal: this process is already pinned in memory,
|
|
128
|
+
// and a later boot without a pin skips the (unpinned) mint lane and re-runs
|
|
129
|
+
// this exact lane, which rewrites it. Never fail an established session on it.
|
|
130
|
+
const pinned = await binding.pinStore.save(pin);
|
|
131
|
+
if (!pinned) {
|
|
132
|
+
logger_1.logger.warn('Identity session established but the pin did not durably persist — the next cold start will re-establish it', { component: 'identitySession', method: 'establishIdentitySession' });
|
|
133
|
+
}
|
|
134
|
+
return { session, pin };
|
|
135
|
+
}
|
|
@@ -14,7 +14,25 @@ exports.accountIdsOf = accountIdsOf;
|
|
|
14
14
|
* `oxyServices.getUsersByIds(accountIdsOf(state))` and builds `usersById`
|
|
15
15
|
* from the result before calling `deviceStateToClientSessions` /
|
|
16
16
|
* `activeUserOf`.
|
|
17
|
+
*
|
|
18
|
+
* Each projection takes an OPTIONAL `pinnedAccountId`. Omit it (or pass `null`)
|
|
19
|
+
* and the projection resolves the device's `activeAccountId` exactly as before.
|
|
20
|
+
* Pass it — an IDENTITY-BOUND client, whose user is fixed by the local identity
|
|
21
|
+
* key — and the projection resolves THAT account instead, so an account switch
|
|
22
|
+
* made by another app on the same device changes `state` but never the user this
|
|
23
|
+
* client renders.
|
|
17
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* The account a projection should resolve: the pin when one is supplied and
|
|
27
|
+
* non-empty, else the device's active account. An empty-string pin is treated as
|
|
28
|
+
* "not pinned" rather than as an account that can never match.
|
|
29
|
+
*/
|
|
30
|
+
function boundAccountIdOf(state, pinnedAccountId) {
|
|
31
|
+
if (typeof pinnedAccountId === 'string' && pinnedAccountId.length > 0) {
|
|
32
|
+
return pinnedAccountId;
|
|
33
|
+
}
|
|
34
|
+
return state.activeAccountId;
|
|
35
|
+
}
|
|
18
36
|
/**
|
|
19
37
|
* Maps every `SessionAccount` in `state.accounts` to a `ClientSession`.
|
|
20
38
|
*
|
|
@@ -26,9 +44,14 @@ exports.accountIdsOf = accountIdsOf;
|
|
|
26
44
|
* though `ClientSession` only stores `userId` — a session is still
|
|
27
45
|
* projected for an account whose id is absent from `usersById` (no
|
|
28
46
|
* placeholder user is fabricated).
|
|
47
|
+
*
|
|
48
|
+
* `isCurrent` marks the PINNED account when one is supplied, so it can never
|
|
49
|
+
* disagree with {@link activeSessionIdOf} / {@link activeUserOf} for the same
|
|
50
|
+
* pin.
|
|
29
51
|
*/
|
|
30
|
-
function deviceStateToClientSessions(state, usersById) {
|
|
52
|
+
function deviceStateToClientSessions(state, usersById, pinnedAccountId) {
|
|
31
53
|
const provisionalTimestamp = new Date(state.updatedAt).toISOString();
|
|
54
|
+
const boundAccountId = boundAccountIdOf(state, pinnedAccountId);
|
|
32
55
|
return state.accounts.map((account) => ({
|
|
33
56
|
sessionId: account.sessionId,
|
|
34
57
|
deviceId: state.deviceId,
|
|
@@ -36,32 +59,48 @@ function deviceStateToClientSessions(state, usersById) {
|
|
|
36
59
|
expiresAt: provisionalTimestamp,
|
|
37
60
|
lastActive: provisionalTimestamp,
|
|
38
61
|
userId: account.accountId,
|
|
39
|
-
isCurrent: account.accountId ===
|
|
62
|
+
isCurrent: account.accountId === boundAccountId,
|
|
40
63
|
authuser: account.authuser,
|
|
41
64
|
}));
|
|
42
65
|
}
|
|
43
66
|
/**
|
|
44
|
-
* The
|
|
45
|
-
*
|
|
67
|
+
* The bound account's `sessionId`, or `null` when there is no state, no bound
|
|
68
|
+
* account, or that account has no session on this device.
|
|
69
|
+
*
|
|
70
|
+
* A pinned account that is ABSENT from `state.accounts` yields `null` — the
|
|
71
|
+
* honest signal that this device no longer carries a session for the pinned
|
|
72
|
+
* identity, which the caller answers by re-establishing the identity session
|
|
73
|
+
* (never by adopting the device's active account).
|
|
46
74
|
*/
|
|
47
|
-
function activeSessionIdOf(state) {
|
|
48
|
-
if (state === null
|
|
75
|
+
function activeSessionIdOf(state, pinnedAccountId) {
|
|
76
|
+
if (state === null) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const boundAccountId = boundAccountIdOf(state, pinnedAccountId);
|
|
80
|
+
if (boundAccountId === null) {
|
|
49
81
|
return null;
|
|
50
82
|
}
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
return activeAccount?.sessionId ?? null;
|
|
83
|
+
const boundAccount = state.accounts.find((account) => account.accountId === boundAccountId);
|
|
84
|
+
return boundAccount?.sessionId ?? null;
|
|
54
85
|
}
|
|
55
86
|
/**
|
|
56
|
-
* The
|
|
57
|
-
*
|
|
58
|
-
*
|
|
87
|
+
* The bound account's `User`, resolved from `usersById`. `null` when there is no
|
|
88
|
+
* state, no bound account, or the bound account id is absent from `usersById`.
|
|
89
|
+
*
|
|
90
|
+
* A pinned user is resolved from `usersById` alone — deliberately NOT gated on
|
|
91
|
+
* device membership — so a transient device-state gap cannot flicker the
|
|
92
|
+
* identity vault's rendered user. Whether the pinned session still exists on the
|
|
93
|
+
* device is answered by {@link activeSessionIdOf}.
|
|
59
94
|
*/
|
|
60
|
-
function activeUserOf(state, usersById) {
|
|
61
|
-
if (state === null
|
|
95
|
+
function activeUserOf(state, usersById, pinnedAccountId) {
|
|
96
|
+
if (state === null) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const boundAccountId = boundAccountIdOf(state, pinnedAccountId);
|
|
100
|
+
if (boundAccountId === null) {
|
|
62
101
|
return null;
|
|
63
102
|
}
|
|
64
|
-
return usersById.get(
|
|
103
|
+
return usersById.get(boundAccountId) ?? null;
|
|
65
104
|
}
|
|
66
105
|
/**
|
|
67
106
|
* All account ids in `state`, suitable for an `oxyServices.getUsersByIds(...)`
|
|
@@ -6,6 +6,7 @@ exports.refreshPersistedSession = refreshPersistedSession;
|
|
|
6
6
|
exports.createAuthRefreshHandler = createAuthRefreshHandler;
|
|
7
7
|
exports.installAuthRefreshHandler = installAuthRefreshHandler;
|
|
8
8
|
exports.startTokenRefreshScheduler = startTokenRefreshScheduler;
|
|
9
|
+
const identitySession_1 = require("./identitySession");
|
|
9
10
|
const platform_1 = require("../utils/platform");
|
|
10
11
|
const errorUtils_1 = require("../utils/errorUtils");
|
|
11
12
|
const logger_1 = require("../logger");
|
|
@@ -58,9 +59,16 @@ const MAX_FAILURE_BACKOFF_MS = 5 * 60000;
|
|
|
58
59
|
* planting. This function performs NO store mutation on failure — the caller
|
|
59
60
|
* applies the drop/clear policy (which differs web vs native) from the returned
|
|
60
61
|
* status.
|
|
62
|
+
*
|
|
63
|
+
* `pin` makes the mint IDENTITY-BOUND: the request carries the pinned
|
|
64
|
+
* `accountId` (so the server mints that account's token without touching
|
|
65
|
+
* `activeAccountId`), and the persisted `sessionId`/`userId` are resolved from
|
|
66
|
+
* the PINNED account entry — never from `state.activeAccountId`, whose drift is
|
|
67
|
+
* exactly what the pin exists to stop.
|
|
61
68
|
*/
|
|
62
69
|
async function refreshDeviceSecretArm(deps) {
|
|
63
70
|
const { oxy, store } = deps;
|
|
71
|
+
const pin = deps.pin ?? null;
|
|
64
72
|
return oxy.httpService.runSingleFlightDeviceSecretMint(async () => {
|
|
65
73
|
const persisted = await store.load();
|
|
66
74
|
if (!persisted?.deviceId || !persisted?.deviceSecret) {
|
|
@@ -68,7 +76,13 @@ async function refreshDeviceSecretArm(deps) {
|
|
|
68
76
|
}
|
|
69
77
|
let mint;
|
|
70
78
|
try {
|
|
71
|
-
|
|
79
|
+
// Unpinned callers pass NO third argument at all, so the account-mode call
|
|
80
|
+
// shape (and therefore the request body) is untouched by this feature.
|
|
81
|
+
mint = pin
|
|
82
|
+
? await oxy.mintFromDeviceSecret(persisted.deviceId, persisted.deviceSecret, {
|
|
83
|
+
accountId: pin.accountId,
|
|
84
|
+
})
|
|
85
|
+
: await oxy.mintFromDeviceSecret(persisted.deviceId, persisted.deviceSecret);
|
|
72
86
|
}
|
|
73
87
|
catch (error) {
|
|
74
88
|
if ((0, errorUtils_1.extractErrorStatus)(error) === 401) {
|
|
@@ -88,18 +102,26 @@ async function refreshDeviceSecretArm(deps) {
|
|
|
88
102
|
return { status: 'invalid-secret' };
|
|
89
103
|
if (body.includes('no_active_session'))
|
|
90
104
|
return { status: 'no-session' };
|
|
105
|
+
// A pinned mint whose account left the device set. The secret is intact —
|
|
106
|
+
// never classify this as a bad secret, or the caller would drop a healthy
|
|
107
|
+
// credential over a stale identity binding.
|
|
108
|
+
if (body.includes('account_not_on_device'))
|
|
109
|
+
return { status: 'account-not-on-device' };
|
|
91
110
|
return { status: 'transient' };
|
|
92
111
|
}
|
|
93
112
|
return { status: 'transient' };
|
|
94
113
|
}
|
|
95
|
-
|
|
114
|
+
// The account this session is BOUND to: the pinned one when identity-bound
|
|
115
|
+
// (the server already minted for it), else the device's active account.
|
|
116
|
+
const boundAccountId = pin ? pin.accountId : mint.state.activeAccountId;
|
|
117
|
+
const bound = mint.state.accounts.find((a) => a.accountId === boundAccountId);
|
|
96
118
|
const next = {
|
|
97
119
|
...persisted,
|
|
98
120
|
deviceId: mint.state.deviceId,
|
|
99
121
|
deviceSecret: mint.nextDeviceSecret,
|
|
100
122
|
accessToken: mint.accessToken,
|
|
101
123
|
expiresAt: mint.expiresAt,
|
|
102
|
-
...(
|
|
124
|
+
...(bound ? { sessionId: bound.sessionId, userId: bound.accountId } : {}),
|
|
103
125
|
};
|
|
104
126
|
// Rotation-in-use anti-loss: persist the NEXT secret and read-back-VERIFY it
|
|
105
127
|
// landed BEFORE planting the token. A failed durable persist must NOT plant.
|
|
@@ -127,11 +149,25 @@ async function refreshDeviceSecretArm(deps) {
|
|
|
127
149
|
* recovered `{deviceId, deviceSecret, …}` is PERSISTED so the fast device-secret
|
|
128
150
|
* lane is repopulated (mirrors the cold boot's `shared-key-signin` step) — an
|
|
129
151
|
* in-session shared-key recovery must not leave the fast-lane credential empty.
|
|
152
|
+
*
|
|
153
|
+
* IDENTITY-BOUND clients (`deps.identity`) run a different arm 2: the
|
|
154
|
+
* shared-keychain lane is DISABLED (its cross-app slot may hold a different
|
|
155
|
+
* identity) and replaced by {@link establishIdentitySession}, which re-signs a
|
|
156
|
+
* challenge with the PRIMARY local key and rewrites the pin. Arm 1 is pinned.
|
|
130
157
|
*/
|
|
131
158
|
async function refreshPersistedSession(deps) {
|
|
132
159
|
const { oxy, store } = deps;
|
|
133
|
-
const
|
|
134
|
-
|
|
160
|
+
const identity = deps.identity ?? null;
|
|
161
|
+
// The shared keychain is never an identity-bound client's recovery path.
|
|
162
|
+
const allowSharedKeyFallback = identity ? false : (deps.allowSharedKeyFallback ?? (0, platform_1.isNative)());
|
|
163
|
+
// Resolved per call: a re-established identity session can move the pin, and a
|
|
164
|
+
// replaced/removed local key clears it (in which case arm 1 must NOT mint —
|
|
165
|
+
// an unpinned mint would adopt whatever account the device switched to).
|
|
166
|
+
const pin = identity ? await (0, identitySession_1.resolveIdentityPin)(identity) : null;
|
|
167
|
+
if (identity && !pin) {
|
|
168
|
+
return recoverIdentitySession(oxy, store, identity);
|
|
169
|
+
}
|
|
170
|
+
const arm1 = await refreshDeviceSecretArm({ oxy, store, pin });
|
|
135
171
|
switch (arm1.status) {
|
|
136
172
|
case 'ok':
|
|
137
173
|
return arm1.token;
|
|
@@ -147,11 +183,12 @@ async function refreshPersistedSession(deps) {
|
|
|
147
183
|
return null;
|
|
148
184
|
case 'invalid-secret':
|
|
149
185
|
case 'no-session': {
|
|
150
|
-
// 401: secret diverged or no live session.
|
|
151
|
-
//
|
|
186
|
+
// 401: secret diverged or no live session. When a key-based arm 2 can still
|
|
187
|
+
// recover (native shared key, or an identity-bound client's own primary
|
|
188
|
+
// key) drop ONLY the secret and keep the deviceId; otherwise (web) the
|
|
152
189
|
// session is over — clear the store.
|
|
153
190
|
const persisted = await store.load();
|
|
154
|
-
if (allowSharedKeyFallback) {
|
|
191
|
+
if (allowSharedKeyFallback || identity) {
|
|
155
192
|
if (persisted) {
|
|
156
193
|
await store.save({ ...persisted, deviceSecret: undefined });
|
|
157
194
|
}
|
|
@@ -161,9 +198,17 @@ async function refreshPersistedSession(deps) {
|
|
|
161
198
|
}
|
|
162
199
|
break;
|
|
163
200
|
}
|
|
201
|
+
case 'account-not-on-device':
|
|
202
|
+
// The pinned account left this device's session set. The secret is healthy —
|
|
203
|
+
// leave the store untouched and let the identity arm re-establish.
|
|
204
|
+
logger_1.logger.debug('Pinned device-secret mint rejected: the pinned account is no longer on this device — re-establishing from the identity key', { component: 'refresh', method: 'refreshPersistedSession' });
|
|
205
|
+
break;
|
|
164
206
|
case 'no-secret':
|
|
165
207
|
break;
|
|
166
208
|
}
|
|
209
|
+
if (identity) {
|
|
210
|
+
return recoverIdentitySession(oxy, store, identity);
|
|
211
|
+
}
|
|
167
212
|
if (allowSharedKeyFallback) {
|
|
168
213
|
try {
|
|
169
214
|
const session = await oxy.signInWithSharedIdentity();
|
|
@@ -188,6 +233,26 @@ async function refreshPersistedSession(deps) {
|
|
|
188
233
|
}
|
|
189
234
|
return null;
|
|
190
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Arm 2 for an IDENTITY-BOUND client: re-establish the session from the PRIMARY
|
|
238
|
+
* local key and rewrite the pin (`establishIdentitySession` plants the token and
|
|
239
|
+
* persists the device credential itself).
|
|
240
|
+
*
|
|
241
|
+
* Returns `null` — never throws — when there is no local identity, the verify
|
|
242
|
+
* yielded no token, or the exchange failed: the caller treats that as "could not
|
|
243
|
+
* refresh", exactly like the shared-key arm. A locked keychain therefore ends
|
|
244
|
+
* signed out rather than falling back to the device's active account.
|
|
245
|
+
*/
|
|
246
|
+
async function recoverIdentitySession(oxy, store, binding) {
|
|
247
|
+
try {
|
|
248
|
+
const established = await (0, identitySession_1.establishIdentitySession)({ oxy, store, binding });
|
|
249
|
+
return established?.session.accessToken ?? null;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
logger_1.logger.debug('Identity-key re-sign-in failed', { component: 'refresh', method: 'recoverIdentitySession' }, error);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
191
256
|
/**
|
|
192
257
|
* Build the reactive `AuthRefreshHandler` (arm 1 + arm 2). Install it via
|
|
193
258
|
* {@link installAuthRefreshHandler} or directly on
|