@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
@@ -149,7 +149,7 @@ export function OxyServicesUtilityMixin(Base) {
149
149
  * additionally checked for `aud`, `iss`, and `type` claims to prevent
150
150
  * cross-token-type confusion attacks.
151
151
  * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
152
- * direct access to `SERVICE_TOKEN_SECRET` / `ACCESS_TOKEN_SECRET`.
152
+ * direct access to `ACCESS_TOKEN_SECRET`.
153
153
  *
154
154
  * **Why session-less user tokens are refused rather than trusted:**
155
155
  * every user access token the Oxy API issues carries a `sessionId` (see
@@ -181,7 +181,7 @@ export function OxyServicesUtilityMixin(Base) {
181
181
  * const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
182
182
  *
183
183
  * // Protect all routes under /protected
184
- * app.use('/protected', oxy.auth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }));
184
+ * app.use('/protected', oxy.auth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }));
185
185
  *
186
186
  * // Access user in route handler
187
187
  * app.get('/protected/me', (req, res) => {
@@ -195,7 +195,7 @@ export function OxyServicesUtilityMixin(Base) {
195
195
  * app.use('/public', oxy.auth({ optional: true }));
196
196
  *
197
197
  * // Require a specific scope on a service-token-protected route
198
- * app.use('/internal/files', oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }), oxy.requireScope('files:write'));
198
+ * app.use('/internal/files', oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }), oxy.requireScope('files:write'));
199
199
  * ```
200
200
  *
201
201
  * @param options Optional configuration
@@ -360,13 +360,19 @@ export function OxyServicesUtilityMixin(Base) {
360
360
  return onError(error);
361
361
  return res.status(401).json(error);
362
362
  }
363
- // Validate required service token fields
363
+ // Validate required service token fields. All of them are
364
+ // required, `ownerAccountId` included: an optional billing
365
+ // principal is one fallback away from being resolved from the
366
+ // delegated user, which is the exact confusion ADR 0007 forbids.
364
367
  const appId = decoded.appId;
365
368
  const credentialId = decoded.credentialId;
369
+ const ownerAccountId = decoded.ownerAccountId;
366
370
  const environment = decoded.environment;
367
371
  if (!appId ||
368
372
  typeof credentialId !== 'string' ||
369
373
  credentialId.length === 0 ||
374
+ typeof ownerAccountId !== 'string' ||
375
+ ownerAccountId.length === 0 ||
370
376
  !isOxyServiceEnvironment(environment)) {
371
377
  if (optional) {
372
378
  req.userId = null;
@@ -405,6 +411,11 @@ export function OxyServicesUtilityMixin(Base) {
405
411
  return onError(error);
406
412
  return res.status(403).json(error);
407
413
  }
414
+ // ATTRIBUTION ONLY. `req.userId` answers "on whose behalf", never
415
+ // "who pays": the billing principal stays `req.serviceApp
416
+ // .ownerAccountId`, which this branch does not touch. Read it
417
+ // through `getOxyBillingPrincipal` (`@oxyhq/core/server`), whose
418
+ // return type a user id cannot satisfy (ADR 0007).
408
419
  req.userId = oxyUserId;
409
420
  req.user = { id: oxyUserId };
410
421
  req.serviceActingAs = { userId: oxyUserId, scopes: grant.scopes };
@@ -419,6 +430,7 @@ export function OxyServicesUtilityMixin(Base) {
419
430
  appId,
420
431
  appName: decoded.appName || 'unknown',
421
432
  credentialId,
433
+ ownerAccountId,
422
434
  scopes: Array.isArray(decoded.scopes) ? decoded.scopes : [],
423
435
  environment,
424
436
  };
@@ -735,7 +747,7 @@ export function OxyServicesUtilityMixin(Base) {
735
747
  * @example
736
748
  * ```typescript
737
749
  * // Protect internal endpoints
738
- * app.use('/internal', oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }));
750
+ * app.use('/internal', oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }));
739
751
  *
740
752
  * app.post('/internal/trigger', (req, res) => {
741
753
  * console.log('Service app:', req.serviceApp);
@@ -773,7 +785,7 @@ export function OxyServicesUtilityMixin(Base) {
773
785
  * ```typescript
774
786
  * app.use(
775
787
  * '/internal/files',
776
- * oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }),
788
+ * oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }),
777
789
  * oxy.requireScope('files:write'),
778
790
  * );
