@oxyhq/core 21.0.0 → 21.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +47 -8
- package/dist/cjs/i18n/locales/en-US.json +7 -2
- package/dist/cjs/i18n/locales/es-ES.json +7 -2
- package/dist/cjs/i18n/locales/locales/en-US.json +7 -2
- package/dist/cjs/i18n/locales/locales/es-ES.json +7 -2
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/inference/OxyInferenceClient.js +330 -0
- package/dist/cjs/mixins/OxyServices.accounts.js +5 -72
- package/dist/cjs/mixins/OxyServices.inference.js +59 -0
- package/dist/cjs/mixins/OxyServices.utility.js +18 -6
- package/dist/cjs/mixins/index.js +6 -0
- package/dist/cjs/server/auth.js +76 -0
- package/dist/cjs/server/cors.js +84 -15
- package/dist/cjs/server/index.js +5 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +47 -8
- package/dist/esm/i18n/locales/en-US.json +7 -2
- package/dist/esm/i18n/locales/es-ES.json +7 -2
- package/dist/esm/i18n/locales/locales/en-US.json +7 -2
- package/dist/esm/i18n/locales/locales/es-ES.json +7 -2
- package/dist/esm/index.js +4 -0
- package/dist/esm/inference/OxyInferenceClient.js +325 -0
- package/dist/esm/mixins/OxyServices.accounts.js +5 -72
- package/dist/esm/mixins/OxyServices.inference.js +56 -0
- package/dist/esm/mixins/OxyServices.utility.js +18 -6
- package/dist/esm/mixins/index.js +6 -0
- package/dist/esm/server/auth.js +72 -0
- package/dist/esm/server/cors.js +82 -15
- package/dist/esm/server/index.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +39 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/inference/OxyInferenceClient.d.ts +324 -0
- package/dist/types/mixins/OxyServices.accounts.d.ts +73 -95
- package/dist/types/mixins/OxyServices.inference.d.ts +95 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +44 -13
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/server/auth.d.ts +80 -0
- package/dist/types/server/cors.d.ts +41 -0
- package/dist/types/server/index.d.ts +2 -2
- package/package.json +2 -2
- package/src/HttpService.ts +50 -10
- package/src/__tests__/httpServiceUnwrapEnvelope.test.ts +115 -0
- package/src/i18n/locales/en-US.json +7 -2
- package/src/i18n/locales/es-ES.json +7 -2
- package/src/index.ts +19 -7
- package/src/inference/OxyInferenceClient.ts +590 -0
- package/src/inference/__tests__/OxyInferenceClient.test.ts +383 -0
- package/src/mixins/OxyServices.accounts.ts +75 -176
- package/src/mixins/OxyServices.inference.ts +57 -0
- package/src/mixins/OxyServices.utility.ts +58 -14
- package/src/mixins/__tests__/accounts.test.ts +57 -102
- package/src/mixins/__tests__/inferenceFactory.test.ts +58 -0
- package/src/mixins/__tests__/serviceAuth.test.ts +2 -0
- package/src/mixins/index.ts +8 -0
- package/src/server/__tests__/cors.socket.test.ts +225 -0
- package/src/server/__tests__/serviceTokenAttribution.test.ts +396 -0
- package/src/server/auth.ts +118 -0
- package/src/server/cors.ts +87 -12
- package/src/server/index.ts +6 -0
- 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 `
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
788
|
+
* oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }),
|
|
777
789
|
* oxy.requireScope('files:write'),
|
|
778
790
|
* );
|
|
779
791
|
* ```
|
package/dist/esm/mixins/index.js
CHANGED
|
@@ -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,
|
package/dist/esm/server/auth.js
CHANGED
|
@@ -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) {
|
package/dist/esm/server/cors.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* one-label subdomains such as `auth.oxy.so`, `api.oxy.so`,
|
|
16
16
|
* `accounts.oxy.so`, `console.oxy.so`, and `inbox.oxy.so`,
|
|
17
17
|
* - allows the caller's explicit `appOrigins`,
|
|
18
|
+
* - REFUSES the opaque origin on both sides (see `OPAQUE_ORIGIN`),
|
|
18
19
|
* - DENIES everything else (no reflection, never a wildcard with credentials),
|
|
19
20
|
* - echoes back the EXACT matched origin (so credentialed requests work) and
|
|
20
21
|
* sets `Vary: Origin` for correct caching,
|
|
@@ -22,7 +23,9 @@
|
|
|
22
23
|
*
|
|
23
24
|
* Node/Express-only: exported solely from `@oxyhq/core/server`.
|
|
24
25
|
*/
|
|
26
|
+
import { createLogger } from '../logger/index.js';
|
|
25
27
|
import { CENTRAL_IDP_APEX } from '../utils/authWebUrl.js';
|
|
28
|
+
const log = createLogger('OxyCors');
|
|
26
29
|
/** Default HTTP methods allowed across origins. */
|
|
27
30
|
const DEFAULT_ALLOWED_METHODS = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'];
|
|
28
31
|
/** Default request headers a browser may send on a credentialed cross-origin call. */
|
|
@@ -61,6 +64,29 @@ function isOxyFamilyOrigin(candidate) {
|
|
|
61
64
|
return false;
|
|
62
65
|
}
|
|
63
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The URL standard's serialization of an OPAQUE origin: the literal string
|
|
69
|
+
* `"null"`, which `new URL(x).origin` returns for every scheme that has no
|
|
70
|
+
* origin to speak of — `exp:`, `capacitor:`, `chrome-extension:`,
|
|
71
|
+
* `vscode-webview:`, and also `file:`, `data:` and `about:`.
|
|
72
|
+
*
|
|
73
|
+
* This value is why an allowlist may never store it. Every such scheme
|
|
74
|
+
* normalizes to the SAME `"null"`, so a set built by normalization cannot tell
|
|
75
|
+
* them apart: ONE opaque entry admits ALL of them. With credentials on and the
|
|
76
|
+
* raw header echoed back, a single `myapp://` in `appOrigins` turned this
|
|
77
|
+
* helper into "allow any custom-scheme browsing context" — measured live, an
|
|
78
|
+
* `exp://localhost:8150` entry answered `Origin: vscode-webview://…` with
|
|
79
|
+
* `access-control-allow-origin: vscode-webview://…` and
|
|
80
|
+
* `access-control-allow-credentials: true`.
|
|
81
|
+
*
|
|
82
|
+
* There is deliberately no escape hatch that matches such an origin by raw
|
|
83
|
+
* string instead. Admitting a custom-scheme browsing context to a CREDENTIALED
|
|
84
|
+
* allowlist is a distinct decision with its own threat model, and it must not
|
|
85
|
+
* arrive as a side effect of someone adding one line to `appOrigins`. Note
|
|
86
|
+
* also that a native client is not subject to CORS at all — React Native sends
|
|
87
|
+
* no `Origin` header — so a mobile app never needs an entry here.
|
|
88
|
+
*/
|
|
89
|
+
const OPAQUE_ORIGIN = 'null';
|
|
64
90
|
/** Normalize a raw origin string to its canonical `scheme://host[:port]` form. */
|
|
65
91
|
function normalizeOrigin(raw) {
|
|
66
92
|
try {
|
|
@@ -71,24 +97,65 @@ function normalizeOrigin(raw) {
|
|
|
71
97
|
}
|
|
72
98
|
}
|
|
73
99
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
100
|
+
* Normalize the configured `appOrigins` into the exact-match set — the
|
|
101
|
+
* CONFIGURE-SIDE half of the opaque-origin guard.
|
|
102
|
+
*
|
|
103
|
+
* An entry that is not a URL, or whose origin is opaque, is dropped and named
|
|
104
|
+
* in an error log. Dropped rather than thrown on because `appOrigins` is
|
|
105
|
+
* deployment configuration — at least one Oxy backend reads it from the
|
|
106
|
+
* environment — and a typo there must cost that one origin its CORS headers,
|
|
107
|
+
* never the whole service its boot. Both failure modes are equally SAFE (the
|
|
108
|
+
* entry is absent from the set either way), so the choice is purely about
|
|
109
|
+
* blast radius, and dropping keeps it to one origin whose requests then fail
|
|
110
|
+
* visibly in the browser.
|
|
111
|
+
*
|
|
112
|
+
* Exported for `__tests__/cors.socket.test.ts` and NOT re-exported from
|
|
113
|
+
* `server/index.ts`, so it is not part of the package's public surface. The
|
|
114
|
+
* two halves of the guard are separately exported because they are separately
|
|
115
|
+
* testable only that way: with this half in place the match-side half is
|
|
116
|
+
* unreachable through `createOxyCors`, so a test driving the public API alone
|
|
117
|
+
* would measure this function twice and the other one never.
|
|
76
118
|
*/
|
|
77
|
-
function
|
|
119
|
+
export function normalizeAppOrigins(appOrigins) {
|
|
78
120
|
const explicit = new Set();
|
|
79
121
|
for (const raw of appOrigins) {
|
|
80
122
|
const normalized = normalizeOrigin(raw);
|
|
81
|
-
if (normalized)
|
|
82
|
-
|
|
123
|
+
if (normalized === null) {
|
|
124
|
+
log.error('CORS allowlist entry ignored: it is not a URL', undefined, { entry: raw });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (normalized === OPAQUE_ORIGIN) {
|
|
128
|
+
log.error('CORS allowlist entry ignored: it has no origin to match against', undefined, {
|
|
129
|
+
entry: raw,
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
explicit.add(normalized);
|
|
83
134
|
}
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
135
|
+
return explicit;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Whether `origin` may be echoed back: it is in the built-in HTTPS Oxy apex
|
|
139
|
+
* family, or it exactly matches one of the configured app origins.
|
|
140
|
+
*
|
|
141
|
+
* The opaque-origin refusal here is the MATCH-SIDE half of the guard, and it
|
|
142
|
+
* is what makes the property hold regardless of how `explicit` was built — a
|
|
143
|
+
* set that somehow contains `"null"` still matches nothing, because no
|
|
144
|
+
* incoming origin ever normalizes past this line. `normalizeAppOrigins` is
|
|
145
|
+
* what stops such a set existing today; this is what stops it mattering.
|
|
146
|
+
*
|
|
147
|
+
* Exported for the same reason as `normalizeAppOrigins`, and likewise absent
|
|
148
|
+
* from `server/index.ts`.
|
|
149
|
+
*/
|
|
150
|
+
export function matchesAllowedOrigin(explicit, origin) {
|
|
151
|
+
const normalized = normalizeOrigin(origin);
|
|
152
|
+
if (normalized === null)
|
|
153
|
+
return false;
|
|
154
|
+
if (normalized === OPAQUE_ORIGIN)
|
|
155
|
+
return false;
|
|
156
|
+
if (explicit.has(normalized))
|
|
157
|
+
return true;
|
|
158
|
+
return isOxyFamilyOrigin(normalized);
|
|
92
159
|
}
|
|
93
160
|
/**
|
|
94
161
|
* Create a strict Oxy CORS middleware. See module docs.
|
|
@@ -100,7 +167,7 @@ function buildOriginAllowed(appOrigins) {
|
|
|
100
167
|
*/
|
|
101
168
|
export function createOxyCors(options = {}) {
|
|
102
169
|
const { appOrigins = [], allowCredentials = true, methods = DEFAULT_ALLOWED_METHODS, allowedHeaders = DEFAULT_ALLOWED_HEADERS, exposedHeaders = [], maxAgeSeconds = DEFAULT_MAX_AGE_SECONDS, } = options;
|
|
103
|
-
const
|
|
170
|
+
const explicitOrigins = normalizeAppOrigins(appOrigins);
|
|
104
171
|
const methodsHeader = methods.join(', ');
|
|
105
172
|
const allowedHeadersHeader = allowedHeaders.join(', ');
|
|
106
173
|
const exposedHeadersHeader = exposedHeaders.join(', ');
|
|
@@ -118,7 +185,7 @@ export function createOxyCors(options = {}) {
|
|
|
118
185
|
}
|
|
119
186
|
// Origin is present. Caching correctness: this response varies by Origin.
|
|
120
187
|
res.setHeader('Vary', 'Origin');
|
|
121
|
-
if (!
|
|
188
|
+
if (!matchesAllowedOrigin(explicitOrigins, origin)) {
|
|
122
189
|
// DENY: do NOT reflect the origin, do NOT emit a wildcard. The browser
|
|
123
190
|
// will block the cross-origin read. Preflights for denied origins get a
|
|
124
191
|
// 204 with no CORS headers (the actual request then fails CORS).
|
package/dist/esm/server/index.js
CHANGED
|
@@ -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';
|