@oxyhq/core 20.0.0 → 20.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +10 -9
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/mixins/OxyServices.chains.js +73 -0
- package/dist/cjs/mixins/OxyServices.store.js +266 -0
- package/dist/cjs/mixins/OxyServices.utility.js +159 -104
- package/dist/cjs/mixins/index.js +7 -0
- package/dist/cjs/server/rateLimit.js +15 -6
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/mixins/OxyServices.chains.js +70 -0
- package/dist/esm/mixins/OxyServices.store.js +263 -0
- package/dist/esm/mixins/OxyServices.utility.js +159 -104
- package/dist/esm/mixins/index.js +7 -0
- package/dist/esm/server/rateLimit.js +15 -6
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +7 -0
- package/dist/types/mixins/OxyServices.chains.d.ts +156 -0
- package/dist/types/mixins/OxyServices.store.d.ts +334 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +31 -8
- package/dist/types/mixins/index.d.ts +3 -1
- package/package.json +1 -1
- package/src/index.ts +30 -0
- package/src/mixins/OxyServices.chains.ts +134 -0
- package/src/mixins/OxyServices.store.ts +585 -0
- package/src/mixins/OxyServices.utility.ts +161 -108
- package/src/mixins/__tests__/chains.test.ts +113 -0
- package/src/mixins/__tests__/store.test.ts +304 -0
- package/src/mixins/__tests__/userTokenAuth.test.ts +746 -0
- package/src/mixins/index.ts +9 -0
- package/src/server/__tests__/rateLimit.test.ts +47 -0
- package/src/server/rateLimit.ts +18 -8
package/src/mixins/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { OxyServicesReputationMixin } from './OxyServices.reputation';
|
|
|
17
17
|
import { OxyServicesAssetsMixin } from './OxyServices.assets';
|
|
18
18
|
import { OxyServicesAccountsMixin } from './OxyServices.accounts';
|
|
19
19
|
import { OxyServicesConnectedAppsMixin } from './OxyServices.connectedApps';
|
|
20
|
+
import { OxyServicesStoreMixin } from './OxyServices.store';
|
|
20
21
|
import { OxyServicesLocationMixin } from './OxyServices.location';
|
|
21
22
|
import { OxyServicesAnalyticsMixin } from './OxyServices.analytics';
|
|
22
23
|
import { OxyServicesDevicesMixin } from './OxyServices.devices';
|
|
@@ -28,6 +29,7 @@ import { OxyServicesContactsMixin } from './OxyServices.contacts';
|
|
|
28
29
|
import { OxyServicesNotificationsMixin } from './OxyServices.notifications';
|
|
29
30
|
import { OxyServicesAppDataMixin } from './OxyServices.appData';
|
|
30
31
|
import { OxyServicesCivicMixin } from './OxyServices.civic';
|
|
32
|
+
import { OxyServicesChainsMixin } from './OxyServices.chains';
|
|
31
33
|
import { OxyServicesNodesMixin } from './OxyServices.nodes';
|
|
32
34
|
import { OxyServicesLinksMixin } from './OxyServices.links';
|
|
33
35
|
import { OxyServicesFollowGraphMixin } from './OxyServices.followGraph';
|
|
@@ -55,6 +57,7 @@ type AllMixinInstances =
|
|
|
55
57
|
& InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>>
|
|
56
58
|
& InstanceType<ReturnType<typeof OxyServicesAccountsMixin<typeof OxyServicesBase>>>
|
|
57
59
|
& InstanceType<ReturnType<typeof OxyServicesConnectedAppsMixin<typeof OxyServicesBase>>>
|
|
60
|
+
& InstanceType<ReturnType<typeof OxyServicesStoreMixin<typeof OxyServicesBase>>>
|
|
58
61
|
& InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>>
|
|
59
62
|
& InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>>
|
|
60
63
|
& InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>>
|
|
@@ -65,6 +68,7 @@ type AllMixinInstances =
|
|
|
65
68
|
& InstanceType<ReturnType<typeof OxyServicesNotificationsMixin<typeof OxyServicesBase>>>
|
|
66
69
|
& InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>>
|
|
67
70
|
& InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>>
|
|
71
|
+
& InstanceType<ReturnType<typeof OxyServicesChainsMixin<typeof OxyServicesBase>>>
|
|
68
72
|
& InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>>
|
|
69
73
|
& InstanceType<ReturnType<typeof OxyServicesLinksMixin<typeof OxyServicesBase>>>
|
|
70
74
|
& InstanceType<ReturnType<typeof OxyServicesFollowGraphMixin<typeof OxyServicesBase>>>
|
|
@@ -123,6 +127,10 @@ const MIXIN_PIPELINE: MixinFunction[] = [
|
|
|
123
127
|
// OAuth-consent surface (public app identity + connected-app grants). Kept
|
|
124
128
|
// separate from account ownership.
|
|
125
129
|
OxyServicesConnectedAppsMixin,
|
|
130
|
+
// The app store: the public storefront, the reviews on it, and the listing a
|
|
131
|
+
// publisher edits. A module OVER the platform — turn it off and OAuth still
|
|
132
|
+
// works — so it is its own surface rather than more of `accounts`.
|
|
133
|
+
OxyServicesStoreMixin,
|
|
126
134
|
OxyServicesLocationMixin,
|
|
127
135
|
OxyServicesAnalyticsMixin,
|
|
128
136
|
OxyServicesDevicesMixin,
|
|
@@ -137,6 +145,7 @@ const MIXIN_PIPELINE: MixinFunction[] = [
|
|
|
137
145
|
OxyServicesAppDataMixin,
|
|
138
146
|
// Civic / Commons "Oxy ID" (public signed cards, Oxy ID QR payload)
|
|
139
147
|
OxyServicesCivicMixin,
|
|
148
|
+
OxyServicesChainsMixin,
|
|
140
149
|
// User nodes / decentralization (Fase 5): register/read/revoke/manage the
|
|
141
150
|
// caller's personal data node + ingest hint.
|
|
142
151
|
OxyServicesNodesMixin,
|
|
@@ -125,6 +125,53 @@ describe('@oxyhq/core/server rate limiter', () => {
|
|
|
125
125
|
expect(req.observedKey).toBe('user:validated-user');
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
it('does not clobber an identity a preceding middleware already resolved', () => {
|
|
129
|
+
// The limiter mutates the SHARED `req`, so the unconditional
|
|
130
|
+
// `req.userId = null` that `oxy.auth({ optional: true })` writes for every
|
|
131
|
+
// request it cannot authenticate is not merely a bucketing detail — it
|
|
132
|
+
// erases the identity for every handler downstream of the limiter too.
|
|
133
|
+
// The resolver must therefore skip entirely when a user is already present.
|
|
134
|
+
// This handler stands in for that erasure.
|
|
135
|
+
const clobberingAuth = jest.fn(
|
|
136
|
+
(req: RateLimitTestRequest, _res: Response, next: NextFunction) => {
|
|
137
|
+
req.userId = null;
|
|
138
|
+
req.user = null;
|
|
139
|
+
req.sessionId = null;
|
|
140
|
+
next();
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
const oxy = makeOxy(clobberingAuth as unknown as RequestHandler);
|
|
144
|
+
const req = makeRequest({
|
|
145
|
+
userId: 'resolved-by-the-app',
|
|
146
|
+
user: { id: 'resolved-by-the-app' },
|
|
147
|
+
sessionId: 'app-session',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
createOxyRateLimit(oxy)(req, {} as Response, jest.fn());
|
|
151
|
+
|
|
152
|
+
expect(req.userId).toBe('resolved-by-the-app');
|
|
153
|
+
expect(req.user).toEqual({ id: 'resolved-by-the-app' });
|
|
154
|
+
expect(req.sessionId).toBe('app-session');
|
|
155
|
+
expect(req.observedKey).toBe('user:resolved-by-the-app');
|
|
156
|
+
expect(clobberingAuth).not.toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('still resolves the session when no identity is present yet', () => {
|
|
160
|
+
const authHandler = jest.fn((req: RateLimitTestRequest, _res: Response, next: NextFunction) => {
|
|
161
|
+
req.userId = 'resolved-by-oxy';
|
|
162
|
+
req.user = { id: 'resolved-by-oxy' };
|
|
163
|
+
req.sessionId = 'oxy-session';
|
|
164
|
+
next();
|
|
165
|
+
});
|
|
166
|
+
const oxy = makeOxy(authHandler as unknown as RequestHandler);
|
|
167
|
+
const req = makeRequest();
|
|
168
|
+
|
|
169
|
+
createOxyRateLimit(oxy)(req, {} as Response, jest.fn());
|
|
170
|
+
|
|
171
|
+
expect(authHandler).toHaveBeenCalledTimes(1);
|
|
172
|
+
expect(req.observedKey).toBe('user:resolved-by-oxy');
|
|
173
|
+
});
|
|
174
|
+
|
|
128
175
|
it('continues through the anonymous limiter if optional auth returns an error', () => {
|
|
129
176
|
const oxy = makeOxy((_req: Request, _res: Response, next: NextFunction) => {
|
|
130
177
|
next(new Error('token rejected'));
|
package/src/server/rateLimit.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { isIPv4, isIPv6 } from 'node:net';
|
|
|
3
3
|
import type { Request, RequestHandler } from 'express';
|
|
4
4
|
import rateLimit, { type Store } from 'express-rate-limit';
|
|
5
5
|
import type { OxyServices } from '../OxyServices';
|
|
6
|
+
import { createOptionalOxyAuth } from './auth';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Server-only rate limiting for Oxy backends.
|
|
@@ -25,8 +26,9 @@ import type { OxyServices } from '../OxyServices';
|
|
|
25
26
|
* WHAT IT PROVIDES
|
|
26
27
|
* ----------------
|
|
27
28
|
* `createOxyRateLimit(oxy, options)` returns a SINGLE composed middleware that:
|
|
28
|
-
* 1. Resolves the user via `
|
|
29
|
-
*
|
|
29
|
+
* 1. Resolves the user via `createOptionalOxyAuth` (idempotent — it skips
|
|
30
|
+
* resolution entirely if a prior middleware already resolved a user, so
|
|
31
|
+
* the limiter can never erase an identity it did not create).
|
|
30
32
|
* 2. Applies an `express-rate-limit` limiter keyed PER USER when
|
|
31
33
|
* authenticated, falling back to the (IPv6-safe) IP otherwise, with
|
|
32
34
|
* generous, media-app-realistic defaults and sensible exemptions.
|
|
@@ -203,11 +205,12 @@ function hashAnonymousIp(ip: string): string {
|
|
|
203
205
|
/**
|
|
204
206
|
* Resolve the trusted authenticated rate-limit key.
|
|
205
207
|
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
208
|
+
* Only identities that came from a server-validated session or a verified
|
|
209
|
+
* service token/delegation may pick a bucket. `req.sessionId` is the marker
|
|
210
|
+
* for the former: `oxy.auth()` sets it only after `validateSession()` came
|
|
211
|
+
* back valid, so requiring it here means an identity written by some OTHER
|
|
212
|
+
* middleware — which this package cannot vouch for — shares the anonymous
|
|
213
|
+
* per-IP bucket rather than getting the authenticated quota.
|
|
211
214
|
*/
|
|
212
215
|
function resolveTrustedAuthenticatedKey(req: OxyAuthedRequest): string | null {
|
|
213
216
|
const userId = req.userId ?? req.user?.id ?? req.user?._id;
|
|
@@ -260,7 +263,14 @@ export function createOxyRateLimit(
|
|
|
260
263
|
|
|
261
264
|
// Idempotent optional-auth resolver. Reuses the SAME session resolution as
|
|
262
265
|
// every protected route, so the limiter keys by the real user identity.
|
|
263
|
-
|
|
266
|
+
//
|
|
267
|
+
// `createOptionalOxyAuth` — NOT the raw `oxy.auth({ optional: true })` —
|
|
268
|
+
// because only the former skips resolution when a preceding middleware has
|
|
269
|
+
// already resolved a user. The raw middleware writes `req.userId = null` on
|
|
270
|
+
// every request it cannot authenticate, and because it mutates the shared
|
|
271
|
+
// `req` that erasure is visible to every handler downstream of the limiter,
|
|
272
|
+
// not just to the bucket calculation.
|
|
273
|
+
const resolveSession = createOptionalOxyAuth(oxy, { auth });
|
|
264
274
|
|
|
265
275
|
const skip = (req: Request): boolean =>
|
|
266
276
|
isBuiltInExempt(req) || (exempt ? exempt(req) : false);
|