@heavybit/shared-lib 2.1.0 → 2.7.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/README.md +163 -2
- package/dist/cjs/access/index.js +9 -1
- package/dist/cjs/access/index.js.map +1 -1
- package/dist/cjs/approvals/index.js +137 -0
- package/dist/cjs/approvals/index.js.map +1 -0
- package/dist/cjs/audit/index.js +74 -0
- package/dist/cjs/audit/index.js.map +1 -0
- package/dist/cjs/cache/index.js +90 -16
- package/dist/cjs/cache/index.js.map +1 -1
- package/dist/cjs/cache/keyspace.js +101 -0
- package/dist/cjs/cache/keyspace.js.map +1 -0
- package/dist/cjs/common/circuit-breaker.js +74 -0
- package/dist/cjs/common/circuit-breaker.js.map +1 -0
- package/dist/cjs/common/index.js +4 -0
- package/dist/cjs/common/index.js.map +1 -1
- package/dist/cjs/common/internal-client.js +177 -0
- package/dist/cjs/common/internal-client.js.map +1 -0
- package/dist/cjs/common/internal-constants.js +12 -0
- package/dist/cjs/common/internal-constants.js.map +1 -0
- package/dist/cjs/common/internal-service-auth.js +160 -0
- package/dist/cjs/common/internal-service-auth.js.map +1 -0
- package/dist/cjs/common/money.js +215 -0
- package/dist/cjs/common/money.js.map +1 -0
- package/dist/cjs/common/phone.js +41 -22
- package/dist/cjs/common/phone.js.map +1 -1
- package/dist/cjs/common/time.js +28 -2
- package/dist/cjs/common/time.js.map +1 -1
- package/dist/cjs/config/index.js +139 -19
- package/dist/cjs/config/index.js.map +1 -1
- package/dist/cjs/contracts/notification-dispatch.js +1 -1
- package/dist/cjs/database/db.js +37 -0
- package/dist/cjs/database/db.js.map +1 -0
- package/dist/cjs/database/index.js +22 -51
- package/dist/cjs/database/index.js.map +1 -1
- package/dist/cjs/database/pagination.js +88 -0
- package/dist/cjs/database/pagination.js.map +1 -0
- package/dist/cjs/database/pool.js +60 -0
- package/dist/cjs/database/pool.js.map +1 -0
- package/dist/cjs/database/scoped-repository.js +132 -0
- package/dist/cjs/database/scoped-repository.js.map +1 -0
- package/dist/cjs/database/scopes.js +166 -0
- package/dist/cjs/database/scopes.js.map +1 -0
- package/dist/cjs/database/table-names.js +14 -0
- package/dist/cjs/database/table-names.js.map +1 -0
- package/dist/cjs/database/tables.js +71 -0
- package/dist/cjs/database/tables.js.map +1 -0
- package/dist/cjs/express/auth.js +64 -50
- package/dist/cjs/express/auth.js.map +1 -1
- package/dist/cjs/express/index.js +2 -0
- package/dist/cjs/express/index.js.map +1 -1
- package/dist/cjs/express/internal-caller.js +52 -0
- package/dist/cjs/express/internal-caller.js.map +1 -0
- package/dist/cjs/express/step-up.js +41 -0
- package/dist/cjs/express/step-up.js.map +1 -0
- package/dist/cjs/http/index.js +3 -71
- package/dist/cjs/http/index.js.map +1 -1
- package/dist/cjs/identity/index.js +269 -67
- package/dist/cjs/identity/index.js.map +1 -1
- package/dist/cjs/identity/step-up.js +52 -0
- package/dist/cjs/identity/step-up.js.map +1 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/knex/columns.js +36 -0
- package/dist/cjs/knex/columns.js.map +1 -0
- package/dist/cjs/knex/index.js +26 -43
- package/dist/cjs/knex/index.js.map +1 -1
- package/dist/cjs/knex/migration-helpers.js +57 -0
- package/dist/cjs/knex/migration-helpers.js.map +1 -0
- package/dist/cjs/logging/redact.js +6 -11
- package/dist/cjs/logging/redact.js.map +1 -1
- package/dist/cjs/messaging/base-consumer.js +36 -0
- package/dist/cjs/messaging/base-consumer.js.map +1 -0
- package/dist/cjs/messaging/consumer.js +142 -73
- package/dist/cjs/messaging/consumer.js.map +1 -1
- package/dist/cjs/messaging/envelope.js +53 -10
- package/dist/cjs/messaging/envelope.js.map +1 -1
- package/dist/cjs/messaging/idempotency.js +95 -8
- package/dist/cjs/messaging/idempotency.js.map +1 -1
- package/dist/cjs/messaging/index.js +2 -1
- package/dist/cjs/messaging/index.js.map +1 -1
- package/dist/cjs/messaging/outbox.js +97 -55
- package/dist/cjs/messaging/outbox.js.map +1 -1
- package/dist/cjs/messaging/publisher.js +86 -38
- package/dist/cjs/messaging/publisher.js.map +1 -1
- package/dist/cjs/messaging/staff-notifier.js +4 -4
- package/dist/cjs/messaging/staff-notifier.js.map +1 -1
- package/dist/cjs/messaging/topology.js +41 -5
- package/dist/cjs/messaging/topology.js.map +1 -1
- package/dist/cjs/rate-limit/index.js +57 -6
- package/dist/cjs/rate-limit/index.js.map +1 -1
- package/dist/cjs/rate-limit/keys.js +47 -0
- package/dist/cjs/rate-limit/keys.js.map +1 -0
- package/dist/cjs/socket/index.js +127 -16
- package/dist/cjs/socket/index.js.map +1 -1
- package/dist/cjs/socket/ticket.js +124 -0
- package/dist/cjs/socket/ticket.js.map +1 -0
- package/dist/cjs/testing/index.js +23 -14
- package/dist/cjs/testing/index.js.map +1 -1
- package/dist/esm/access/index.js +9 -1
- package/dist/esm/access/index.js.map +1 -1
- package/dist/esm/approvals/index.js +127 -0
- package/dist/esm/approvals/index.js.map +1 -0
- package/dist/esm/audit/index.js +70 -0
- package/dist/esm/audit/index.js.map +1 -0
- package/dist/esm/cache/index.js +70 -13
- package/dist/esm/cache/index.js.map +1 -1
- package/dist/esm/cache/keyspace.js +95 -0
- package/dist/esm/cache/keyspace.js.map +1 -0
- package/dist/esm/common/circuit-breaker.js +70 -0
- package/dist/esm/common/circuit-breaker.js.map +1 -0
- package/dist/esm/common/index.js +4 -0
- package/dist/esm/common/index.js.map +1 -1
- package/dist/esm/common/internal-client.js +173 -0
- package/dist/esm/common/internal-client.js.map +1 -0
- package/dist/esm/common/internal-constants.js +9 -0
- package/dist/esm/common/internal-constants.js.map +1 -0
- package/dist/esm/common/internal-service-auth.js +150 -0
- package/dist/esm/common/internal-service-auth.js.map +1 -0
- package/dist/esm/common/money.js +207 -0
- package/dist/esm/common/money.js.map +1 -0
- package/dist/esm/common/phone.js +41 -22
- package/dist/esm/common/phone.js.map +1 -1
- package/dist/esm/common/time.js +27 -2
- package/dist/esm/common/time.js.map +1 -1
- package/dist/esm/config/index.js +135 -18
- package/dist/esm/config/index.js.map +1 -1
- package/dist/esm/contracts/notification-dispatch.js +1 -1
- package/dist/esm/database/db.js +32 -0
- package/dist/esm/database/db.js.map +1 -0
- package/dist/esm/database/index.js +9 -45
- package/dist/esm/database/index.js.map +1 -1
- package/dist/esm/database/pagination.js +81 -0
- package/dist/esm/database/pagination.js.map +1 -0
- package/dist/esm/database/pool.js +51 -0
- package/dist/esm/database/pool.js.map +1 -0
- package/dist/esm/database/scoped-repository.js +128 -0
- package/dist/esm/database/scoped-repository.js.map +1 -0
- package/dist/esm/database/scopes.js +157 -0
- package/dist/esm/database/scopes.js.map +1 -0
- package/dist/esm/database/table-names.js +11 -0
- package/dist/esm/database/table-names.js.map +1 -0
- package/dist/esm/database/tables.js +61 -0
- package/dist/esm/database/tables.js.map +1 -0
- package/dist/esm/express/auth.js +62 -48
- package/dist/esm/express/auth.js.map +1 -1
- package/dist/esm/express/index.js +2 -0
- package/dist/esm/express/index.js.map +1 -1
- package/dist/esm/express/internal-caller.js +49 -0
- package/dist/esm/express/internal-caller.js.map +1 -0
- package/dist/esm/express/step-up.js +38 -0
- package/dist/esm/express/step-up.js.map +1 -0
- package/dist/esm/http/index.js +3 -70
- package/dist/esm/http/index.js.map +1 -1
- package/dist/esm/identity/index.js +246 -64
- package/dist/esm/identity/index.js.map +1 -1
- package/dist/esm/identity/step-up.js +47 -0
- package/dist/esm/identity/step-up.js.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/knex/columns.js +28 -0
- package/dist/esm/knex/columns.js.map +1 -0
- package/dist/esm/knex/index.js +8 -34
- package/dist/esm/knex/index.js.map +1 -1
- package/dist/esm/knex/migration-helpers.js +51 -0
- package/dist/esm/knex/migration-helpers.js.map +1 -0
- package/dist/esm/logging/redact.js +3 -7
- package/dist/esm/logging/redact.js.map +1 -1
- package/dist/esm/messaging/base-consumer.js +32 -0
- package/dist/esm/messaging/base-consumer.js.map +1 -0
- package/dist/esm/messaging/consumer.js +139 -71
- package/dist/esm/messaging/consumer.js.map +1 -1
- package/dist/esm/messaging/envelope.js +50 -10
- package/dist/esm/messaging/envelope.js.map +1 -1
- package/dist/esm/messaging/idempotency.js +91 -7
- package/dist/esm/messaging/idempotency.js.map +1 -1
- package/dist/esm/messaging/index.js +2 -1
- package/dist/esm/messaging/index.js.map +1 -1
- package/dist/esm/messaging/outbox.js +97 -54
- package/dist/esm/messaging/outbox.js.map +1 -1
- package/dist/esm/messaging/publisher.js +86 -38
- package/dist/esm/messaging/publisher.js.map +1 -1
- package/dist/esm/messaging/staff-notifier.js +4 -4
- package/dist/esm/messaging/staff-notifier.js.map +1 -1
- package/dist/esm/messaging/topology.js +39 -4
- package/dist/esm/messaging/topology.js.map +1 -1
- package/dist/esm/rate-limit/index.js +41 -6
- package/dist/esm/rate-limit/index.js.map +1 -1
- package/dist/esm/rate-limit/keys.js +43 -0
- package/dist/esm/rate-limit/keys.js.map +1 -0
- package/dist/esm/socket/index.js +112 -17
- package/dist/esm/socket/index.js.map +1 -1
- package/dist/esm/socket/ticket.js +117 -0
- package/dist/esm/socket/ticket.js.map +1 -0
- package/dist/esm/testing/index.js +24 -15
- package/dist/esm/testing/index.js.map +1 -1
- package/dist/types/access/index.d.ts +9 -3
- package/dist/types/access/index.d.ts.map +1 -1
- package/dist/types/approvals/index.d.ts +99 -0
- package/dist/types/approvals/index.d.ts.map +1 -0
- package/dist/types/audit/index.d.ts +82 -0
- package/dist/types/audit/index.d.ts.map +1 -0
- package/dist/types/cache/index.d.ts +64 -14
- package/dist/types/cache/index.d.ts.map +1 -1
- package/dist/types/cache/keyspace.d.ts +84 -0
- package/dist/types/cache/keyspace.d.ts.map +1 -0
- package/dist/types/common/circuit-breaker.d.ts +51 -0
- package/dist/types/common/circuit-breaker.d.ts.map +1 -0
- package/dist/types/common/index.d.ts +4 -0
- package/dist/types/common/index.d.ts.map +1 -1
- package/dist/types/common/internal-client.d.ts +116 -0
- package/dist/types/common/internal-client.d.ts.map +1 -0
- package/dist/types/common/internal-constants.d.ts +9 -0
- package/dist/types/common/internal-constants.d.ts.map +1 -0
- package/dist/types/common/internal-service-auth.d.ts +102 -0
- package/dist/types/common/internal-service-auth.d.ts.map +1 -0
- package/dist/types/common/money.d.ts +104 -0
- package/dist/types/common/money.d.ts.map +1 -0
- package/dist/types/common/phone.d.ts +25 -15
- package/dist/types/common/phone.d.ts.map +1 -1
- package/dist/types/common/time.d.ts +20 -2
- package/dist/types/common/time.d.ts.map +1 -1
- package/dist/types/config/index.d.ts +251 -27
- package/dist/types/config/index.d.ts.map +1 -1
- package/dist/types/contracts/notification-dispatch.d.ts +13 -13
- package/dist/types/database/db.d.ts +46 -0
- package/dist/types/database/db.d.ts.map +1 -0
- package/dist/types/database/index.d.ts +9 -22
- package/dist/types/database/index.d.ts.map +1 -1
- package/dist/types/database/pagination.d.ts +68 -0
- package/dist/types/database/pagination.d.ts.map +1 -0
- package/dist/types/database/pool.d.ts +39 -0
- package/dist/types/database/pool.d.ts.map +1 -0
- package/dist/types/database/scoped-repository.d.ts +92 -0
- package/dist/types/database/scoped-repository.d.ts.map +1 -0
- package/dist/types/database/scopes.d.ts +159 -0
- package/dist/types/database/scopes.d.ts.map +1 -0
- package/dist/types/database/table-names.d.ts +13 -0
- package/dist/types/database/table-names.d.ts.map +1 -0
- package/dist/types/database/tables.d.ts +748 -0
- package/dist/types/database/tables.d.ts.map +1 -0
- package/dist/types/express/auth.d.ts +40 -24
- package/dist/types/express/auth.d.ts.map +1 -1
- package/dist/types/express/index.d.ts +2 -0
- package/dist/types/express/index.d.ts.map +1 -1
- package/dist/types/express/internal-caller.d.ts +53 -0
- package/dist/types/express/internal-caller.d.ts.map +1 -0
- package/dist/types/express/step-up.d.ts +38 -0
- package/dist/types/express/step-up.d.ts.map +1 -0
- package/dist/types/http/index.d.ts +3 -31
- package/dist/types/http/index.d.ts.map +1 -1
- package/dist/types/identity/index.d.ts +159 -21
- package/dist/types/identity/index.d.ts.map +1 -1
- package/dist/types/identity/step-up.d.ts +41 -0
- package/dist/types/identity/step-up.d.ts.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/knex/columns.d.ts +15 -0
- package/dist/types/knex/columns.d.ts.map +1 -0
- package/dist/types/knex/index.d.ts +3 -11
- package/dist/types/knex/index.d.ts.map +1 -1
- package/dist/types/knex/migration-helpers.d.ts +28 -0
- package/dist/types/knex/migration-helpers.d.ts.map +1 -0
- package/dist/types/logging/redact.d.ts +2 -2
- package/dist/types/logging/redact.d.ts.map +1 -1
- package/dist/types/messaging/base-consumer.d.ts +33 -0
- package/dist/types/messaging/base-consumer.d.ts.map +1 -0
- package/dist/types/messaging/consumer.d.ts +59 -21
- package/dist/types/messaging/consumer.d.ts.map +1 -1
- package/dist/types/messaging/envelope.d.ts +50 -11
- package/dist/types/messaging/envelope.d.ts.map +1 -1
- package/dist/types/messaging/idempotency.d.ts +60 -3
- package/dist/types/messaging/idempotency.d.ts.map +1 -1
- package/dist/types/messaging/index.d.ts +7 -0
- package/dist/types/messaging/index.d.ts.map +1 -1
- package/dist/types/messaging/outbox.d.ts +62 -22
- package/dist/types/messaging/outbox.d.ts.map +1 -1
- package/dist/types/messaging/publisher.d.ts +54 -21
- package/dist/types/messaging/publisher.d.ts.map +1 -1
- package/dist/types/messaging/staff-notifier.d.ts +9 -9
- package/dist/types/messaging/staff-notifier.d.ts.map +1 -1
- package/dist/types/messaging/staff-recipients.d.ts +1 -1
- package/dist/types/messaging/topology.d.ts +40 -8
- package/dist/types/messaging/topology.d.ts.map +1 -1
- package/dist/types/rate-limit/index.d.ts +40 -4
- package/dist/types/rate-limit/index.d.ts.map +1 -1
- package/dist/types/rate-limit/keys.d.ts +52 -0
- package/dist/types/rate-limit/keys.d.ts.map +1 -0
- package/dist/types/socket/index.d.ts +86 -2
- package/dist/types/socket/index.d.ts.map +1 -1
- package/dist/types/socket/ticket.d.ts +90 -0
- package/dist/types/socket/ticket.d.ts.map +1 -0
- package/dist/types/testing/index.d.ts +31 -10
- package/dist/types/testing/index.d.ts.map +1 -1
- package/dist/types/types/index.d.ts +26 -14
- package/dist/types/types/index.d.ts.map +1 -1
- package/package.json +3 -1
|
@@ -2,23 +2,48 @@ import { RateLimiterMemory, RateLimiterRedis } from 'rate-limiter-flexible';
|
|
|
2
2
|
import { errorResponse } from '../common/response.js';
|
|
3
3
|
import { TooManyRequestsError } from '../common/errors.js';
|
|
4
4
|
import { silentLogger } from '../logging/index.js';
|
|
5
|
-
|
|
5
|
+
import { encodeKeyPart } from '../cache/keyspace.js';
|
|
6
|
+
import { RATE_LIMIT_PURPOSE } from './keys.js';
|
|
7
|
+
export * from './keys.js';
|
|
8
|
+
/**
|
|
9
|
+
* Redis limiter with an in-memory insurance limiter so a Redis outage never blocks traffic. Its
|
|
10
|
+
* keys come from the keyspace; pass already-encoded keys (see {@link limiterKey}) to `consume`.
|
|
11
|
+
*
|
|
12
|
+
* @param options Points, window, keyspace and scope.
|
|
13
|
+
* @returns The limiter.
|
|
14
|
+
* @example const limiter = createLimiter({ keyspace: ks, scope: 'sign-in-ip', points: 10, durationSec: 60, redis });
|
|
15
|
+
*/
|
|
6
16
|
export function createLimiter(options) {
|
|
7
|
-
const
|
|
17
|
+
const keyPrefix = options.keyspace.key(RATE_LIMIT_PURPOSE, options.scope);
|
|
18
|
+
const memory = new RateLimiterMemory({ keyPrefix, points: options.points, duration: options.durationSec, blockDuration: options.blockDurationSec ?? 0 });
|
|
8
19
|
if (!options.redis)
|
|
9
20
|
return memory;
|
|
10
21
|
return new RateLimiterRedis({
|
|
11
22
|
storeClient: options.redis,
|
|
12
|
-
keyPrefix
|
|
23
|
+
keyPrefix,
|
|
13
24
|
points: options.points,
|
|
14
25
|
duration: options.durationSec,
|
|
15
26
|
blockDuration: options.blockDurationSec ?? 0,
|
|
16
27
|
insuranceLimiter: memory,
|
|
17
28
|
});
|
|
18
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Encodes a raw limiter key (user id, IP …) so it cannot add segments to the limiter's Redis key.
|
|
32
|
+
*
|
|
33
|
+
* @param raw Raw identifier.
|
|
34
|
+
* @returns The encoded key part.
|
|
35
|
+
* @example limiter.consume(limiterKey(req.ip))
|
|
36
|
+
*/
|
|
37
|
+
export const limiterKey = (raw) => encodeKeyPart(raw);
|
|
19
38
|
/**
|
|
20
39
|
* Consumes one point for `key`; throws TooManyRequestsError (429) when exhausted.
|
|
21
|
-
* For in-service checks such as per-identifier
|
|
40
|
+
* For in-service checks such as per-identifier sign-in throttling.
|
|
41
|
+
*
|
|
42
|
+
* @param limiter Limiter from {@link createLimiter}.
|
|
43
|
+
* @param key Encoded key (see {@link limiterKey}).
|
|
44
|
+
* @param message Optional public message for the 429.
|
|
45
|
+
* @throws TooManyRequestsError when the limit is exhausted; rethrows limiter failures.
|
|
46
|
+
* @example await consumeOrThrow(limiter, limiterKey(userId));
|
|
22
47
|
*/
|
|
23
48
|
export async function consumeOrThrow(limiter, key, message) {
|
|
24
49
|
try {
|
|
@@ -30,13 +55,23 @@ export async function consumeOrThrow(limiter, key, message) {
|
|
|
30
55
|
throw rejection;
|
|
31
56
|
}
|
|
32
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Express middleware that consumes one point per request. Sets `X-RateLimit-Limit` and
|
|
60
|
+
* `X-RateLimit-Remaining`; when exhausted answers 429 `RATE_LIMITED` with `Retry-After` (seconds).
|
|
61
|
+
* Fails open: a limiter error (not a rejection) is logged and the request continues.
|
|
62
|
+
*
|
|
63
|
+
* @param options Limiter options and `keyOf` (default user id, else client IP).
|
|
64
|
+
* @returns The middleware.
|
|
65
|
+
* @example router.post('/sessions', rateLimitMiddleware({ keyspace: ks, scope: 'sign-in-ip', points: 10, durationSec: 60, redis }), handler);
|
|
66
|
+
*/
|
|
33
67
|
export function rateLimitMiddleware(options) {
|
|
34
68
|
const limiter = createLimiter(options);
|
|
35
69
|
const logger = options.logger ?? silentLogger;
|
|
36
70
|
const keyOf = options.keyOf ?? ((req) => req.user?.id ?? req.ip ?? 'unknown');
|
|
71
|
+
const scope = options.scope;
|
|
37
72
|
return async (req, res, next) => {
|
|
38
73
|
try {
|
|
39
|
-
const result = await limiter.consume(keyOf(req));
|
|
74
|
+
const result = await limiter.consume(limiterKey(keyOf(req)));
|
|
40
75
|
res.setHeader('X-RateLimit-Limit', options.points);
|
|
41
76
|
res.setHeader('X-RateLimit-Remaining', result.remainingPoints);
|
|
42
77
|
next();
|
|
@@ -44,7 +79,7 @@ export function rateLimitMiddleware(options) {
|
|
|
44
79
|
catch (rejection) {
|
|
45
80
|
const ms = rejection?.msBeforeNext;
|
|
46
81
|
if (typeof ms !== 'number') {
|
|
47
|
-
logger.error('Rate limiter failure; allowing request', {
|
|
82
|
+
logger.error('Rate limiter failure; allowing request', { scope, error: String(rejection) });
|
|
48
83
|
next();
|
|
49
84
|
return;
|
|
50
85
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rate-limit/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAA4B,MAAM,uBAAuB,CAAC;AACtG,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rate-limit/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ConfigError } from '../config/index.js';
|
|
2
|
+
import { sensitivePart } from '../cache/keyspace.js';
|
|
3
|
+
/**
|
|
4
|
+
* Rate-limit key builder and the sliding-window limiter contract. Dependency-free (exported from the
|
|
5
|
+
* package root as well as `/rate-limit`); the consuming service implements the sliding window.
|
|
6
|
+
*/
|
|
7
|
+
/** Purpose segment for every rate-limit key: `{prefix}rl:{scope}:…`. */
|
|
8
|
+
export const RATE_LIMIT_PURPOSE = 'rl';
|
|
9
|
+
const SCOPE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
10
|
+
const DIMENSIONS = ['ip', 'userId', 'email', 'host', 'route'];
|
|
11
|
+
const DIMENSION_SEGMENT = { ip: 'ip', userId: 'user', email: 'email', host: 'host', route: 'route' };
|
|
12
|
+
function dimensionValue(name, raw) {
|
|
13
|
+
const value = raw.trim();
|
|
14
|
+
if (name === 'email')
|
|
15
|
+
return sensitivePart(value.toLowerCase());
|
|
16
|
+
if (name === 'host')
|
|
17
|
+
return value.toLowerCase();
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Builds `{prefix}rl:{scope}[:ip:{ip}][:user:{id}][:email:{hash}][:host:{host}][:route:{route}]`.
|
|
22
|
+
*
|
|
23
|
+
* @param keyspace The service keyspace.
|
|
24
|
+
* @param input Scope plus at least one dimension.
|
|
25
|
+
* @returns The rate-limit key.
|
|
26
|
+
* @throws ConfigError when no dimension is given or the scope is invalid.
|
|
27
|
+
* @example rateLimitKey(ks, { scope: 'sign-in', ip: req.ip, email: body.email })
|
|
28
|
+
*/
|
|
29
|
+
export function rateLimitKey(keyspace, input) {
|
|
30
|
+
if (!SCOPE_PATTERN.test(input.scope))
|
|
31
|
+
throw new ConfigError(`Rate-limit scope "${input.scope}" must be lower-case kebab-case`);
|
|
32
|
+
const parts = [];
|
|
33
|
+
for (const name of DIMENSIONS) {
|
|
34
|
+
const raw = input[name];
|
|
35
|
+
if (raw === undefined || raw.trim() === '')
|
|
36
|
+
continue;
|
|
37
|
+
parts.push(DIMENSION_SEGMENT[name], dimensionValue(name, raw));
|
|
38
|
+
}
|
|
39
|
+
if (!parts.length)
|
|
40
|
+
throw new ConfigError(`Rate-limit key "${input.scope}" needs at least one of ${DIMENSIONS.join(', ')}`);
|
|
41
|
+
return keyspace.key(RATE_LIMIT_PURPOSE, input.scope, ...parts);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../../src/rate-limit/keys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAwD,MAAM,sBAAsB,CAAC;AAE3G;;;GAGG;AAEH,wEAAwE;AACxE,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAgBvC,MAAM,aAAa,GAAG,4BAA4B,CAAC;AACnD,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAU,CAAC;AAGvE,MAAM,iBAAiB,GAA8B,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAEhI,SAAS,cAAc,CAAC,IAAe,EAAE,GAAW;IAClD,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACzB,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,aAAa,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;IAChE,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;IAChD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,QAAuB,EAAE,KAAwB;IAC5E,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,WAAW,CAAC,qBAAqB,KAAK,CAAC,KAAK,iCAAiC,CAAC,CAAC;IAC/H,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,SAAS;QACrD,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,MAAM,IAAI,WAAW,CAAC,mBAAmB,KAAK,CAAC,KAAK,2BAA2B,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC3H,OAAO,QAAQ,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,KAAK,CAAC,CAAC;AACjE,CAAC"}
|
package/dist/esm/socket/index.js
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
|
+
import { createAdapter } from '@socket.io/redis-adapter';
|
|
1
2
|
import { Server } from 'socket.io';
|
|
2
|
-
import {
|
|
3
|
+
import { assertGatewaySecret, verifyGatewayIdentity } from '../identity/index.js';
|
|
3
4
|
import { silentLogger } from '../logging/index.js';
|
|
5
|
+
import { verifySocketTicket } from './ticket.js';
|
|
6
|
+
export * from './ticket.js';
|
|
7
|
+
/** Name of the private room every connection of `userId` joins. */
|
|
4
8
|
export const userRoom = (userId) => `user:${userId}`;
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
/** Builds the server: authenticate every handshake, then join only server-derived rooms. */
|
|
10
|
+
function buildServer(httpServer, core) {
|
|
11
|
+
const { logger } = core;
|
|
7
12
|
const io = new Server(httpServer, {
|
|
8
|
-
path:
|
|
9
|
-
cors:
|
|
10
|
-
...(
|
|
11
|
-
...
|
|
13
|
+
path: core.path,
|
|
14
|
+
cors: core.cors,
|
|
15
|
+
...(core.adapter ? { adapter: core.adapter } : {}),
|
|
16
|
+
...core.serverOptions,
|
|
12
17
|
});
|
|
13
18
|
io.use(async (socket, next) => {
|
|
14
19
|
try {
|
|
15
|
-
const
|
|
16
|
-
if (!
|
|
20
|
+
const identity = await core.authenticate(socket);
|
|
21
|
+
if (!identity)
|
|
17
22
|
return next(new Error('Authentication failed'));
|
|
18
|
-
socket.data.
|
|
23
|
+
socket.data.identity = identity;
|
|
19
24
|
next();
|
|
20
25
|
}
|
|
21
26
|
catch (error) {
|
|
@@ -24,11 +29,12 @@ export function createSocketServer(httpServer, options) {
|
|
|
24
29
|
}
|
|
25
30
|
});
|
|
26
31
|
io.on('connection', (socket) => {
|
|
27
|
-
const
|
|
28
|
-
const
|
|
32
|
+
const identity = socket.data.identity;
|
|
33
|
+
const userId = core.userIdOf(identity);
|
|
34
|
+
const rooms = core.rooms(identity);
|
|
29
35
|
void socket.join(rooms);
|
|
30
|
-
logger.debug('Socket connected', { userId
|
|
31
|
-
socket.on('disconnect', (reason) => logger.debug('Socket disconnected', { userId
|
|
36
|
+
logger.debug('Socket connected', { userId, rooms });
|
|
37
|
+
socket.on('disconnect', (reason) => logger.debug('Socket disconnected', { userId, reason }));
|
|
32
38
|
});
|
|
33
39
|
return {
|
|
34
40
|
io,
|
|
@@ -37,11 +43,100 @@ export function createSocketServer(httpServer, options) {
|
|
|
37
43
|
close: () => new Promise((resolve) => { io.close(() => resolve()); }),
|
|
38
44
|
};
|
|
39
45
|
}
|
|
40
|
-
/**
|
|
41
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Creates a Socket.IO server that authenticates every connection and joins it only to server-derived rooms.
|
|
48
|
+
* The verified user is in `socket.data.identity`.
|
|
49
|
+
*
|
|
50
|
+
* @example const sockets = createSocketServer(httpServer, { path: '/ws', authenticate: gatewaySocketAuth({ secret, protocol }) });
|
|
51
|
+
*/
|
|
52
|
+
export function createSocketServer(httpServer, options) {
|
|
53
|
+
return buildServer(httpServer, {
|
|
54
|
+
path: options.path,
|
|
55
|
+
cors: { origin: options.corsOrigins?.length ? options.corsOrigins : false, credentials: true },
|
|
56
|
+
adapter: options.adapter,
|
|
57
|
+
serverOptions: options.serverOptions,
|
|
58
|
+
logger: options.logger ?? silentLogger,
|
|
59
|
+
authenticate: options.authenticate,
|
|
60
|
+
userIdOf: (user) => user.id,
|
|
61
|
+
rooms: options.rooms ?? ((user) => [userRoom(user.id)]),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Trusts the gateway-signed (v2) identity headers forwarded on the WebSocket upgrade request. The gateway
|
|
66
|
+
* signs the upgrade as `GET <handshake path>` (query ignored).
|
|
67
|
+
*
|
|
68
|
+
* @param options The shared secret (at least 32 characters) and the app's identity protocol.
|
|
69
|
+
* @returns An `authenticate` function for {@link createSocketServer}.
|
|
70
|
+
* @example createSocketServer(server, { path: '/ws', authenticate: gatewaySocketAuth({ secret, protocol }) });
|
|
71
|
+
*/
|
|
72
|
+
export function gatewaySocketAuth(options) {
|
|
73
|
+
const secret = assertGatewaySecret(options.secret);
|
|
42
74
|
return (socket) => {
|
|
43
|
-
const result =
|
|
75
|
+
const result = verifyGatewayIdentity({
|
|
76
|
+
headers: socket.handshake.headers, method: 'GET', path: socket.handshake.url, secret, protocol: options.protocol,
|
|
77
|
+
});
|
|
44
78
|
return result.status === 'authenticated' ? result.user : null;
|
|
45
79
|
};
|
|
46
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* The handshake check on its own (testable without a server): verifies the ticket from
|
|
83
|
+
* `socket.handshake.auth.ticket`, then the browser origin against `allowedOrigins(claims)`, then
|
|
84
|
+
* single use.
|
|
85
|
+
*
|
|
86
|
+
* @param options Ticket keys, audience, origin rule, replay check and rejection hook.
|
|
87
|
+
* @returns `authenticate(socket)` resolving to the claims, or null when refused.
|
|
88
|
+
*/
|
|
89
|
+
export function createTicketHandshake(options) {
|
|
90
|
+
const reject = (reason, userId, origin) => {
|
|
91
|
+
options.onRejected?.({ reason, userId, path: options.path, origin });
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
return async (socket) => {
|
|
95
|
+
const origin = typeof socket.handshake.headers.origin === 'string' ? socket.handshake.headers.origin : null;
|
|
96
|
+
const auth = (socket.handshake.auth ?? {});
|
|
97
|
+
const result = verifySocketTicket(auth.ticket, options);
|
|
98
|
+
if (!result.ok)
|
|
99
|
+
return reject(result.reason, null, origin);
|
|
100
|
+
const { claims } = result;
|
|
101
|
+
if (origin ? !(await options.allowedOrigins(claims)).includes(origin) : !options.allowMissingOrigin) {
|
|
102
|
+
return reject('origin_not_allowed', claims.userId, origin);
|
|
103
|
+
}
|
|
104
|
+
if (options.isFirstUse && !(await options.isFirstUse(claims)))
|
|
105
|
+
return reject('replayed', claims.userId, origin);
|
|
106
|
+
return claims;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Creates a Socket.IO server whose clients authenticate with a short-lived ticket
|
|
111
|
+
* (`io(url, { path, auth: (cb) => getTicket().then((ticket) => cb({ ticket })) })`). Each connection
|
|
112
|
+
* auto-joins `user:{userId}`; the verified claims are in `socket.data.identity` for later room checks
|
|
113
|
+
* with `canJoinScopeRoom`.
|
|
114
|
+
*
|
|
115
|
+
* @param options HTTP server, path, ticket keys, origin rule, Redis + keyspace, rejection hook.
|
|
116
|
+
* @returns The emitter.
|
|
117
|
+
* @example createTicketSocketServer({ httpServer, path: '/ws', secret, domain, audience: 'admin-app', redis, keyspace, allowedOrigins: () => origins, onRejected })
|
|
118
|
+
*/
|
|
119
|
+
export function createTicketSocketServer(options) {
|
|
120
|
+
const { redis, keyspace } = options;
|
|
121
|
+
if (redis && !keyspace)
|
|
122
|
+
throw new Error('createTicketSocketServer: a keyspace is required with redis');
|
|
123
|
+
const isFirstUse = redis && keyspace
|
|
124
|
+
? async (claims) => {
|
|
125
|
+
const ttl = Math.max(1, claims.exp - Math.floor(Date.now() / 1000));
|
|
126
|
+
return (await redis.set(keyspace.key('socket-ticket', claims.jti), '1', 'EX', ttl, 'NX')) === 'OK';
|
|
127
|
+
}
|
|
128
|
+
: undefined;
|
|
129
|
+
const authenticate = createTicketHandshake({ ...options, isFirstUse });
|
|
130
|
+
return buildServer(options.httpServer, {
|
|
131
|
+
path: options.path,
|
|
132
|
+
// Browsers do not apply CORS to WebSocket upgrades; the per-ticket origin check above is the real gate.
|
|
133
|
+
cors: { origin: true, credentials: false },
|
|
134
|
+
adapter: redis && keyspace ? createAdapter(redis, redis.duplicate(), { key: keyspace.key('socket-io') }) : undefined,
|
|
135
|
+
serverOptions: options.serverOptions,
|
|
136
|
+
logger: options.logger ?? silentLogger,
|
|
137
|
+
authenticate,
|
|
138
|
+
userIdOf: (claims) => claims.userId,
|
|
139
|
+
rooms: (claims) => [userRoom(claims.userId), ...(options.rooms?.(claims) ?? [])],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
47
142
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/socket/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAmC,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/socket/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { hasPermission } from '../access/index.js';
|
|
2
|
+
import { hmacSha256, safeEqual } from '../common/crypto.js';
|
|
3
|
+
import { newId } from '../common/id.js';
|
|
4
|
+
/**
|
|
5
|
+
* Short-lived socket handshake tickets. The browser cannot send its in-memory access token on a
|
|
6
|
+
* WebSocket upgrade header, so it asks the API for a ticket (≤ 60 s) and passes it in the Socket.IO
|
|
7
|
+
* `auth` payload. A ticket is `v1.<base64url claims>.<base64url HMAC-SHA256>`, signed over a domain
|
|
8
|
+
* separator so a ticket minted for another app or purpose never verifies here.
|
|
9
|
+
*
|
|
10
|
+
* Dependency-free (no socket.io), so the issuer can sign tickets without the socket stack.
|
|
11
|
+
*/
|
|
12
|
+
/** Longest allowed ticket lifetime (and the default), in seconds. */
|
|
13
|
+
export const SOCKET_TICKET_MAX_TTL_SECONDS = 60;
|
|
14
|
+
/** Shortest accepted signing secret. */
|
|
15
|
+
export const MIN_SOCKET_TICKET_SECRET_LENGTH = 32;
|
|
16
|
+
const VERSION = 'v1';
|
|
17
|
+
/** Tolerated clock skew for `iat` in the future, in seconds. */
|
|
18
|
+
const MAX_FUTURE_SKEW_SECONDS = 5;
|
|
19
|
+
function assertKey(options) {
|
|
20
|
+
if (!options.secret || options.secret.length < MIN_SOCKET_TICKET_SECRET_LENGTH) {
|
|
21
|
+
throw new Error(`Socket ticket secret must be at least ${MIN_SOCKET_TICKET_SECRET_LENGTH} characters`);
|
|
22
|
+
}
|
|
23
|
+
if (!options.domain)
|
|
24
|
+
throw new Error('Socket tickets need a domain separator');
|
|
25
|
+
}
|
|
26
|
+
const signature = (options, body) => Buffer.from(hmacSha256(options.secret, `${options.domain}\n${VERSION}.${body}`), 'hex').toString('base64url');
|
|
27
|
+
const nowSeconds = (options) => Math.floor((options.now?.() ?? new Date()).getTime() / 1000);
|
|
28
|
+
/**
|
|
29
|
+
* Signs a ticket.
|
|
30
|
+
*
|
|
31
|
+
* @param input User, audience, scopes and permissions.
|
|
32
|
+
* @param options Secret, domain separator and `ttlSeconds` (1–60, default 60).
|
|
33
|
+
* @returns The ticket string.
|
|
34
|
+
* @example signSocketTicket({ userId, aud: 'admin-app', scopes: { tenant: [tenantId] }, permissions }, { secret, domain: 'myapp-socket-ticket:v1' })
|
|
35
|
+
*/
|
|
36
|
+
export function signSocketTicket(input, options) {
|
|
37
|
+
assertKey(options);
|
|
38
|
+
const ttl = options.ttlSeconds ?? SOCKET_TICKET_MAX_TTL_SECONDS;
|
|
39
|
+
if (!Number.isInteger(ttl) || ttl < 1 || ttl > SOCKET_TICKET_MAX_TTL_SECONDS) {
|
|
40
|
+
throw new RangeError(`Socket ticket TTL must be 1–${SOCKET_TICKET_MAX_TTL_SECONDS} seconds`);
|
|
41
|
+
}
|
|
42
|
+
if (!input.userId || !input.aud)
|
|
43
|
+
throw new RangeError('Socket tickets need a user id and an audience');
|
|
44
|
+
const iat = nowSeconds(options);
|
|
45
|
+
const claims = {
|
|
46
|
+
userId: input.userId,
|
|
47
|
+
aud: input.aud,
|
|
48
|
+
scopes: input.scopes ?? {},
|
|
49
|
+
permissions: input.permissions ?? [],
|
|
50
|
+
isSuperAdmin: input.isSuperAdmin === true,
|
|
51
|
+
jti: newId(),
|
|
52
|
+
iat,
|
|
53
|
+
exp: iat + ttl,
|
|
54
|
+
};
|
|
55
|
+
const body = Buffer.from(JSON.stringify(claims)).toString('base64url');
|
|
56
|
+
return `${VERSION}.${body}.${signature(options, body)}`;
|
|
57
|
+
}
|
|
58
|
+
function parseClaims(body) {
|
|
59
|
+
try {
|
|
60
|
+
const c = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
|
|
61
|
+
const valid = typeof c.userId === 'string' && typeof c.aud === 'string' && typeof c.jti === 'string'
|
|
62
|
+
&& Number.isInteger(c.iat) && Number.isInteger(c.exp) && Array.isArray(c.permissions)
|
|
63
|
+
&& !!c.scopes && typeof c.scopes === 'object' && typeof c.isSuperAdmin === 'boolean';
|
|
64
|
+
return valid ? c : null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Verifies a ticket's signature, domain, lifetime and audience. Never throws for a bad ticket.
|
|
72
|
+
*
|
|
73
|
+
* @param ticket The string from the handshake.
|
|
74
|
+
* @param options Secret, domain separator and the accepted audience(s).
|
|
75
|
+
* @returns The claims, or the rejection reason.
|
|
76
|
+
* @example const result = verifySocketTicket(socket.handshake.auth.ticket, { secret, domain, audience: 'admin-app' });
|
|
77
|
+
*/
|
|
78
|
+
export function verifySocketTicket(ticket, options) {
|
|
79
|
+
assertKey(options);
|
|
80
|
+
if (typeof ticket !== 'string' || !ticket)
|
|
81
|
+
return { ok: false, reason: 'missing' };
|
|
82
|
+
const parts = ticket.split('.');
|
|
83
|
+
if (parts.length !== 3 || parts[0] !== VERSION || !parts[1] || !parts[2])
|
|
84
|
+
return { ok: false, reason: 'malformed' };
|
|
85
|
+
if (!safeEqual(parts[2], signature(options, parts[1])))
|
|
86
|
+
return { ok: false, reason: 'bad_signature' };
|
|
87
|
+
const claims = parseClaims(parts[1]);
|
|
88
|
+
if (!claims)
|
|
89
|
+
return { ok: false, reason: 'malformed' };
|
|
90
|
+
const now = nowSeconds(options);
|
|
91
|
+
if (claims.exp <= now || claims.exp - claims.iat > SOCKET_TICKET_MAX_TTL_SECONDS || claims.iat > now + MAX_FUTURE_SKEW_SECONDS) {
|
|
92
|
+
return { ok: false, reason: 'expired' };
|
|
93
|
+
}
|
|
94
|
+
const audiences = typeof options.audience === 'string' ? [options.audience] : options.audience;
|
|
95
|
+
if (!audiences.includes(claims.aud))
|
|
96
|
+
return { ok: false, reason: 'wrong_audience' };
|
|
97
|
+
return { ok: true, claims };
|
|
98
|
+
}
|
|
99
|
+
/** Name of the room for one scope id, e.g. `tenant:01J…`. */
|
|
100
|
+
export const scopeRoom = (scope, scopeId) => `${scope}:${scopeId}`;
|
|
101
|
+
/**
|
|
102
|
+
* True when the ticket holder may join the room of `scopeId` in `scope`: the id is in the ticket's scope
|
|
103
|
+
* ids (or the holder is a super admin) and, when given, the holder has `permission`.
|
|
104
|
+
*
|
|
105
|
+
* @param claims Verified ticket claims.
|
|
106
|
+
* @param scope Scope name, e.g. `tenant`.
|
|
107
|
+
* @param scopeId The id whose room is requested.
|
|
108
|
+
* @param permission Permission the room needs (optional).
|
|
109
|
+
* @example canJoinScopeRoom(claims, 'tenant', tenantId, 'widgets.view')
|
|
110
|
+
*/
|
|
111
|
+
export function canJoinScopeRoom(claims, scope, scopeId, permission) {
|
|
112
|
+
const inScope = claims.isSuperAdmin || (claims.scopes[scope] ?? []).includes(scopeId);
|
|
113
|
+
if (!inScope || !scopeId)
|
|
114
|
+
return false;
|
|
115
|
+
return permission ? hasPermission(claims, permission) : true;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=ticket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticket.js","sourceRoot":"","sources":["../../../src/socket/ticket.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { newId } from '../common/id.js';
|
|
2
|
-
import {
|
|
2
|
+
import { signedIdentityHeaders } from '../identity/index.js';
|
|
3
3
|
import { buildEnvelope } from '../messaging/envelope.js';
|
|
4
4
|
/** A logger spy: records every line (level, message, meta) so tests can assert on log context. */
|
|
5
5
|
export class RecordingLogger {
|
|
@@ -13,24 +13,26 @@ export class RecordingLogger {
|
|
|
13
13
|
export function makeAuthUser(overrides = {}) {
|
|
14
14
|
return { id: newId(), email: 'user@example.com', roles: [], permissions: [], scopes: {}, attrs: {}, ...overrides };
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Gateway-signed (v2) identity headers for route tests (use with supertest `.set()`). The method and path
|
|
18
|
+
* must match the request the test sends.
|
|
19
|
+
*
|
|
20
|
+
* @example request(app).get('/v1/items').set(identityHeaders({ user, secret, protocol, method: 'GET', path: '/v1/items' }));
|
|
21
|
+
*/
|
|
22
|
+
export function identityHeaders(input) {
|
|
23
|
+
return signedIdentityHeaders(input);
|
|
19
24
|
}
|
|
20
|
-
/** Records everything published/sent so tests can assert on events, commands and
|
|
25
|
+
/** Records everything published/sent so tests can assert on events, commands, queue sends and delayed sends. */
|
|
21
26
|
export class RecordingBus {
|
|
22
|
-
source;
|
|
23
27
|
events = [];
|
|
24
28
|
commands = [];
|
|
25
29
|
queued = [];
|
|
30
|
+
delayed = [];
|
|
26
31
|
failWith = null;
|
|
27
|
-
|
|
28
|
-
this.source = source;
|
|
29
|
-
}
|
|
30
|
-
async publish(type, data, meta) {
|
|
32
|
+
async publish(routingKey, data, meta) {
|
|
31
33
|
if (this.failWith)
|
|
32
34
|
throw this.failWith;
|
|
33
|
-
const envelope = buildEnvelope(
|
|
35
|
+
const envelope = buildEnvelope(routingKey, data, meta);
|
|
34
36
|
this.events.push(envelope);
|
|
35
37
|
return envelope;
|
|
36
38
|
}
|
|
@@ -42,7 +44,7 @@ export class RecordingBus {
|
|
|
42
44
|
async send(command, data, meta) {
|
|
43
45
|
if (this.failWith)
|
|
44
46
|
throw this.failWith;
|
|
45
|
-
const envelope = buildEnvelope(command, data,
|
|
47
|
+
const envelope = buildEnvelope(command, data, meta);
|
|
46
48
|
this.commands.push(envelope);
|
|
47
49
|
return envelope;
|
|
48
50
|
}
|
|
@@ -56,11 +58,18 @@ export class RecordingBus {
|
|
|
56
58
|
throw this.failWith;
|
|
57
59
|
this.queued.push({ queue, payload, options });
|
|
58
60
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
async scheduleDelayed(queue, payload, delayMs, options = {}) {
|
|
62
|
+
if (this.failWith)
|
|
63
|
+
throw this.failWith;
|
|
64
|
+
this.delayed.push({ queue, payload, delayMs, options });
|
|
65
|
+
}
|
|
66
|
+
/** Published events with this routing key. */
|
|
67
|
+
eventsOfType(routingKey) {
|
|
68
|
+
return this.events.filter((e) => e.routingKey === routingKey);
|
|
61
69
|
}
|
|
70
|
+
/** Sent commands with this name. */
|
|
62
71
|
commandsOfType(command) {
|
|
63
|
-
return this.commands.filter((e) => e.
|
|
72
|
+
return this.commands.filter((e) => e.routingKey === command);
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACxC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,11 +7,16 @@ import type { AuthUser } from '../types/index.js';
|
|
|
7
7
|
* wildcards: `*` grants everything; `media.*` grants any permission starting with `media.`.
|
|
8
8
|
*/
|
|
9
9
|
export declare const WILDCARD_PERMISSION = "*";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export declare function
|
|
10
|
+
type PermissionHolder = Pick<AuthUser, 'permissions' | 'isSuperAdmin'>;
|
|
11
|
+
/** True when the user holds at least one of `required`. A super admin holds every permission. */
|
|
12
|
+
export declare function hasPermission(user: PermissionHolder | null | undefined, ...required: string[]): boolean;
|
|
13
|
+
/** True when the user holds every one of `required`. A super admin holds every permission. */
|
|
14
|
+
export declare function hasAllPermissions(user: PermissionHolder | null | undefined, ...required: string[]): boolean;
|
|
15
|
+
/** True when the user has at least one of `roles`. Prefer permission checks; roles are coarse. */
|
|
13
16
|
export declare function hasRole(user: Pick<AuthUser, 'roles'> | null | undefined, ...roles: string[]): boolean;
|
|
17
|
+
/** Returns the user, or throws UnauthorizedError (401) when there is none. */
|
|
14
18
|
export declare function requireUser(user: AuthUser | null | undefined): AuthUser;
|
|
19
|
+
/** Returns the user when they hold any of `required` (super admin always does); throws 401/403 otherwise. */
|
|
15
20
|
export declare function assertPermission(user: AuthUser | null | undefined, ...required: string[]): AuthUser;
|
|
16
21
|
/** IDs the user may act on within a scope (e.g. profiles they are a member of). */
|
|
17
22
|
export declare function scopeIds(user: Pick<AuthUser, 'scopes'> | null | undefined, scope: string): string[];
|
|
@@ -47,4 +52,5 @@ export type OwnerFilter = {
|
|
|
47
52
|
* otherwise the caller's own scope IDs (possibly empty → return nothing).
|
|
48
53
|
*/
|
|
49
54
|
export declare function ownerFilter(user: AuthUser | null | undefined, scope: string, bypassPermission?: string): OwnerFilter;
|
|
55
|
+
export {};
|
|
50
56
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/access/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAOvC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/access/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAOvC,KAAK,gBAAgB,GAAG,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,cAAc,CAAC,CAAC;AAEvE,iGAAiG;AACjG,wBAAgB,aAAa,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAIvG;AAED,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAI3G;AAED,kGAAkG;AAClG,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAErG;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,GAAG,QAAQ,CAGvE;AAED,6GAA6G;AAC7G,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,QAAQ,CAInG;AAED,mFAAmF;AACnF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAEnG;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,QAAQ,CAKlI;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,QAAQ,CAK5I;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CASxH;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAGzH;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,GAAG,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAExE;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,WAAW,CAIpH"}
|