@oxy.so/core 4.1.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/cjs/HttpService.js +142 -33
  2. package/dist/cjs/api/accounts.js +49 -10
  3. package/dist/cjs/api/apps.js +2 -2
  4. package/dist/cjs/api/auth.js +24 -3
  5. package/dist/cjs/api/billing.js +35 -0
  6. package/dist/cjs/boot/sessionColdBoot.js +28 -57
  7. package/dist/cjs/inference/OxyInferenceClient.js +34 -15
  8. package/dist/cjs/server/OxyServer.js +107 -51
  9. package/dist/cjs/server/agentAccount.js +19 -0
  10. package/dist/cjs/server/auth.js +19 -0
  11. package/dist/cjs/server/capabilityTicket.js +3 -41
  12. package/dist/cjs/server/index.js +10 -2
  13. package/dist/cjs/server/liveCapabilityTicket.js +48 -0
  14. package/dist/cjs/server/middleware.js +60 -18
  15. package/dist/cjs/server/namespaces.js +57 -0
  16. package/dist/cjs/server/serviceTokenLifetime.js +13 -0
  17. package/dist/cjs/session/SessionClient.js +90 -12
  18. package/dist/cjs/session/authStateStore.js +130 -39
  19. package/dist/cjs/session/refresh.js +152 -30
  20. package/dist/cjs/session/sharedDeviceCredential.js +17 -1
  21. package/dist/cjs/utils/officialOrigins.js +7 -18
  22. package/dist/esm/HttpService.js +143 -34
  23. package/dist/esm/api/accounts.js +38 -0
  24. package/dist/esm/api/apps.js +2 -2
  25. package/dist/esm/api/auth.js +23 -3
  26. package/dist/esm/api/billing.js +35 -0
  27. package/dist/esm/boot/sessionColdBoot.js +29 -58
  28. package/dist/esm/inference/OxyInferenceClient.js +32 -13
  29. package/dist/esm/server/OxyServer.js +107 -51
  30. package/dist/esm/server/agentAccount.js +16 -0
  31. package/dist/esm/server/auth.js +18 -0
  32. package/dist/esm/server/capabilityTicket.js +1 -39
  33. package/dist/esm/server/index.js +4 -1
  34. package/dist/esm/server/liveCapabilityTicket.js +45 -0
  35. package/dist/esm/server/middleware.js +60 -18
  36. package/dist/esm/server/namespaces.js +57 -0
  37. package/dist/esm/server/serviceTokenLifetime.js +9 -0
  38. package/dist/esm/session/SessionClient.js +90 -12
  39. package/dist/esm/session/authStateStore.js +128 -38
  40. package/dist/esm/session/refresh.js +151 -30
  41. package/dist/esm/session/sharedDeviceCredential.js +17 -1
  42. package/dist/esm/utils/officialOrigins.js +2 -13
  43. package/dist/types/HttpService.d.ts +26 -12
  44. package/dist/types/api/accounts.d.ts +38 -0
  45. package/dist/types/api/agency.d.ts +3 -1
  46. package/dist/types/api/apps.d.ts +3 -1
  47. package/dist/types/api/auth.d.ts +12 -1
  48. package/dist/types/api/billing.d.ts +12 -0
  49. package/dist/types/api/session.d.ts +3 -1
  50. package/dist/types/boot/sessionColdBoot.d.ts +1 -1
  51. package/dist/types/index.d.ts +1 -1
  52. package/dist/types/inference/OxyInferenceClient.d.ts +18 -0
  53. package/dist/types/inference/index.d.ts +1 -1
  54. package/dist/types/server/OxyServer.d.ts +16 -9
  55. package/dist/types/server/agentAccount.d.ts +14 -0
  56. package/dist/types/server/auth.d.ts +18 -0
  57. package/dist/types/server/capabilityTicket.d.ts +2 -4
  58. package/dist/types/server/index.d.ts +7 -1
  59. package/dist/types/server/liveCapabilityTicket.d.ts +15 -0
  60. package/dist/types/server/middleware.d.ts +15 -6
  61. package/dist/types/server/namespaces.d.ts +78 -2
  62. package/dist/types/server/serviceTokenLifetime.d.ts +7 -0
  63. package/dist/types/session/SessionClient.d.ts +8 -0
  64. package/dist/types/session/authStateStore.d.ts +15 -0
  65. package/dist/types/session/index.d.ts +1 -1
  66. package/dist/types/session/refresh.d.ts +32 -7
  67. package/dist/types/session/sharedDeviceCredential.d.ts +3 -1
  68. package/dist/types/utils/officialOrigins.d.ts +1 -5
  69. package/package.json +6 -6
  70. package/src/HttpService.ts +138 -50
  71. package/src/api/accounts.ts +22 -0
  72. package/src/api/agency.ts +3 -1
  73. package/src/api/apps.ts +2 -2
  74. package/src/api/auth.ts +31 -2
  75. package/src/api/billing.ts +71 -9
  76. package/src/api/session.ts +3 -1
  77. package/src/boot/sessionColdBoot.ts +27 -72
  78. package/src/index.ts +1 -1
  79. package/src/inference/OxyInferenceClient.ts +35 -5
  80. package/src/inference/index.ts +1 -0
  81. package/src/server/OxyServer.ts +114 -53
  82. package/src/server/agentAccount.ts +28 -0
  83. package/src/server/auth.ts +25 -0
  84. package/src/server/capabilityTicket.ts +2 -42
  85. package/src/server/index.ts +13 -0
  86. package/src/server/liveCapabilityTicket.ts +47 -0
  87. package/src/server/middleware.ts +72 -25
  88. package/src/server/namespaces.ts +99 -2
  89. package/src/server/serviceTokenLifetime.ts +13 -0
  90. package/src/session/SessionClient.ts +85 -12
  91. package/src/session/authStateStore.ts +130 -46
  92. package/src/session/index.ts +1 -0
  93. package/src/session/refresh.ts +144 -28
  94. package/src/session/sharedDeviceCredential.ts +14 -2
  95. package/src/utils/officialOrigins.ts +2 -13
