@oxyhq/core 21.0.0 → 21.0.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.
Files changed (57) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +47 -8
  3. package/dist/cjs/i18n/locales/en-US.json +7 -2
  4. package/dist/cjs/i18n/locales/es-ES.json +7 -2
  5. package/dist/cjs/i18n/locales/locales/en-US.json +7 -2
  6. package/dist/cjs/i18n/locales/locales/es-ES.json +7 -2
  7. package/dist/cjs/index.js +8 -1
  8. package/dist/cjs/inference/OxyInferenceClient.js +330 -0
  9. package/dist/cjs/mixins/OxyServices.accounts.js +5 -72
  10. package/dist/cjs/mixins/OxyServices.inference.js +59 -0
  11. package/dist/cjs/mixins/OxyServices.utility.js +18 -6
  12. package/dist/cjs/mixins/index.js +6 -0
  13. package/dist/cjs/server/auth.js +76 -0
  14. package/dist/cjs/server/index.js +5 -1
  15. package/dist/esm/.tsbuildinfo +1 -1
  16. package/dist/esm/HttpService.js +47 -8
  17. package/dist/esm/i18n/locales/en-US.json +7 -2
  18. package/dist/esm/i18n/locales/es-ES.json +7 -2
  19. package/dist/esm/i18n/locales/locales/en-US.json +7 -2
  20. package/dist/esm/i18n/locales/locales/es-ES.json +7 -2
  21. package/dist/esm/index.js +4 -0
  22. package/dist/esm/inference/OxyInferenceClient.js +325 -0
  23. package/dist/esm/mixins/OxyServices.accounts.js +5 -72
  24. package/dist/esm/mixins/OxyServices.inference.js +56 -0
  25. package/dist/esm/mixins/OxyServices.utility.js +18 -6
  26. package/dist/esm/mixins/index.js +6 -0
  27. package/dist/esm/server/auth.js +72 -0
  28. package/dist/esm/server/index.js +1 -1
  29. package/dist/types/.tsbuildinfo +1 -1
  30. package/dist/types/HttpService.d.ts +39 -1
  31. package/dist/types/index.d.ts +3 -1
  32. package/dist/types/inference/OxyInferenceClient.d.ts +324 -0
  33. package/dist/types/mixins/OxyServices.accounts.d.ts +73 -95
  34. package/dist/types/mixins/OxyServices.inference.d.ts +95 -0
  35. package/dist/types/mixins/OxyServices.utility.d.ts +44 -13
  36. package/dist/types/mixins/index.d.ts +2 -1
  37. package/dist/types/server/auth.d.ts +80 -0
  38. package/dist/types/server/index.d.ts +2 -2
  39. package/package.json +2 -2
  40. package/src/HttpService.ts +50 -10
  41. package/src/__tests__/httpServiceUnwrapEnvelope.test.ts +115 -0
  42. package/src/i18n/locales/en-US.json +7 -2
  43. package/src/i18n/locales/es-ES.json +7 -2
  44. package/src/index.ts +19 -7
  45. package/src/inference/OxyInferenceClient.ts +590 -0
  46. package/src/inference/__tests__/OxyInferenceClient.test.ts +383 -0
  47. package/src/mixins/OxyServices.accounts.ts +75 -176
  48. package/src/mixins/OxyServices.inference.ts +57 -0
  49. package/src/mixins/OxyServices.utility.ts +58 -14
  50. package/src/mixins/__tests__/accounts.test.ts +57 -102
  51. package/src/mixins/__tests__/inferenceFactory.test.ts +58 -0
  52. package/src/mixins/__tests__/serviceAuth.test.ts +2 -0
  53. package/src/mixins/index.ts +8 -0
  54. package/src/server/__tests__/serviceTokenAttribution.test.ts +396 -0
  55. package/src/server/auth.ts +118 -0
  56. package/src/server/index.ts +6 -0
  57. package/src/session/__tests__/accountDialogShape.test.ts +118 -0
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * The inference API, reached with whatever bearer this session already holds
4
+ * (issue #972, workstream 15).
5
+ *
6
+ * ```typescript
7
+ * const models = await oxyServices.inference().listModels();
8
+ * ```
9
+ *
10
+ * One method, and it is a FACTORY rather than a set of inference methods on
11
+ * `OxyServices`. The calls themselves live once, in
12
+ * {@link OxyInferenceClient} — which an external developer holding only an
13
+ * `oxy_sk_…` machine key constructs directly, with no Oxy session anywhere in
14
+ * the picture. Declaring the same calls a second time here would give the
15
+ * ecosystem two spellings of one request, and only one of them would stay
16
+ * correct.
17
+ *
18
+ * This is the reasoning `createLinkedClient` is already built on: the plumbing
19
+ * that binds an Oxy bearer to a client belongs in core, once, rather than in
20
+ * each app.
21
+ *
22
+ * The credential is a FUNCTION, not the current token: a session bearer rotates
23
+ * on refresh and on account switch, and a client that captured one at
24
+ * construction would start answering 401 an hour into the process's life.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.OxyServicesInferenceMixin = OxyServicesInferenceMixin;
28
+ const OxyInferenceClient_1 = require("../inference/OxyInferenceClient");
29
+ function OxyServicesInferenceMixin(Base) {
30
+ return class extends Base {
31
+ constructor() {
32
+ super(...arguments);
33
+ /** @internal Memoized so repeated calls return one object identity. */
34
+ this._inferenceClient = null;
35
+ }
36
+ /**
37
+ * The inference client for this session.
38
+ *
39
+ * Bound to this instance's base URL and to `getAccessToken()`, so it
40
+ * follows every refresh, sign-in and account switch without being
41
+ * rebuilt.
42
+ *
43
+ * A service-authenticated process wants a different credential and
44
+ * builds {@link OxyInferenceClient} directly:
45
+ * `new OxyInferenceClient({ credential: () => oxy.getServiceToken() })`.
46
+ * The mint is asynchronous and cached, which is exactly what a
47
+ * credential function is for.
48
+ */
49
+ inference() {
50
+ if (this._inferenceClient === null) {
51
+ this._inferenceClient = new OxyInferenceClient_1.OxyInferenceClient({
52
+ baseURL: this.getBaseURL(),
53
+ credential: () => this.getAccessToken(),
54
+ });
55
+ }
56
+ return this._inferenceClient;
57
+ }
58
+ };
59
+ }
@@ -152,7 +152,7 @@ function OxyServicesUtilityMixin(Base) {
152
152
  * additionally checked for `aud`, `iss`, and `type` claims to prevent
153
153
  * cross-token-type confusion attacks.
154
154
  * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
155
- * direct access to `SERVICE_TOKEN_SECRET` / `ACCESS_TOKEN_SECRET`.
155
+ * direct access to `ACCESS_TOKEN_SECRET`.
156
156
  *
157
157
  * **Why session-less user tokens are refused rather than trusted:**
158
158
  * every user access token the Oxy API issues carries a `sessionId` (see
@@ -184,7 +184,7 @@ function OxyServicesUtilityMixin(Base) {
184
184
  * const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
185
185
  *
186
186
  * // Protect all routes under /protected
187
- * app.use('/protected', oxy.auth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }));
187
+ * app.use('/protected', oxy.auth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }));
188
188
  *
