@oxy.so/core 4.0.0 → 4.2.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/cjs/HttpService.js +142 -33
- package/dist/cjs/api/accounts.js +49 -10
- package/dist/cjs/api/apps.js +2 -2
- package/dist/cjs/api/auth.js +24 -3
- package/dist/cjs/api/billing.js +35 -0
- package/dist/cjs/boot/sessionColdBoot.js +28 -57
- package/dist/cjs/inference/OxyInferenceClient.js +53 -20
- package/dist/cjs/server/OxyServer.js +107 -51
- package/dist/cjs/server/agentAccount.js +19 -0
- package/dist/cjs/server/auth.js +19 -0
- package/dist/cjs/server/capabilityTicket.js +3 -41
- package/dist/cjs/server/index.js +10 -2
- package/dist/cjs/server/liveCapabilityTicket.js +48 -0
- package/dist/cjs/server/middleware.js +60 -18
- package/dist/cjs/server/namespaces.js +57 -0
- package/dist/cjs/server/serviceTokenLifetime.js +13 -0
- package/dist/cjs/session/SessionClient.js +90 -12
- package/dist/cjs/session/authStateStore.js +130 -39
- package/dist/cjs/session/refresh.js +152 -30
- package/dist/cjs/session/sharedDeviceCredential.js +17 -1
- package/dist/cjs/utils/officialOrigins.js +7 -18
- package/dist/esm/HttpService.js +143 -34
- package/dist/esm/api/accounts.js +38 -0
- package/dist/esm/api/apps.js +2 -2
- package/dist/esm/api/auth.js +23 -3
- package/dist/esm/api/billing.js +35 -0
- package/dist/esm/boot/sessionColdBoot.js +29 -58
- package/dist/esm/inference/OxyInferenceClient.js +46 -13
- package/dist/esm/server/OxyServer.js +107 -51
- package/dist/esm/server/agentAccount.js +16 -0
- package/dist/esm/server/auth.js +18 -0
- package/dist/esm/server/capabilityTicket.js +1 -39
- package/dist/esm/server/index.js +4 -1
- package/dist/esm/server/liveCapabilityTicket.js +45 -0
- package/dist/esm/server/middleware.js +60 -18
- package/dist/esm/server/namespaces.js +57 -0
- package/dist/esm/server/serviceTokenLifetime.js +9 -0
- package/dist/esm/session/SessionClient.js +90 -12
- package/dist/esm/session/authStateStore.js +128 -38
- package/dist/esm/session/refresh.js +151 -30
- package/dist/esm/session/sharedDeviceCredential.js +17 -1
- package/dist/esm/utils/officialOrigins.js +2 -13
- package/dist/types/HttpService.d.ts +26 -12
- package/dist/types/api/accounts.d.ts +38 -0
- package/dist/types/api/agency.d.ts +3 -1
- package/dist/types/api/apps.d.ts +3 -1
- package/dist/types/api/auth.d.ts +12 -1
- package/dist/types/api/billing.d.ts +12 -0
- package/dist/types/api/session.d.ts +3 -1
- package/dist/types/boot/sessionColdBoot.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/inference/OxyInferenceClient.d.ts +12 -0
- package/dist/types/inference/index.d.ts +1 -1
- package/dist/types/server/OxyServer.d.ts +16 -9
- package/dist/types/server/agentAccount.d.ts +14 -0
- package/dist/types/server/auth.d.ts +18 -0
- package/dist/types/server/capabilityTicket.d.ts +2 -4
- package/dist/types/server/index.d.ts +7 -1
- package/dist/types/server/liveCapabilityTicket.d.ts +15 -0
- package/dist/types/server/middleware.d.ts +15 -6
- package/dist/types/server/namespaces.d.ts +78 -2
- package/dist/types/server/serviceTokenLifetime.d.ts +7 -0
- package/dist/types/session/SessionClient.d.ts +8 -0
- package/dist/types/session/authStateStore.d.ts +15 -0
- package/dist/types/session/index.d.ts +1 -1
- package/dist/types/session/refresh.d.ts +32 -7
- package/dist/types/session/sharedDeviceCredential.d.ts +3 -1
- package/dist/types/utils/officialOrigins.d.ts +1 -5
- package/package.json +6 -6
- package/src/HttpService.ts +138 -50
- package/src/api/accounts.ts +22 -0
- package/src/api/agency.ts +3 -1
- package/src/api/apps.ts +2 -2
- package/src/api/auth.ts +31 -2
- package/src/api/billing.ts +71 -9
- package/src/api/session.ts +3 -1
- package/src/boot/sessionColdBoot.ts +27 -72
- package/src/index.ts +1 -1
- package/src/inference/OxyInferenceClient.ts +50 -5
- package/src/inference/index.ts +1 -0
- package/src/server/OxyServer.ts +114 -53
- package/src/server/agentAccount.ts +28 -0
- package/src/server/auth.ts +25 -0
- package/src/server/capabilityTicket.ts +2 -42
- package/src/server/index.ts +13 -0
- package/src/server/liveCapabilityTicket.ts +47 -0
- package/src/server/middleware.ts +72 -25
- package/src/server/namespaces.ts +99 -2
- package/src/server/serviceTokenLifetime.ts +13 -0
- package/src/session/SessionClient.ts +85 -12
- package/src/session/authStateStore.ts +130 -46
- package/src/session/index.ts +1 -0
- package/src/session/refresh.ts +144 -28
- package/src/session/sharedDeviceCredential.ts +14 -2
- package/src/utils/officialOrigins.ts +2 -13
package/dist/cjs/HttpService.js
CHANGED
|
@@ -27,6 +27,35 @@ const OxyServices_errors_1 = require("./OxyServices.errors");
|
|
|
27
27
|
const edgeRegion_1 = require("./utils/edgeRegion");
|
|
28
28
|
const activityId_1 = require("./utils/activityId");
|
|
29
29
|
const slashes_1 = require("./utils/slashes");
|
|
30
|
+
/** Comparison only fences local async work; it never validates token authority. */
|
|
31
|
+
function sameTokenSession(left, right) {
|
|
32
|
+
if (left === right)
|
|
33
|
+
return true;
|
|
34
|
+
if (!left || !right)
|
|
35
|
+
return false;
|
|
36
|
+
const a = (0, tokenClaims_1.decodeTokenClaims)(left);
|
|
37
|
+
const b = (0, tokenClaims_1.decodeTokenClaims)(right);
|
|
38
|
+
if (!a || !b || !(a.userId || a.id || a.sub) || !(b.userId || b.id || b.sub))
|
|
39
|
+
return false;
|
|
40
|
+
// A refresh may rotate exp/iat/jti while preserving its account and session.
|
|
41
|
+
// Opaque/malformed context fields cannot establish that equivalence.
|
|
42
|
+
return ['userId', 'id', 'sub', 'sessionId', 'operatorId', 'clientId', 'appId', 'authMethodId'].every((key) => (a[key] === undefined || typeof a[key] === 'string') && a[key] === b[key]);
|
|
43
|
+
}
|
|
44
|
+
/** Cancel this wait only; the shared mint continues for other callers. */
|
|
45
|
+
function awaitRequestSignal(pending, signal) {
|
|
46
|
+
if (!signal)
|
|
47
|
+
return pending;
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const onAbort = () => { reject((0, errorUtils_1.createCancelledError)()); };
|
|
50
|
+
if (signal.aborted)
|
|
51
|
+
onAbort();
|
|
52
|
+
else
|
|
53
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
54
|
+
// Both handlers remain attached even after cancellation, so a late shared
|
|
55
|
+
// failure is observed without reviving this request or becoming unhandled.
|
|
56
|
+
pending.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
30
59
|
/** Methods whose calls concurrent callers may share (read-only). */
|
|
31
60
|
const SAFE_METHODS = new Set(['GET']);
|
|
32
61
|
/** Methods safe to re-send after a transient failure (RFC 9110 §9.2.2). */
|
|
@@ -305,8 +334,7 @@ class HttpService {
|
|
|
305
334
|
const currentToken = this.tokenStore.getAccessToken();
|
|
306
335
|
if (providedToken) {
|
|
307
336
|
if (providedToken !== currentToken) {
|
|
308
|
-
this.
|
|
309
|
-
this.notifyTokenChange();
|
|
337
|
+
this.setTokens(providedToken);
|
|
310
338
|
}
|
|
311
339
|
return providedToken;
|
|
312
340
|
}
|
|
@@ -1014,8 +1042,12 @@ class HttpService {
|
|
|
1014
1042
|
this.tokenRefreshPromise = this.authRefreshHandler(reason)
|
|
1015
1043
|
.then((newToken) => {
|
|
1016
1044
|
if (epoch !== this.sessionEpoch) {
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1045
|
+
// A guarded refresh handler can itself adopt a new active context.
|
|
1046
|
+
// Acknowledge only an already-installed, exact result; never plant
|
|
1047
|
+
// a stale return value over a concurrent switch/logout.
|
|
1048
|
+
if (newToken && !this.sessionEnded && this.tokenStore.getAccessToken() === newToken) {
|
|
1049
|
+
return newToken;
|
|
1050
|
+
}
|
|
1019
1051
|
this.logger.debug('Discarded a token refresh that outlived its session');
|
|
1020
1052
|
return null;
|
|
1021
1053
|
}
|
|
@@ -1026,8 +1058,7 @@ class HttpService {
|
|
|
1026
1058
|
// A token is planted again, so there is a session again.
|
|
1027
1059
|
this.sessionEnded = false;
|
|
1028
1060
|
if (this.tokenStore.getAccessToken() !== newToken) {
|
|
1029
|
-
this.
|
|
1030
|
-
this.notifyTokenChange();
|
|
1061
|
+
this.setTokens(newToken);
|
|
1031
1062
|
}
|
|
1032
1063
|
this.noFresherTokenThan = isInsideRefreshLeadWindow(newToken) ? newToken : null;
|
|
1033
1064
|
// A success clears the failure timestamp so the next refresh is never
|
|
@@ -1183,23 +1214,61 @@ class HttpService {
|
|
|
1183
1214
|
* abort signal defines the lifetime of a healthy stream.
|
|
1184
1215
|
*/
|
|
1185
1216
|
async requestAuthenticatedResponse(config) {
|
|
1186
|
-
|
|
1187
|
-
|
|
1217
|
+
return this.requestRawResponse(config, true);
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Raw response for public or authenticated app endpoints. The SDK supplies
|
|
1221
|
+
* its current bearer when available; caller headers never supply authority.
|
|
1222
|
+
* No caching, envelope transformation or transport-error retry is performed.
|
|
1223
|
+
*/
|
|
1224
|
+
async requestResponse(config) {
|
|
1225
|
+
return this.requestRawResponse(config, false);
|
|
1226
|
+
}
|
|
1227
|
+
async requestRawResponse(config, requireAuth) {
|
|
1228
|
+
// Validate before obtaining a bearer: an unrelated origin must not trigger
|
|
1229
|
+
// an auth mint, even when the trusted transport accepts absolute URLs.
|
|
1230
|
+
const target = new URL(this.buildURL(config.url));
|
|
1231
|
+
const base = new URL(this.baseURL);
|
|
1232
|
+
if (target.origin !== base.origin || !['http:', 'https:'].includes(target.protocol)
|
|
1233
|
+
|| target.username || target.password) {
|
|
1234
|
+
throw new Error('Raw response requests must target the configured API origin');
|
|
1235
|
+
}
|
|
1236
|
+
if (config.signal?.aborted)
|
|
1237
|
+
throw (0, errorUtils_1.createCancelledError)();
|
|
1238
|
+
// Snapshot caller-owned options once, including headers, before any await.
|
|
1239
|
+
const request = { ...config, headers: new Headers(config.headers) };
|
|
1240
|
+
const contextToken = this.syncAccessTokenFromProvider();
|
|
1241
|
+
const beforeAuthEpoch = this.sessionEpoch;
|
|
1242
|
+
const authHeader = await awaitRequestSignal(this.getAuthHeader(), config.signal);
|
|
1243
|
+
if (beforeAuthEpoch !== this.sessionEpoch || !sameTokenSession(contextToken, this.tokenStore.getAccessToken())) {
|
|
1244
|
+
throw new OxyServices_errors_1.OxyAuthenticationError('The request session changed', 'AUTH_SESSION_CHANGED');
|
|
1245
|
+
}
|
|
1246
|
+
if (config.signal?.aborted)
|
|
1247
|
+
throw (0, errorUtils_1.createCancelledError)();
|
|
1248
|
+
if (requireAuth && !authHeader) {
|
|
1188
1249
|
throw new OxyServices_errors_1.OxyAuthenticationError('An active Oxy session is required for this request', 'AUTH_REQUIRED');
|
|
1189
1250
|
}
|
|
1190
|
-
return this.
|
|
1251
|
+
return this.requestResponseAttempt(request, target.href, authHeader, false, this.sessionEpoch, contextToken);
|
|
1191
1252
|
}
|
|
1192
|
-
async
|
|
1253
|
+
async requestResponseAttempt(config, fullUrl, authHeader, isAuthRetry, epoch, contextToken) {
|
|
1254
|
+
const assertCurrent = () => {
|
|
1255
|
+
this.syncAccessTokenFromProvider();
|
|
1256
|
+
if (epoch !== this.sessionEpoch || !sameTokenSession(contextToken, this.tokenStore.getAccessToken())) {
|
|
1257
|
+
throw new OxyServices_errors_1.OxyAuthenticationError('The request session changed', 'AUTH_SESSION_CHANGED');
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1193
1260
|
const startTime = Date.now();
|
|
1261
|
+
if (config.signal?.aborted)
|
|
1262
|
+
throw (0, errorUtils_1.createCancelledError)();
|
|
1263
|
+
assertCurrent();
|
|
1194
1264
|
const headers = new Headers(config.headers);
|
|
1195
|
-
if (!headers.has('Accept'))
|
|
1265
|
+
if (!headers.has('Accept'))
|
|
1196
1266
|
headers.set('Accept', 'application/json');
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
// to a second in front of requests.
|
|
1267
|
+
// Never accept ambient credentials or another authority from a caller.
|
|
1268
|
+
for (const name of ['Authorization', 'Cookie', 'Cookie2', 'Host', 'Proxy-Authorization'])
|
|
1269
|
+
headers.delete(name);
|
|
1270
|
+
if (authHeader)
|
|
1271
|
+
headers.set('Authorization', authHeader);
|
|
1203
1272
|
const edgeRegionHeader = (0, edgeRegion_1.peekBrowserEdgeRegionHeader)();
|
|
1204
1273
|
for (const [name, value] of Object.entries(edgeRegionHeader))
|
|
1205
1274
|
headers.set(name, value);
|
|
@@ -1207,22 +1276,51 @@ class HttpService {
|
|
|
1207
1276
|
for (const [name, value] of Object.entries(activityIdHeader))
|
|
1208
1277
|
headers.set(name, value);
|
|
1209
1278
|
try {
|
|
1210
|
-
const
|
|
1211
|
-
const
|
|
1279
|
+
const body = config.method === 'GET' ? undefined : config.body;
|
|
1280
|
+
const init = {
|
|
1212
1281
|
method: config.method,
|
|
1213
1282
|
headers,
|
|
1214
|
-
body
|
|
1283
|
+
body,
|
|
1215
1284
|
signal: config.signal,
|
|
1216
|
-
credentials:
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1285
|
+
credentials: 'omit',
|
|
1286
|
+
redirect: 'error',
|
|
1287
|
+
};
|
|
1288
|
+
// Node fetch requires this flag for streaming uploads; ordinary and
|
|
1289
|
+
// multipart bodies keep the platform's existing body serialization.
|
|
1290
|
+
if (body && typeof body === 'object' && 'getReader' in body)
|
|
1291
|
+
init.duplex = 'half';
|
|
1292
|
+
const response = await (config.fetch ?? globalThis.fetch)(fullUrl, init);
|
|
1293
|
+
try {
|
|
1294
|
+
assertCurrent();
|
|
1295
|
+
}
|
|
1296
|
+
catch (error) {
|
|
1297
|
+
await response.body?.cancel();
|
|
1298
|
+
throw error;
|
|
1299
|
+
}
|
|
1300
|
+
const replayable = body === undefined || typeof body === 'string';
|
|
1301
|
+
if (response.status === 401 && !isAuthRetry && authHeader && replayable && !config.signal?.aborted) {
|
|
1302
|
+
let refreshed;
|
|
1303
|
+
try {
|
|
1304
|
+
refreshed = await awaitRequestSignal(this.refreshAccessToken('response-401'), config.signal);
|
|
1305
|
+
}
|
|
1306
|
+
catch (error) {
|
|
1307
|
+
void response.body?.cancel().catch(() => undefined);
|
|
1308
|
+
throw error;
|
|
1309
|
+
}
|
|
1310
|
+
try {
|
|
1311
|
+
assertCurrent();
|
|
1312
|
+
}
|
|
1313
|
+
catch (error) {
|
|
1314
|
+
await response.body?.cancel();
|
|
1315
|
+
throw error;
|
|
1316
|
+
}
|
|
1220
1317
|
if (refreshed) {
|
|
1221
1318
|
await response.body?.cancel();
|
|
1222
|
-
|
|
1319
|
+
if (config.signal?.aborted)
|
|
1320
|
+
throw (0, errorUtils_1.createCancelledError)();
|
|
1321
|
+
return this.requestResponseAttempt(config, fullUrl, `Bearer ${refreshed}`, true, epoch, contextToken);
|
|
1223
1322
|
}
|
|
1224
|
-
this.
|
|
1225
|
-
this.notifyTokenChange();
|
|
1323
|
+
this.clearTokens();
|
|
1226
1324
|
}
|
|
1227
1325
|
const duration = Date.now() - startTime;
|
|
1228
1326
|
this.updateMetrics(response.ok, duration);
|
|
@@ -1233,8 +1331,12 @@ class HttpService {
|
|
|
1233
1331
|
const duration = Date.now() - startTime;
|
|
1234
1332
|
this.updateMetrics(false, duration);
|
|
1235
1333
|
this.config.onRequestEnd?.(config.url, config.method, duration, false);
|
|
1236
|
-
const normalizedError =
|
|
1334
|
+
const normalizedError = config.signal?.aborted || (0, errorUtils_1.isCancelledError)(error)
|
|
1335
|
+
? (0, errorUtils_1.createCancelledError)()
|
|
1336
|
+
: error instanceof Error ? error : new Error(String(error));
|
|
1237
1337
|
this.config.onRequestError?.(config.url, config.method, normalizedError);
|
|
1338
|
+
if ((0, errorUtils_1.isCancelledError)(normalizedError) || normalizedError instanceof OxyServices_errors_1.OxyAuthenticationError)
|
|
1339
|
+
throw normalizedError;
|
|
1238
1340
|
throw (0, errorUtils_1.handleHttpError)(normalizedError);
|
|
1239
1341
|
}
|
|
1240
1342
|
}
|
|
@@ -1256,6 +1358,9 @@ class HttpService {
|
|
|
1256
1358
|
}
|
|
1257
1359
|
// Token management
|
|
1258
1360
|
setTokens(accessToken) {
|
|
1361
|
+
const current = this.tokenStore.getAccessToken();
|
|
1362
|
+
if (current && !sameTokenSession(current, accessToken))
|
|
1363
|
+
this.sessionEpoch += 1;
|
|
1259
1364
|
this.sessionEnded = false;
|
|
1260
1365
|
this.tokenStore.setTokens(accessToken);
|
|
1261
1366
|
this.notifyTokenChange();
|
|
@@ -1264,17 +1369,19 @@ class HttpService {
|
|
|
1264
1369
|
* End the local session: clear the bearer and abandon every re-mint already
|
|
1265
1370
|
* in flight, so none of them can plant a token after the sign-out.
|
|
1266
1371
|
*
|
|
1267
|
-
* {@link clearTokens}
|
|
1268
|
-
*
|
|
1269
|
-
* refresh started a moment earlier would finish and plant a new one.
|
|
1372
|
+
* Unlike {@link clearTokens}, this also marks the session as ended and
|
|
1373
|
+
* invalidates pending re-mints even when no bearer is currently present.
|
|
1270
1374
|
*/
|
|
1271
1375
|
endSession() {
|
|
1272
|
-
this.
|
|
1376
|
+
if (!this.tokenStore.getAccessToken())
|
|
1377
|
+
this.sessionEpoch += 1;
|
|
1273
1378
|
this.sessionEnded = true;
|
|
1274
1379
|
this.clearTokens();
|
|
1275
1380
|
}
|
|
1276
1381
|
/**
|
|
1277
|
-
* The current
|
|
1382
|
+
* The current context epoch: account/session switches and bearer removal
|
|
1383
|
+
* invalidate pending work; rotation within the same context preserves it.
|
|
1384
|
+
* A re-mint lane that
|
|
1278
1385
|
* plants tokens itself (the device-secret arm) compares it across its awaits.
|
|
1279
1386
|
*/
|
|
1280
1387
|
getSessionEpoch() {
|
|
@@ -1301,6 +1408,8 @@ class HttpService {
|
|
|
1301
1408
|
this.anonymousAuthProvider = provider;
|
|
1302
1409
|
}
|
|
1303
1410
|
clearTokens() {
|
|
1411
|
+
if (this.tokenStore.getAccessToken())
|
|
1412
|
+
this.sessionEpoch += 1;
|
|
1304
1413
|
this.tokenStore.clearTokens();
|
|
1305
1414
|
// Drop the response cache on logout. The cache is identity-scoped, so a
|
|
1306
1415
|
// different user could never read these entries, but a logged-out client
|
package/dist/cjs/api/accounts.js
CHANGED
|
@@ -1,21 +1,41 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AccountsApi = exports.AccountMembersApi = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.isOperatorSwitchTargetKind = exports.isDelegatedActAsEligibleKind = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_KINDS = exports.ACCOUNT_CATEGORY_IDS = void 0;
|
|
3
|
+
exports.AccountsApi = exports.AccountAgentKeysApi = exports.AccountMembersApi = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.isOperatorSwitchTargetKind = exports.isDelegatedActAsEligibleKind = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_KINDS = exports.ACCOUNT_CATEGORY_IDS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `oxy.accounts` — the unified account graph (`/accounts`).
|
|
6
|
+
*
|
|
7
|
+
* An account is a relational, tree-structured principal (the `User` document
|
|
8
|
+
* generalised): a `personal` account is a human login at the root of its tree;
|
|
9
|
+
* `organization` / `project` / `bot` / `channel` accounts are non-login
|
|
10
|
+
* principals operated through membership. Accounts form a tree
|
|
11
|
+
* (`parentAccountId`) and expose a single membership model (`AccountMember`)
|
|
12
|
+
* with a unified role set and an explicit-but-inheritable cascade down the
|
|
13
|
+
* subtree. The applications an account owns are `oxy.apps`.
|
|
14
|
+
*
|
|
15
|
+
* Reference accounts by their `_id` (`accountId`, the underlying `User._id`)
|
|
16
|
+
* and members by their member `_id` — never by name, slug, or handle.
|
|
17
|
+
*
|
|
18
|
+
* SWITCHING INTO AN ACCOUNT: `accounts.actAs(accountId)` mints a REAL
|
|
19
|
+
* session for the target account and plants it as the active session — there is
|
|
20
|
+
* no per-request "acting-as" header. Identity is carried by the session/token,
|
|
21
|
+
* so a switch propagates through reload and cross-domain exactly like a login.
|
|
22
|
+
*/
|
|
23
|
+
const contracts_1 = require("@oxy.so/contracts");
|
|
4
24
|
const userIdentity_1 = require("../utils/userIdentity");
|
|
5
25
|
const identityCacheSweep_1 = require("../utils/identityCacheSweep");
|
|
6
26
|
const accountCacheSweep_1 = require("../utils/accountCacheSweep");
|
|
7
27
|
const MEDIUM_TTL = 2 * 60 * 1000;
|
|
8
28
|
const LONG_TTL = 5 * 60 * 1000;
|
|
9
29
|
const enc = encodeURIComponent;
|
|
10
|
-
var
|
|
11
|
-
Object.defineProperty(exports, "ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return
|
|
12
|
-
Object.defineProperty(exports, "ACCOUNT_KINDS", { enumerable: true, get: function () { return
|
|
13
|
-
Object.defineProperty(exports, "MAX_ACCOUNT_CATEGORIES", { enumerable: true, get: function () { return
|
|
14
|
-
Object.defineProperty(exports, "SELECTABLE_ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return
|
|
15
|
-
Object.defineProperty(exports, "isDelegatedActAsEligibleKind", { enumerable: true, get: function () { return
|
|
16
|
-
Object.defineProperty(exports, "isOperatorSwitchTargetKind", { enumerable: true, get: function () { return
|
|
17
|
-
Object.defineProperty(exports, "isSelectableAccountCategoryId", { enumerable: true, get: function () { return
|
|
18
|
-
Object.defineProperty(exports, "kindAcceptsAccountCategories", { enumerable: true, get: function () { return
|
|
30
|
+
var contracts_2 = require("@oxy.so/contracts");
|
|
31
|
+
Object.defineProperty(exports, "ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return contracts_2.ACCOUNT_CATEGORY_IDS; } });
|
|
32
|
+
Object.defineProperty(exports, "ACCOUNT_KINDS", { enumerable: true, get: function () { return contracts_2.ACCOUNT_KINDS; } });
|
|
33
|
+
Object.defineProperty(exports, "MAX_ACCOUNT_CATEGORIES", { enumerable: true, get: function () { return contracts_2.MAX_ACCOUNT_CATEGORIES; } });
|
|
34
|
+
Object.defineProperty(exports, "SELECTABLE_ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return contracts_2.SELECTABLE_ACCOUNT_CATEGORY_IDS; } });
|
|
35
|
+
Object.defineProperty(exports, "isDelegatedActAsEligibleKind", { enumerable: true, get: function () { return contracts_2.isDelegatedActAsEligibleKind; } });
|
|
36
|
+
Object.defineProperty(exports, "isOperatorSwitchTargetKind", { enumerable: true, get: function () { return contracts_2.isOperatorSwitchTargetKind; } });
|
|
37
|
+
Object.defineProperty(exports, "isSelectableAccountCategoryId", { enumerable: true, get: function () { return contracts_2.isSelectableAccountCategoryId; } });
|
|
38
|
+
Object.defineProperty(exports, "kindAcceptsAccountCategories", { enumerable: true, get: function () { return contracts_2.kindAcceptsAccountCategories; } });
|
|
19
39
|
/** `oxy.accounts.members` — who can act on an account, and with what role. */
|
|
20
40
|
class AccountMembersApi {
|
|
21
41
|
constructor(ctx) {
|
|
@@ -79,10 +99,29 @@ class AccountMembersApi {
|
|
|
79
99
|
}
|
|
80
100
|
}
|
|
81
101
|
exports.AccountMembersApi = AccountMembersApi;
|
|
102
|
+
/** Current account governors and autonomous rotation use action-bound proofs. */
|
|
103
|
+
class AccountAgentKeysApi {
|
|
104
|
+
constructor(ctx) {
|
|
105
|
+
this.ctx = ctx;
|
|
106
|
+
}
|
|
107
|
+
async list(accountId) {
|
|
108
|
+
return contracts_1.agentKeyListResponseSchema.parse(await this.ctx.request('GET', `/accounts/${enc(accountId)}/agent-keys`, undefined, { cache: false }));
|
|
109
|
+
}
|
|
110
|
+
async requestChallenge(accountId, operation) {
|
|
111
|
+
const response = await this.ctx.request('POST', `/accounts/${enc(accountId)}/agent-keys/challenge`, contracts_1.agentKeyOperationSchema.parse(operation), { cache: false });
|
|
112
|
+
return contracts_1.agentKeyOperationChallengeSchema.parse(response);
|
|
113
|
+
}
|
|
114
|
+
async execute(accountId, operation, proof) {
|
|
115
|
+
const response = await this.ctx.request('POST', `/accounts/${enc(accountId)}/agent-keys/execute`, contracts_1.executeAgentKeyOperationSchema.parse({ operation, proof }), { cache: false });
|
|
116
|
+
return contracts_1.agentKeyOperationResultSchema.parse(response);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.AccountAgentKeysApi = AccountAgentKeysApi;
|
|
82
120
|
class AccountsApi {
|
|
83
121
|
constructor(ctx) {
|
|
84
122
|
this.ctx = ctx;
|
|
85
123
|
this.members = new AccountMembersApi(ctx);
|
|
124
|
+
this.agentKeys = new AccountAgentKeysApi(ctx);
|
|
86
125
|
}
|
|
87
126
|
/**
|
|
88
127
|
* List the accounts the caller can access: their own personal (root)
|
package/dist/cjs/api/apps.js
CHANGED
|
@@ -172,10 +172,10 @@ class AppsApi {
|
|
|
172
172
|
* consent, authorize, and device-flow approval screens before any session
|
|
173
173
|
* exists.
|
|
174
174
|
*/
|
|
175
|
-
async getPublic(clientId) {
|
|
175
|
+
async getPublic(clientId, options = {}) {
|
|
176
176
|
const res = await this.ctx.request('GET', `/auth/oauth/client/${enc(clientId)}`, undefined,
|
|
177
177
|
// Public client metadata (pre-session consent UI) — skip the bearer preflight.
|
|
178
|
-
{ cache: true, cacheTTL: MEDIUM_TTL, skipAuth: true });
|
|
178
|
+
{ cache: options.cache ?? true, cacheTTL: MEDIUM_TTL, skipAuth: true });
|
|
179
179
|
return res.application;
|
|
180
180
|
}
|
|
181
181
|
/**
|
package/dist/cjs/api/auth.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.AuthApi = exports.AuthCommonsApi = exports.AuthOAuthApi = exports.AuthTotpApi = exports.AuthPasswordApi = exports.AuthEmailApi = void 0;
|
|
36
|
+
exports.AuthApi = exports.AuthAgentApi = exports.AuthCommonsApi = exports.AuthOAuthApi = exports.AuthTotpApi = exports.AuthPasswordApi = exports.AuthEmailApi = void 0;
|
|
37
37
|
const contracts_1 = require("@oxy.so/contracts");
|
|
38
38
|
const OxyServices_errors_1 = require("../OxyServices.errors");
|
|
39
39
|
const logger_1 = require("../logger");
|
|
@@ -303,7 +303,8 @@ class AuthOAuthApi {
|
|
|
303
303
|
* out, so the app can run the OAuth flow again. It never degrades into a
|
|
304
304
|
* session that looks alive and cannot refresh.
|
|
305
305
|
*/
|
|
306
|
-
|
|
306
|
+
/** `plantTokens: false` defers bearer installation to a validated provider commit. */
|
|
307
|
+
async exchangeCode(params, options = {}) {
|
|
307
308
|
const form = new URLSearchParams({
|
|
308
309
|
grant_type: 'authorization_code',
|
|
309
310
|
code: params.code,
|
|
@@ -334,7 +335,9 @@ class AuthOAuthApi {
|
|
|
334
335
|
}
|
|
335
336
|
const expiresInSec = typeof record.expires_in === 'number' ? record.expires_in : DEFAULT_ACCESS_TOKEN_TTL_SECONDS;
|
|
336
337
|
const expiresAt = new Date(Date.now() + expiresInSec * 1000).toISOString();
|
|
337
|
-
|
|
338
|
+
// Provider completion validates the expected subject before committing.
|
|
339
|
+
// Direct callers retain the historical token-planting default.
|
|
340
|
+
if (accessToken && options.plantTokens !== false) {
|
|
338
341
|
this.ctx.oxy.session.setAccessToken(accessToken);
|
|
339
342
|
}
|
|
340
343
|
if (!deviceId || !deviceSecret) {
|
|
@@ -683,6 +686,23 @@ class AuthCommonsApi {
|
|
|
683
686
|
}
|
|
684
687
|
}
|
|
685
688
|
exports.AuthCommonsApi = AuthCommonsApi;
|
|
689
|
+
/** Bot entry uses caller-owned keys and the normal account session lane. */
|
|
690
|
+
class AuthAgentApi {
|
|
691
|
+
constructor(ctx) {
|
|
692
|
+
this.ctx = ctx;
|
|
693
|
+
}
|
|
694
|
+
async requestChallenge(publicKey) {
|
|
695
|
+
const body = contracts_1.agentChallengeRequestSchema.parse({ publicKey });
|
|
696
|
+
const response = await this.ctx.request('POST', '/auth/agent/challenge', body, { cache: false, skipAuth: true });
|
|
697
|
+
return contracts_1.agentProofClaimsSchema.parse(response);
|
|
698
|
+
}
|
|
699
|
+
async verify(publicKey, proof) {
|
|
700
|
+
const body = contracts_1.agentVerifyRequestSchema.parse({ publicKey, ...proof });
|
|
701
|
+
const response = await this.ctx.request('POST', '/auth/agent/verify', body, { cache: false, skipAuth: true });
|
|
702
|
+
return plantSession(this.ctx, response, 'auth/agent/verify');
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
exports.AuthAgentApi = AuthAgentApi;
|
|
686
706
|
class AuthApi {
|
|
687
707
|
constructor(ctx) {
|
|
688
708
|
this.ctx = ctx;
|
|
@@ -691,6 +711,7 @@ class AuthApi {
|
|
|
691
711
|
this.totp = new AuthTotpApi(ctx);
|
|
692
712
|
this.oauth = new AuthOAuthApi(ctx);
|
|
693
713
|
this.commons = new AuthCommonsApi(ctx);
|
|
714
|
+
this.agent = new AuthAgentApi(ctx);
|
|
694
715
|
}
|
|
695
716
|
/**
|
|
696
717
|
* Register a new identity with public key authentication
|
package/dist/cjs/api/billing.js
CHANGED
|
@@ -1,12 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BillingApi = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `oxy.billing` — the signed-in user's subscription, wallet and payment history.
|
|
6
|
+
*
|
|
7
|
+
* Every call reads the SIGNED-IN user's data unless a `userId` is passed (the
|
|
8
|
+
* API refuses any other user's). Checkout, plan changes and cancellation live
|
|
9
|
+
* under `/billing` (Stripe) and are not wrapped here yet.
|
|
10
|
+
*
|
|
11
|
+
* The types model the JSON these routes actually return (`subscription.controller`,
|
|
12
|
+
* `payment.controller`, `wallet.routes` in `packages/api`); `Date` columns
|
|
13
|
+
* arrive as ISO-8601 strings.
|
|
14
|
+
*/
|
|
15
|
+
const contracts_1 = require("@oxy.so/contracts");
|
|
16
|
+
const contracts_2 = require("@oxy.so/contracts");
|
|
4
17
|
const SUBSCRIPTION_TTL = 2 * 60 * 1000;
|
|
5
18
|
const WALLET_TTL = 60 * 1000;
|
|
6
19
|
class BillingApi {
|
|
7
20
|
constructor(ctx) {
|
|
8
21
|
this.ctx = ctx;
|
|
9
22
|
}
|
|
23
|
+
/** Product rights only; requires a production application-bound user session. Never cached. */
|
|
24
|
+
async productAccess(query) {
|
|
25
|
+
const parsed = contracts_1.subjectProductAccessQuerySchema.parse(query);
|
|
26
|
+
const response = await this.ctx.request('GET', `/v1/products/${encodeURIComponent(parsed.productId)}/access/${encodeURIComponent(parsed.subjectAccountId)}`, undefined, { cache: false });
|
|
27
|
+
return contracts_1.subjectProductAccessSchema.parse(response);
|
|
28
|
+
}
|
|
29
|
+
/** Named commercial sources for the current subject; no inferred plan collapse. */
|
|
30
|
+
async productSubscriptions() {
|
|
31
|
+
return contracts_2.productSubscriptionsResponseSchema.parse(await this.ctx.request("GET", "/billing/product-subscriptions", undefined, { cache: false })).subscriptions;
|
|
32
|
+
}
|
|
33
|
+
/** Per-grant credit provenance; no interpretation of the historical mixed balance. */
|
|
34
|
+
async creditGrants() {
|
|
35
|
+
return contracts_2.subscriptionCreditGrantsResponseSchema.parse(await this.ctx.request("GET", "/billing/credit-grants", undefined, { cache: false })).grants;
|
|
36
|
+
}
|
|
37
|
+
/** Payer-only named-source cancellation. Resolves on provider acceptance, including pending local reconciliation. Use cancelProductSubscriptionWithStatus for detailed completion state. */
|
|
38
|
+
async cancelProductSubscription(sourceId, expectedSubjectAccountId) {
|
|
39
|
+
await this.ctx.request("POST", "/billing/product-subscriptions/cancel", contracts_2.cancelProductSubscriptionSchema.parse({ sourceId, ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }), { cache: false });
|
|
40
|
+
}
|
|
41
|
+
/** Provider acceptance versus completed local cancellation, for user-facing flows. */
|
|
42
|
+
async cancelProductSubscriptionWithStatus(sourceId, expectedSubjectAccountId) {
|
|
43
|
+
return contracts_2.productSubscriptionCancellationResultSchema.parse(await this.ctx.request('POST', '/billing/product-subscriptions/cancel', contracts_2.cancelProductSubscriptionSchema.parse({ sourceId, ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }), { cache: false }));
|
|
44
|
+
}
|
|
10
45
|
/** The signed-in user's payment history. Never cached. */
|
|
11
46
|
async payments() {
|
|
12
47
|
return this.ctx.request('GET', '/payments/user', undefined, { cache: false });
|
|
@@ -49,7 +49,6 @@ const logger_1 = require("../logger");
|
|
|
49
49
|
const cacheKey_1 = require("../utils/cacheKey");
|
|
50
50
|
const refresh_1 = require("../session/refresh");
|
|
51
51
|
const identitySession_1 = require("../session/identitySession");
|
|
52
|
-
const sharedDeviceCredential_1 = require("../session/sharedDeviceCredential");
|
|
53
52
|
/**
|
|
54
53
|
* Run the device-first cold boot. Resolves to the `runColdBoot` outcome and, as
|
|
55
54
|
* a side effect, invokes `onSession` (winning session, token already planted) or
|
|
@@ -73,6 +72,8 @@ async function runSessionColdBoot(opts) {
|
|
|
73
72
|
// Boot-local (not module-level) so it cannot leak across boots or break under
|
|
74
73
|
// bundler re-evaluation.
|
|
75
74
|
let signedOutReason = 'no_session';
|
|
75
|
+
let rejectedLocalHolder = false;
|
|
76
|
+
let accountRecoverySuperseded = false;
|
|
76
77
|
// Boot-local memo for the identity pin. Resolved lazily INSIDE a step so the
|
|
77
78
|
// (local, but storage-backed) read is covered by `overallDeadlineMs`, and
|
|
78
79
|
// resolved at most once per boot so two steps cannot disagree. Always `null`
|
|
@@ -104,6 +105,8 @@ async function runSessionColdBoot(opts) {
|
|
|
104
105
|
steps.push({
|
|
105
106
|
id: 'warm-token-plant',
|
|
106
107
|
run: async () => {
|
|
108
|
+
if (identityBinding === null && isNative && await store.isAutomaticIdentitySignInSuppressed?.())
|
|
109
|
+
return { kind: 'skip' };
|
|
107
110
|
const persisted = await store.load();
|
|
108
111
|
if (!persisted?.accessToken || !persisted.sessionId || !persisted.userId || !persisted.expiresAt) {
|
|
109
112
|
return { kind: 'skip' };
|
|
@@ -176,6 +179,11 @@ async function runSessionColdBoot(opts) {
|
|
|
176
179
|
},
|
|
177
180
|
};
|
|
178
181
|
case 'invalid-secret': {
|
|
182
|
+
if (identityBinding === null && store.setAutomaticIdentitySignInSuppressed && await store.isAutomaticIdentitySignInSuppressed?.()) {
|
|
183
|
+
rejectedLocalHolder = true;
|
|
184
|
+
signedOutReason = 'no_session';
|
|
185
|
+
return { kind: 'skip' };
|
|
186
|
+
}
|
|
179
187
|
// Stale/diverged secret — drop it so the mint lane stops firing. On
|
|
180
188
|
// native the Commons-proof step below can still recover; on web this ends
|
|
181
189
|
// signed out. Setting it undefined drops the key on the store's JSON
|
|
@@ -187,6 +195,7 @@ async function runSessionColdBoot(opts) {
|
|
|
187
195
|
return { kind: 'skip' };
|
|
188
196
|
}
|
|
189
197
|
case 'no-session':
|
|
198
|
+
rejectedLocalHolder = identityBinding === null;
|
|
190
199
|
// Device known, no live session — authoritative signed-out. Keep the
|
|
191
200
|
// secret (the device may sign in again).
|
|
192
201
|
signedOutReason = 'no_session';
|
|
@@ -208,7 +217,10 @@ async function runSessionColdBoot(opts) {
|
|
|
208
217
|
logger_1.logger.debug('device-secret mint failed (transient) — keeping secret', { component: 'sessionColdBoot', method: 'device-secret-mint' });
|
|
209
218
|
return { kind: 'skip' };
|
|
210
219
|
case 'session-ended':
|
|
211
|
-
//
|
|
220
|
+
// The prior holder/epoch was superseded. This boot must not turn a
|
|
221
|
+
// late rejection into a fresh Commons sign-in; a new boot can reconcile.
|
|
222
|
+
if (identityBinding === null)
|
|
223
|
+
accountRecoverySuperseded = true;
|
|
212
224
|
return { kind: 'skip' };
|
|
213
225
|
case 'no-secret':
|
|
214
226
|
return { kind: 'skip' };
|
|
@@ -273,63 +285,16 @@ async function runSessionColdBoot(opts) {
|
|
|
273
285
|
// The adoption itself is local, but it is only worth committing alongside
|
|
274
286
|
// a mint that proves the credential — so the whole lane is online-gated
|
|
275
287
|
// like every other network step.
|
|
276
|
-
enabled: () => isNative && !isOffline(),
|
|
288
|
+
enabled: () => isNative && !isOffline() && !accountRecoverySuperseded,
|
|
277
289
|
run: async () => {
|
|
278
|
-
const
|
|
279
|
-
const decision = (0, sharedDeviceCredential_1.decideSharedDeviceJoin)(before, await sharedSlot.read());
|
|
280
|
-
if (decision.action === 'skip') {
|
|
281
|
-
logger_1.logger.debug(`shared device credential not adopted (${decision.reason})`, { component: 'sessionColdBoot', method: 'shared-device-adopt' });
|
|
282
|
-
return { kind: 'skip' };
|
|
283
|
-
}
|
|
284
|
-
// Restore the store to exactly what it held before this lane touched it.
|
|
285
|
-
// A credential we adopted and could not prove must not be left behind for
|
|
286
|
-
// the next boot's mint lane to keep retrying.
|
|
287
|
-
const revert = async () => {
|
|
288
|
-
if (before) {
|
|
289
|
-
await store.save(before);
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
await store.clear();
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
const adopted = {
|
|
296
|
-
// The mint fills both in from the device's live state; carrying the
|
|
297
|
-
// previous session's ids into a different device session would be a
|
|
298
|
-
// lie for however long the mint takes.
|
|
299
|
-
sessionId: '',
|
|
300
|
-
userId: '',
|
|
301
|
-
deviceId: decision.credential.deviceId,
|
|
302
|
-
deviceSecret: decision.credential.deviceSecret,
|
|
303
|
-
};
|
|
304
|
-
if (!(await store.save(adopted))) {
|
|
305
|
-
logger_1.logger.error('adopted the shared device credential but it could not be durably persisted — reverting', undefined, { component: 'sessionColdBoot', method: 'shared-device-adopt' });
|
|
306
|
-
await revert();
|
|
307
|
-
return { kind: 'skip' };
|
|
308
|
-
}
|
|
309
|
-
const result = await (0, refresh_1.refreshDeviceSecretArm)({ oxy, store, pin: null });
|
|
290
|
+
const result = await (0, refresh_1.refreshSharedDeviceArm)({ oxy, store, shared: sharedSlot, rejectedLocalHolder });
|
|
310
291
|
if (result.status === 'ok') {
|
|
311
|
-
return {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
sessionId: result.sessionId,
|
|
315
|
-
userId: result.userId,
|
|
316
|
-
accessToken: result.token,
|
|
317
|
-
},
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
if (result.status === 'invalid-secret') {
|
|
321
|
-
// The one place we hold POSITIVE proof that the exact bytes in the
|
|
322
|
-
// shared slot are dead — the server rejected them by name. Clearing it
|
|
323
|
-
// signs nobody out (a credential the server does not recognise cannot
|
|
324
|
-
// be minting for anyone) and it is what stops a dead credential from
|
|
325
|
-
// blocking every future install: a stale slot owned by a different
|
|
326
|
-
// `deviceId` is otherwise never overwritten, by design.
|
|
327
|
-
await sharedSlot.clear();
|
|
292
|
+
return { kind: 'session', session: {
|
|
293
|
+
sessionId: result.sessionId, userId: result.userId, accessToken: result.token, state: result.state,
|
|
294
|
+
} };
|
|
328
295
|
}
|
|
329
|
-
|
|
296
|
+
if (result.status === 'no-session')
|
|
330
297
|
signedOutReason = 'no_session';
|
|
331
|
-
}
|
|
332
|
-
await revert();
|
|
333
298
|
return { kind: 'skip' };
|
|
334
299
|
},
|
|
335
300
|
});
|
|
@@ -357,8 +322,11 @@ async function runSessionColdBoot(opts) {
|
|
|
357
322
|
id: 'commons-proof-signin',
|
|
358
323
|
enabled: () => isNative && !isOffline(),
|
|
359
324
|
run: async () => {
|
|
360
|
-
|
|
361
|
-
|
|
325
|
+
if (accountRecoverySuperseded || await store.isAutomaticIdentitySignInSuppressed?.())
|
|
326
|
+
return { kind: 'skip' };
|
|
327
|
+
const epoch = oxy.http.getSessionEpoch();
|
|
328
|
+
const session = await oxy.auth.signInWithCommonsIdentity({ plantTokens: false, requestOptions: { retry: false } });
|
|
329
|
+
if (!session?.accessToken || oxy.http.getSessionEpoch() !== epoch || await store.isAutomaticIdentitySignInSuppressed?.()) {
|
|
362
330
|
return { kind: 'skip' };
|
|
363
331
|
}
|
|
364
332
|
// `verifyChallenge` issues a deviceSecret; persist it so the next
|
|
@@ -374,6 +342,9 @@ async function runSessionColdBoot(opts) {
|
|
|
374
342
|
expiresAt: session.expiresAt,
|
|
375
343
|
});
|
|
376
344
|
}
|
|
345
|
+
if (oxy.http.getSessionEpoch() !== epoch || await store.isAutomaticIdentitySignInSuppressed?.())
|
|
346
|
+
return { kind: 'skip' };
|
|
347
|
+
oxy.session.setAccessToken(session.accessToken);
|
|
377
348
|
return {
|
|
378
349
|
kind: 'session',
|
|
379
350
|
session: {
|