@@ -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.tokenStore.setTokens(providedToken);
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
- // The session ended while this re-mint was in flight. Not a failure
1018
- // (no cooldown), and nothing to plant.
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.tokenStore.setTokens(newToken);
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
- const authHeader = await this.getAuthHeader();
1187
- if (!authHeader) {
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.requestAuthenticatedResponseAttempt(config, authHeader, false);
1251
+ return this.requestResponseAttempt(request, target.href, authHeader, false, this.sessionEpoch, contextToken);
1191
1252
  }
1192
- async requestAuthenticatedResponseAttempt(config, authHeader, isAuthRetry) {
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
- // Authentication is owned by this SDK instance. A caller cannot replace
1199
- // the bearer with a different session or leak one across linked apps.
1200
- headers.set('Authorization', authHeader);
1201
- // Never awaited: the PoP is a hint, and waiting on its trace fetch put up
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 fullUrl = this.buildURL(config.url);
1211
- const response = await fetch(fullUrl, {
1279
+ const body = config.method === 'GET' ? undefined : config.body;
1280
+ const init = {
1212
1281
  method: config.method,
1213
1282
  headers,
1214
- body: config.method === 'GET' ? undefined : config.body,
1283
+ body,
1215
1284
  signal: config.signal,
1216
- credentials: this.getCredentialsMode(fullUrl),
1217
- });
1218
- if (response.status === 401 && !isAuthRetry) {
1219
- const refreshed = await this.refreshAccessToken('response-401');
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
- return this.requestAuthenticatedResponseAttempt(config, `Bearer ${refreshed}`, true);
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.tokenStore.clearTokens();
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 = error instanceof Error ? error : new Error(String(error));
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} alone only drops the bearer, which is right for a
1268
- * mirror (a linked client following its parent) but not for a sign-out: a
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.sessionEpoch += 1;
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 session epoch — see {@link endSession}. A re-mint lane that
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
@@ -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 contracts_1 = require("@oxy.so/contracts");
11
- Object.defineProperty(exports, "ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return contracts_1.ACCOUNT_CATEGORY_IDS; } });
12
- Object.defineProperty(exports, "ACCOUNT_KINDS", { enumerable: true, get: function () { return contracts_1.ACCOUNT_KINDS; } });
13
- Object.defineProperty(exports, "MAX_ACCOUNT_CATEGORIES", { enumerable: true, get: function () { return contracts_1.MAX_ACCOUNT_CATEGORIES; } });
14
- Object.defineProperty(exports, "SELECTABLE_ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return contracts_1.SELECTABLE_ACCOUNT_CATEGORY_IDS; } });
15
- Object.defineProperty(exports, "isDelegatedActAsEligibleKind", { enumerable: true, get: function () { return contracts_1.isDelegatedActAsEligibleKind; } });
16
- Object.defineProperty(exports, "isOperatorSwitchTargetKind", { enumerable: true, get: function () { return contracts_1.isOperatorSwitchTargetKind; } });
17
- Object.defineProperty(exports, "isSelectableAccountCategoryId", { enumerable: true, get: function () { return contracts_1.isSelectableAccountCategoryId; } });
18
- Object.defineProperty(exports, "kindAcceptsAccountCategories", { enumerable: true, get: function () { return contracts_1.kindAcceptsAccountCategories; } });
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)
@@ -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
  /**
@@ -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
- async exchangeCode(params) {
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
- if (accessToken) {
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
@@ -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
- // Signed out while the boot was minting; the arm planted nothing.
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 before = await store.load();
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
- kind: 'session',
313
- session: {
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
- else if (result.status === 'no-session') {
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
- const session = await oxy.auth.signInWithCommonsIdentity({ requestOptions: { retry: false } });
361
- if (!session?.accessToken) {
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: {