189
189
  * // Access user in route handler
190
190
  * app.get('/protected/me', (req, res) => {
@@ -198,7 +198,7 @@ function OxyServicesUtilityMixin(Base) {
198
198
  * app.use('/public', oxy.auth({ optional: true }));
199
199
  *
200
200
  * // Require a specific scope on a service-token-protected route
201
- * app.use('/internal/files', oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }), oxy.requireScope('files:write'));
201
+ * app.use('/internal/files', oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }), oxy.requireScope('files:write'));
202
202
  * ```
203
203
  *
204
204
  * @param options Optional configuration
@@ -363,13 +363,19 @@ function OxyServicesUtilityMixin(Base) {
363
363
  return onError(error);
364
364
  return res.status(401).json(error);
365
365
  }
366
- // Validate required service token fields
366
+ // Validate required service token fields. All of them are
367
+ // required, `ownerAccountId` included: an optional billing
368
+ // principal is one fallback away from being resolved from the
369
+ // delegated user, which is the exact confusion ADR 0007 forbids.
367
370
  const appId = decoded.appId;
368
371
  const credentialId = decoded.credentialId;
372
+ const ownerAccountId = decoded.ownerAccountId;
369
373
  const environment = decoded.environment;
370
374
  if (!appId ||
371
375
  typeof credentialId !== 'string' ||
372
376
  credentialId.length === 0 ||
377
+ typeof ownerAccountId !== 'string' ||
378
+ ownerAccountId.length === 0 ||
373
379
  !isOxyServiceEnvironment(environment)) {
374
380
  if (optional) {
375
381
  req.userId = null;
@@ -408,6 +414,11 @@ function OxyServicesUtilityMixin(Base) {
408
414
  return onError(error);
409
415
  return res.status(403).json(error);
410
416
  }
417
+ // ATTRIBUTION ONLY. `req.userId` answers "on whose behalf", never
418
+ // "who pays": the billing principal stays `req.serviceApp
419
+ // .ownerAccountId`, which this branch does not touch. Read it
420
+ // through `getOxyBillingPrincipal` (`@oxyhq/core/server`), whose
421
+ // return type a user id cannot satisfy (ADR 0007).
411
422
  req.userId = oxyUserId;