779
791
  * ```
@@ -32,6 +32,7 @@ import { OxyServicesChainsMixin } from './OxyServices.chains.js';
32
32
  import { OxyServicesNodesMixin } from './OxyServices.nodes.js';
33
33
  import { OxyServicesLinksMixin } from './OxyServices.links.js';
34
34
  import { OxyServicesFollowGraphMixin } from './OxyServices.followGraph.js';
35
+ import { OxyServicesInferenceMixin } from './OxyServices.inference.js';
35
36
  import { OxyServicesDeviceBootMixin } from './OxyServices.deviceBoot.js';
36
37
  import { OxyServicesDeviceTransferMixin } from './OxyServices.deviceTransfer.js';
37
38
  /**
@@ -96,6 +97,11 @@ const MIXIN_PIPELINE = [
96
97
  // The user-owned follow graph (#809). One relationship per user and target,
97
98
  // shared across applications, with per-application context on top.
98
99
  OxyServicesFollowGraphMixin,
100
+ // The inference model catalogue (#972). Reads only, and deliberately no
101
+ // request/stream/receipt methods — the public inference edge those would
102
+ // call is workstream 4 and does not exist yet. See
103
+ // `docs/inference/README.md` for what is and is not built.
104
+ OxyServicesInferenceMixin,
99
105
  // Device-first token mint: the client half of the zero-cookie transport
100
106
  // (`mintFromDeviceSecret` → `POST /session/device/token`).
101
107
  OxyServicesDeviceBootMixin,
@@ -27,6 +27,78 @@ export function getOxyUserId(req) {
27
27
  export function isOxyAuthenticated(req) {
28
28
  return getOxyUserId(req) !== null;
29
29
  }
30
+ /**
31
+ * The billing principal of a request, or `null` when the request carries no
32
+ * verified service principal (an ordinary user session is not a billable
33
+ * machine principal — its account is resolved from the account graph, not from
34
+ * a token claim).
35
+ *
36
+ * Reads `req.serviceApp` and NOTHING else: not `req.userId`, not `req.user`,
37
+ * not `req.serviceActingAs`. That exclusivity is the invariant this function
38
+ * exists to hold, and `serviceTokenAttribution.test.ts` mutation-tests it.
39
+ *
40
+ * **It answers for the SERVICE-TOKEN lane only.** The API's machine-credential
41
+ * lane (`oxy_sk_*`, issue #972 §2.3) resolves the same five facts into its own
42
+ * `req.machineCredential`, deliberately never `req.serviceApp` — populating the
43
+ * latter would hand a self-serve third-party credential the lane that only
44
+ * platform-trusted applications may enter. So a machine-credential request has
45
+ * no billing principal HERE and resolves `null`, which fails closed: the caller
46
+ * must handle it, and `getRequiredOxyBillingPrincipal` throws rather than
47
+ * charging anyone. One accessor answering for both lanes belongs to the public
48
+ * inference edge that has to admit both, and it needs the machine principal's
49
+ * shape to move into this package first.
50
+ */
51
+ export function getOxyBillingPrincipal(req) {
52
+ const serviceApp = req.serviceApp;
53
+ if (!serviceApp) {
54
+ return null;
55
+ }
56
+ const accountId = normalizeId(serviceApp.ownerAccountId);
57
+ const applicationId = normalizeId(serviceApp.appId);
58
+ const credentialId = normalizeId(serviceApp.credentialId);
59
+ if (!accountId || !applicationId || !credentialId) {
60
+ return null;
61
+ }
62
+ return {
63
+ accountId,
64
+ applicationId,
65
+ credentialId,
66
+ environment: serviceApp.environment,
67
+ scopes: serviceApp.scopes,
68
+ };
69
+ }
70
+ /**
71
+ * {@link getOxyBillingPrincipal}, throwing when the request has none. Use on
72
+ * routes that have already required a service token.
73
+ */
74
+ export function getRequiredOxyBillingPrincipal(req) {
75
+ const principal = getOxyBillingPrincipal(req);
76
+ if (!principal) {
77
+ throw new Error('Request has no verified Oxy service principal');
78
+ }
79
+ return principal;
80
+ }
81
+ /**
82
+ * The delegated end user of a service request, or `null`.
83
+ *
84
+ * Deliberately reads `req.serviceActingAs` — the grant-verified delegation —
85
+ * and not `req.userId`, which on a non-service request is the caller's own
86
+ * session identity and is not a delegation at all.
87
+ */
88
+ export function getOxyDelegatedUserId(req) {
89
+ return normalizeId(req.serviceActingAs?.userId);
90
+ }
91
+ /**
92
+ * The whole attribution tuple for a service request: who pays, which
93
+ * application and credential, and optionally on whose behalf.
94
+ */
95
+ export function getOxyRequestAttribution(req) {
96
+ const principal = getOxyBillingPrincipal(req);
97
+ if (!principal) {
98
+ return null;
99
+ }
100
+ return { ...principal, delegatedUserId: getOxyDelegatedUserId(req) };
101
+ }
30
102
  export function getRequiredOxyUserId(req) {
31
103
  const userId = getOxyUserId(req);
32
104
  if (!userId) {
@@ -14,7 +14,7 @@
14
14
  * app.use(createOxyRateLimit(oxy, { store: redisStore }));
15
15
  * ```
16
16
  */
17
- export { createOptionalOxyAuth, createOxyAuthMiddleware, getOxyUserId, getRequiredOxyUserId, isOxyAuthenticated, requireOxyAuth, OXY_SERVICE_ENVIRONMENTS, } from './auth.js';
17
+ export { createOptionalOxyAuth, createOxyAuthMiddleware, getOxyBillingPrincipal, getOxyDelegatedUserId, getOxyRequestAttribution, getOxyUserId, getRequiredOxyBillingPrincipal, getRequiredOxyUserId, isOxyAuthenticated, requireOxyAuth, OXY_SERVICE_ENVIRONMENTS, } from './auth.js';
18
18
  export { createOxyRateLimit } from './rateLimit.js';
19
19
  // SSRF-safe upstream fetch + URL validation (Node-only).
20
20
  export { assertSafePublicUrl, isBlockedIp, safeFetch, SsrfRejection, UpstreamError, ALLOWED_PORTS, ALLOWED_PROTOCOLS, BLOCKED_HOSTNAMES, DEFAULT_USER_AGENT, MAX_REDIRECTS, MAX_URL_LENGTH, UPSTREAM_HEADERS_TIMEOUT_MS, } from './safeFetch.js';