412
423
  req.user = { id: oxyUserId };
413
424
  req.serviceActingAs = { userId: oxyUserId, scopes: grant.scopes };
@@ -422,6 +433,7 @@ function OxyServicesUtilityMixin(Base) {
422
433
  appId,
423
434
  appName: decoded.appName || 'unknown',
424
435
  credentialId,
436
+ ownerAccountId,
425
437
  scopes: Array.isArray(decoded.scopes) ? decoded.scopes : [],
426
438
  environment,
427
439
  };
@@ -738,7 +750,7 @@ function OxyServicesUtilityMixin(Base) {
738
750
  * @example
739
751
  * ```typescript
740
752
  * // Protect internal endpoints
741
- * app.use('/internal', oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }));
753
+ * app.use('/internal', oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }));
742
754
  *
743
755
  * app.post('/internal/trigger', (req, res) => {
744
756
  * console.log('Service app:', req.serviceApp);
@@ -776,7 +788,7 @@ function OxyServicesUtilityMixin(Base) {
776
788
  * ```typescript
777
789
  * app.use(
778
790
  * '/internal/files',
779
- * oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }),
791
+ * oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }),
780
792
  * oxy.requireScope('files:write'),
781
793
  * );
782
794
  * ```
@@ -36,6 +36,7 @@ const OxyServices_chains_1 = require("./OxyServices.chains");
36
36
  const OxyServices_nodes_1 = require("./OxyServices.nodes");
37
37
  const OxyServices_links_1 = require("./OxyServices.links");
38
38
  const OxyServices_followGraph_1 = require("./OxyServices.followGraph");
39
+ const OxyServices_inference_1 = require("./OxyServices.inference");
39
40
  const OxyServices_deviceBoot_1 = require("./OxyServices.deviceBoot");
40
41
  const OxyServices_deviceTransfer_1 = require("./OxyServices.deviceTransfer");
41
42
  /**
@@ -100,6 +101,11 @@ const MIXIN_PIPELINE = [
100
101
  // The user-owned follow graph (#809). One relationship per user and target,
101
102
  // shared across applications, with per-application context on top.
102
103
  OxyServices_followGraph_1.OxyServicesFollowGraphMixin,
104
+ // The inference model catalogue (#972). Reads only, and deliberately no
105
+ // request/stream/receipt methods — the public inference edge those would
106
+ // call is workstream 4 and does not exist yet. See
107
+ // `docs/inference/README.md` for what is and is not built.
108
+ OxyServices_inference_1.OxyServicesInferenceMixin,
103
109
  // Device-first token mint: the client half of the zero-cookie transport
104
110
  // (`mintFromDeviceSecret` → `POST /session/device/token`).
105
111
  OxyServices_deviceBoot_1.OxyServicesDeviceBootMixin,
@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OXY_SERVICE_ENVIRONMENTS = void 0;
4
4
  exports.getOxyUserId = getOxyUserId;
5
5
  exports.isOxyAuthenticated = isOxyAuthenticated;
6
+ exports.getOxyBillingPrincipal = getOxyBillingPrincipal;
7
+ exports.getRequiredOxyBillingPrincipal = getRequiredOxyBillingPrincipal;
8
+ exports.getOxyDelegatedUserId = getOxyDelegatedUserId;
9
+ exports.getOxyRequestAttribution = getOxyRequestAttribution;
6
10
  exports.getRequiredOxyUserId = getRequiredOxyUserId;
7
11
  exports.requireOxyAuth = requireOxyAuth;
8
12
  exports.createOptionalOxyAuth = createOptionalOxyAuth;
@@ -36,6 +40,78 @@ function getOxyUserId(req) {
36
40
  function isOxyAuthenticated(req) {
37
41
  return getOxyUserId(req) !== null;
38
42
  }
43
+ /**
44
+ * The billing principal of a request, or `null` when the request carries no
45
+ * verified service principal (an ordinary user session is not a billable
46
+ * machine principal — its account is resolved from the account graph, not from
47
+ * a token claim).
48
+ *
49
+ * Reads `req.serviceApp` and NOTHING else: not `req.userId`, not `req.user`,
50
+ * not `req.serviceActingAs`. That exclusivity is the invariant this function
51
+ * exists to hold, and `serviceTokenAttribution.test.ts` mutation-tests it.
52
+ *
53
+ * **It answers for the SERVICE-TOKEN lane only.** The API's machine-credential
54
+ * lane (`oxy_sk_*`, issue #972 §2.3) resolves the same five facts into its own
55
+ * `req.machineCredential`, deliberately never `req.serviceApp` — populating the
56
+ * latter would hand a self-serve third-party credential the lane that only
57
+ * platform-trusted applications may enter. So a machine-credential request has
58
+ * no billing principal HERE and resolves `null`, which fails closed: the caller
59
+ * must handle it, and `getRequiredOxyBillingPrincipal` throws rather than
60
+ * charging anyone. One accessor answering for both lanes belongs to the public
61
+ * inference edge that has to admit both, and it needs the machine principal's
62
+ * shape to move into this package first.
63
+ */
64
+ function getOxyBillingPrincipal(req) {
65
+ const serviceApp = req.serviceApp;
66
+ if (!serviceApp) {
67
+ return null;
68
+ }
69
+ const accountId = normalizeId(serviceApp.ownerAccountId);
70
+ const applicationId = normalizeId(serviceApp.appId);
71
+ const credentialId = normalizeId(serviceApp.credentialId);
72
+ if (!accountId || !applicationId || !credentialId) {
73
+ return null;
74
+ }
75
+ return {
76
+ accountId,
77
+ applicationId,
78
+ credentialId,
79
+ environment: serviceApp.environment,
80
+ scopes: serviceApp.scopes,
81
+ };
82
+ }
83
+ /**
84
+ * {@link getOxyBillingPrincipal}, throwing when the request has none. Use on
85
+ * routes that have already required a service token.
86
+ */
87
+ function getRequiredOxyBillingPrincipal(req) {
88
+ const principal = getOxyBillingPrincipal(req);
89
+ if (!principal) {
90
+ throw new Error('Request has no verified Oxy service principal');
91
+ }
92
+ return principal;
93
+ }
94
+ /**
95
+ * The delegated end user of a service request, or `null`.
96
+ *
97
+ * Deliberately reads `req.serviceActingAs` — the grant-verified delegation —
98
+ * and not `req.userId`, which on a non-service request is the caller's own
99
+ * session identity and is not a delegation at all.
100
+ */
101
+ function getOxyDelegatedUserId(req) {
102
+ return normalizeId(req.serviceActingAs?.userId);
103
+ }
104
+ /**
105
+ * The whole attribution tuple for a service request: who pays, which
106
+ * application and credential, and optionally on whose behalf.
107
+ */
108
+ function getOxyRequestAttribution(req) {
109
+ const principal = getOxyBillingPrincipal(req);
110
+ if (!principal) {
111
+ return null;
112
+ }
113
+ return { ...principal, delegatedUserId: getOxyDelegatedUserId(req) };
114
+ }
39
115
  function getRequiredOxyUserId(req) {
40
116
  const userId = getOxyUserId(req);
41
117
  if (!userId) {
@@ -16,11 +16,15 @@
16
16
  * ```
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.isOfficialWebOrigin = exports.registrableApex = exports.OXY_IDENTITY_CACHE_PREFIXES = exports.oxyUserByIdCacheKey = exports.evictOxyIdentityCache = exports.publishOxyUserInvalidation = exports.createOxyUserInvalidationHandler = exports.verifySecret = exports.OXY_CSP_BASELINE = exports.formatOxyCspPolicy = exports.createOxySecurityHeaders = exports.buildOxyPagesHeaders = exports.buildOxyCspDirectives = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.OXY_SERVICE_ENVIRONMENTS = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getOxyUserId = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = void 0;
19
+ exports.isOfficialWebOrigin = exports.registrableApex = exports.OXY_IDENTITY_CACHE_PREFIXES = exports.oxyUserByIdCacheKey = exports.evictOxyIdentityCache = exports.publishOxyUserInvalidation = exports.createOxyUserInvalidationHandler = exports.verifySecret = exports.OXY_CSP_BASELINE = exports.formatOxyCspPolicy = exports.createOxySecurityHeaders = exports.buildOxyPagesHeaders = exports.buildOxyCspDirectives = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.OXY_SERVICE_ENVIRONMENTS = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getRequiredOxyBillingPrincipal = exports.getOxyUserId = exports.getOxyRequestAttribution = exports.getOxyDelegatedUserId = exports.getOxyBillingPrincipal = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = void 0;
20
20
  var auth_1 = require("./auth");
21
21
  Object.defineProperty(exports, "createOptionalOxyAuth", { enumerable: true, get: function () { return auth_1.createOptionalOxyAuth; } });
22
22
  Object.defineProperty(exports, "createOxyAuthMiddleware", { enumerable: true, get: function () { return auth_1.createOxyAuthMiddleware; } });
23
+ Object.defineProperty(exports, "getOxyBillingPrincipal", { enumerable: true, get: function () { return auth_1.getOxyBillingPrincipal; } });
24
+ Object.defineProperty(exports, "getOxyDelegatedUserId", { enumerable: true, get: function () { return auth_1.getOxyDelegatedUserId; } });
25
+ Object.defineProperty(exports, "getOxyRequestAttribution", { enumerable: true, get: function () { return auth_1.getOxyRequestAttribution; } });
23
26
  Object.defineProperty(exports, "getOxyUserId", { enumerable: true, get: function () { return auth_1.getOxyUserId; } });
27
+ Object.defineProperty(exports, "getRequiredOxyBillingPrincipal", { enumerable: true, get: function () { return auth_1.getRequiredOxyBillingPrincipal; } });
24
28
  Object.defineProperty(exports, "getRequiredOxyUserId", { enumerable: true, get: function () { return auth_1.getRequiredOxyUserId; } });
25
29
  Object.defineProperty(exports, "isOxyAuthenticated", { enumerable: true, get: function () { return auth_1.isOxyAuthenticated; } });
26
30
  Object.defineProperty(exports, "requireOxyAuth", { enumerable: true, get: function () { return auth_1.requireOxyAuth; } });