@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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scopeRoom = exports.MIN_SOCKET_TICKET_SECRET_LENGTH = exports.SOCKET_TICKET_MAX_TTL_SECONDS = void 0;
|
|
4
|
+
exports.signSocketTicket = signSocketTicket;
|
|
5
|
+
exports.verifySocketTicket = verifySocketTicket;
|
|
6
|
+
exports.canJoinScopeRoom = canJoinScopeRoom;
|
|
7
|
+
const index_js_1 = require("../access/index.js");
|
|
8
|
+
const crypto_js_1 = require("../common/crypto.js");
|
|
9
|
+
const id_js_1 = require("../common/id.js");
|
|
10
|
+
/**
|
|
11
|
+
* Short-lived socket handshake tickets. The browser cannot send its in-memory access token on a
|
|
12
|
+
* WebSocket upgrade header, so it asks the API for a ticket (≤ 60 s) and passes it in the Socket.IO
|
|
13
|
+
* `auth` payload. A ticket is `v1.<base64url claims>.<base64url HMAC-SHA256>`, signed over a domain
|
|
14
|
+
* separator so a ticket minted for another app or purpose never verifies here.
|
|
15
|
+
*
|
|
16
|
+
* Dependency-free (no socket.io), so the issuer can sign tickets without the socket stack.
|
|
17
|
+
*/
|
|
18
|
+
/** Longest allowed ticket lifetime (and the default), in seconds. */
|
|
19
|
+
exports.SOCKET_TICKET_MAX_TTL_SECONDS = 60;
|
|
20
|
+
/** Shortest accepted signing secret. */
|
|
21
|
+
exports.MIN_SOCKET_TICKET_SECRET_LENGTH = 32;
|
|
22
|
+
const VERSION = 'v1';
|
|
23
|
+
/** Tolerated clock skew for `iat` in the future, in seconds. */
|
|
24
|
+
const MAX_FUTURE_SKEW_SECONDS = 5;
|
|
25
|
+
function assertKey(options) {
|
|
26
|
+
if (!options.secret || options.secret.length < exports.MIN_SOCKET_TICKET_SECRET_LENGTH) {
|
|
27
|
+
throw new Error(`Socket ticket secret must be at least ${exports.MIN_SOCKET_TICKET_SECRET_LENGTH} characters`);
|
|
28
|
+
}
|
|
29
|
+
if (!options.domain)
|
|
30
|
+
throw new Error('Socket tickets need a domain separator');
|
|
31
|
+
}
|
|
32
|
+
const signature = (options, body) => Buffer.from((0, crypto_js_1.hmacSha256)(options.secret, `${options.domain}\n${VERSION}.${body}`), 'hex').toString('base64url');
|
|
33
|
+
const nowSeconds = (options) => Math.floor((options.now?.() ?? new Date()).getTime() / 1000);
|
|
34
|
+
/**
|
|
35
|
+
* Signs a ticket.
|
|
36
|
+
*
|
|
37
|
+
* @param input User, audience, scopes and permissions.
|
|
38
|
+
* @param options Secret, domain separator and `ttlSeconds` (1–60, default 60).
|
|
39
|
+
* @returns The ticket string.
|
|
40
|
+
* @example signSocketTicket({ userId, aud: 'admin-app', scopes: { tenant: [tenantId] }, permissions }, { secret, domain: 'myapp-socket-ticket:v1' })
|
|
41
|
+
*/
|
|
42
|
+
function signSocketTicket(input, options) {
|
|
43
|
+
assertKey(options);
|
|
44
|
+
const ttl = options.ttlSeconds ?? exports.SOCKET_TICKET_MAX_TTL_SECONDS;
|
|
45
|
+
if (!Number.isInteger(ttl) || ttl < 1 || ttl > exports.SOCKET_TICKET_MAX_TTL_SECONDS) {
|
|
46
|
+
throw new RangeError(`Socket ticket TTL must be 1–${exports.SOCKET_TICKET_MAX_TTL_SECONDS} seconds`);
|
|
47
|
+
}
|
|
48
|
+
if (!input.userId || !input.aud)
|
|
49
|
+
throw new RangeError('Socket tickets need a user id and an audience');
|
|
50
|
+
const iat = nowSeconds(options);
|
|
51
|
+
const claims = {
|
|
52
|
+
userId: input.userId,
|
|
53
|
+
aud: input.aud,
|
|
54
|
+
scopes: input.scopes ?? {},
|
|
55
|
+
permissions: input.permissions ?? [],
|
|
56
|
+
isSuperAdmin: input.isSuperAdmin === true,
|
|
57
|
+
jti: (0, id_js_1.newId)(),
|
|
58
|
+
iat,
|
|
59
|
+
exp: iat + ttl,
|
|
60
|
+
};
|
|
61
|
+
const body = Buffer.from(JSON.stringify(claims)).toString('base64url');
|
|
62
|
+
return `${VERSION}.${body}.${signature(options, body)}`;
|
|
63
|
+
}
|
|
64
|
+
function parseClaims(body) {
|
|
65
|
+
try {
|
|
66
|
+
const c = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
|
|
67
|
+
const valid = typeof c.userId === 'string' && typeof c.aud === 'string' && typeof c.jti === 'string'
|
|
68
|
+
&& Number.isInteger(c.iat) && Number.isInteger(c.exp) && Array.isArray(c.permissions)
|
|
69
|
+
&& !!c.scopes && typeof c.scopes === 'object' && typeof c.isSuperAdmin === 'boolean';
|
|
70
|
+
return valid ? c : null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Verifies a ticket's signature, domain, lifetime and audience. Never throws for a bad ticket.
|
|
78
|
+
*
|
|
79
|
+
* @param ticket The string from the handshake.
|
|
80
|
+
* @param options Secret, domain separator and the accepted audience(s).
|
|
81
|
+
* @returns The claims, or the rejection reason.
|
|
82
|
+
* @example const result = verifySocketTicket(socket.handshake.auth.ticket, { secret, domain, audience: 'admin-app' });
|
|
83
|
+
*/
|
|
84
|
+
function verifySocketTicket(ticket, options) {
|
|
85
|
+
assertKey(options);
|
|
86
|
+
if (typeof ticket !== 'string' || !ticket)
|
|
87
|
+
return { ok: false, reason: 'missing' };
|
|
88
|
+
const parts = ticket.split('.');
|
|
89
|
+
if (parts.length !== 3 || parts[0] !== VERSION || !parts[1] || !parts[2])
|
|
90
|
+
return { ok: false, reason: 'malformed' };
|
|
91
|
+
if (!(0, crypto_js_1.safeEqual)(parts[2], signature(options, parts[1])))
|
|
92
|
+
return { ok: false, reason: 'bad_signature' };
|
|
93
|
+
const claims = parseClaims(parts[1]);
|
|
94
|
+
if (!claims)
|
|
95
|
+
return { ok: false, reason: 'malformed' };
|
|
96
|
+
const now = nowSeconds(options);
|
|
97
|
+
if (claims.exp <= now || claims.exp - claims.iat > exports.SOCKET_TICKET_MAX_TTL_SECONDS || claims.iat > now + MAX_FUTURE_SKEW_SECONDS) {
|
|
98
|
+
return { ok: false, reason: 'expired' };
|
|
99
|
+
}
|
|
100
|
+
const audiences = typeof options.audience === 'string' ? [options.audience] : options.audience;
|
|
101
|
+
if (!audiences.includes(claims.aud))
|
|
102
|
+
return { ok: false, reason: 'wrong_audience' };
|
|
103
|
+
return { ok: true, claims };
|
|
104
|
+
}
|
|
105
|
+
/** Name of the room for one scope id, e.g. `tenant:01J…`. */
|
|
106
|
+
const scopeRoom = (scope, scopeId) => `${scope}:${scopeId}`;
|
|
107
|
+
exports.scopeRoom = scopeRoom;
|
|
108
|
+
/**
|
|
109
|
+
* True when the ticket holder may join the room of `scopeId` in `scope`: the id is in the ticket's scope
|
|
110
|
+
* ids (or the holder is a super admin) and, when given, the holder has `permission`.
|
|
111
|
+
*
|
|
112
|
+
* @param claims Verified ticket claims.
|
|
113
|
+
* @param scope Scope name, e.g. `tenant`.
|
|
114
|
+
* @param scopeId The id whose room is requested.
|
|
115
|
+
* @param permission Permission the room needs (optional).
|
|
116
|
+
* @example canJoinScopeRoom(claims, 'tenant', tenantId, 'widgets.view')
|
|
117
|
+
*/
|
|
118
|
+
function canJoinScopeRoom(claims, scope, scopeId, permission) {
|
|
119
|
+
const inScope = claims.isSuperAdmin || (claims.scopes[scope] ?? []).includes(scopeId);
|
|
120
|
+
if (!inScope || !scopeId)
|
|
121
|
+
return false;
|
|
122
|
+
return permission ? (0, index_js_1.hasPermission)(claims, permission) : true;
|
|
123
|
+
}
|
|
124
|
+
//# 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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"}
|
|
@@ -19,24 +19,26 @@ exports.RecordingLogger = RecordingLogger;
|
|
|
19
19
|
function makeAuthUser(overrides = {}) {
|
|
20
20
|
return { id: (0, id_js_1.newId)(), email: 'user@example.com', roles: [], permissions: [], scopes: {}, attrs: {}, ...overrides };
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Gateway-signed (v2) identity headers for route tests (use with supertest `.set()`). The method and path
|
|
24
|
+
* must match the request the test sends.
|
|
25
|
+
*
|
|
26
|
+
* @example request(app).get('/v1/items').set(identityHeaders({ user, secret, protocol, method: 'GET', path: '/v1/items' }));
|
|
27
|
+
*/
|
|
28
|
+
function identityHeaders(input) {
|
|
29
|
+
return (0, index_js_1.signedIdentityHeaders)(input);
|
|
25
30
|
}
|
|
26
|
-
/** Records everything published/sent so tests can assert on events, commands and
|
|
31
|
+
/** Records everything published/sent so tests can assert on events, commands, queue sends and delayed sends. */
|
|
27
32
|
class RecordingBus {
|
|
28
|
-
source;
|
|
29
33
|
events = [];
|
|
30
34
|
commands = [];
|
|
31
35
|
queued = [];
|
|
36
|
+
delayed = [];
|
|
32
37
|
failWith = null;
|
|
33
|
-
|
|
34
|
-
this.source = source;
|
|
35
|
-
}
|
|
36
|
-
async publish(type, data, meta) {
|
|
38
|
+
async publish(routingKey, data, meta) {
|
|
37
39
|
if (this.failWith)
|
|
38
40
|
throw this.failWith;
|
|
39
|
-
const envelope = (0, envelope_js_1.buildEnvelope)(
|
|
41
|
+
const envelope = (0, envelope_js_1.buildEnvelope)(routingKey, data, meta);
|
|
40
42
|
this.events.push(envelope);
|
|
41
43
|
return envelope;
|
|
42
44
|
}
|
|
@@ -48,7 +50,7 @@ class RecordingBus {
|
|
|
48
50
|
async send(command, data, meta) {
|
|
49
51
|
if (this.failWith)
|
|
50
52
|
throw this.failWith;
|
|
51
|
-
const envelope = (0, envelope_js_1.buildEnvelope)(command, data,
|
|
53
|
+
const envelope = (0, envelope_js_1.buildEnvelope)(command, data, meta);
|
|
52
54
|
this.commands.push(envelope);
|
|
53
55
|
return envelope;
|
|
54
56
|
}
|
|
@@ -62,11 +64,18 @@ class RecordingBus {
|
|
|
62
64
|
throw this.failWith;
|
|
63
65
|
this.queued.push({ queue, payload, options });
|
|
64
66
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
async scheduleDelayed(queue, payload, delayMs, options = {}) {
|
|
68
|
+
if (this.failWith)
|
|
69
|
+
throw this.failWith;
|
|
70
|
+
this.delayed.push({ queue, payload, delayMs, options });
|
|
71
|
+
}
|
|
72
|
+
/** Published events with this routing key. */
|
|
73
|
+
eventsOfType(routingKey) {
|
|
74
|
+
return this.events.filter((e) => e.routingKey === routingKey);
|
|
67
75
|
}
|
|
76
|
+
/** Sent commands with this name. */
|
|
68
77
|
commandsOfType(command) {
|
|
69
|
-
return this.commands.filter((e) => e.
|
|
78
|
+
return this.commands.filter((e) => e.routingKey === command);
|
|
70
79
|
}
|
|
71
80
|
}
|
|
72
81
|
exports.RecordingBus = RecordingBus;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":"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"}
|
package/dist/esm/access/index.js
CHANGED
|
@@ -12,25 +12,33 @@ function grants(granted, required) {
|
|
|
12
12
|
return true;
|
|
13
13
|
return granted.endsWith('.*') && required.startsWith(granted.slice(0, -1));
|
|
14
14
|
}
|
|
15
|
-
/** True when the user holds at least one of `required`. */
|
|
15
|
+
/** True when the user holds at least one of `required`. A super admin holds every permission. */
|
|
16
16
|
export function hasPermission(user, ...required) {
|
|
17
17
|
if (!user)
|
|
18
18
|
return false;
|
|
19
|
+
if (user.isSuperAdmin)
|
|
20
|
+
return true;
|
|
19
21
|
return required.some((perm) => user.permissions.some((g) => grants(g, perm)));
|
|
20
22
|
}
|
|
23
|
+
/** True when the user holds every one of `required`. A super admin holds every permission. */
|
|
21
24
|
export function hasAllPermissions(user, ...required) {
|
|
22
25
|
if (!user)
|
|
23
26
|
return false;
|
|
27
|
+
if (user.isSuperAdmin)
|
|
28
|
+
return true;
|
|
24
29
|
return required.every((perm) => user.permissions.some((g) => grants(g, perm)));
|
|
25
30
|
}
|
|
31
|
+
/** True when the user has at least one of `roles`. Prefer permission checks; roles are coarse. */
|
|
26
32
|
export function hasRole(user, ...roles) {
|
|
27
33
|
return !!user && roles.some((role) => user.roles.includes(role));
|
|
28
34
|
}
|
|
35
|
+
/** Returns the user, or throws UnauthorizedError (401) when there is none. */
|
|
29
36
|
export function requireUser(user) {
|
|
30
37
|
if (!user)
|
|
31
38
|
throw new UnauthorizedError();
|
|
32
39
|
return user;
|
|
33
40
|
}
|
|
41
|
+
/** Returns the user when they hold any of `required` (super admin always does); throws 401/403 otherwise. */
|
|
34
42
|
export function assertPermission(user, ...required) {
|
|
35
43
|
const u = requireUser(user);
|
|
36
44
|
if (!hasPermission(u, ...required))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/access/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGxE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEvC,SAAS,MAAM,CAAC,OAAe,EAAE,QAAgB;IAC/C,IAAI,OAAO,KAAK,mBAAmB,IAAI,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzE,OAAO,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7E,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/access/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { hasAllPermissions } from '../access/index.js';
|
|
2
|
+
import { ConflictError, ForbiddenError } from '../common/errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Two-person approval state machine. A requester asks for a sensitive operation; a different person who
|
|
5
|
+
* holds both the operation's permission and the app's "decide" permission approves or rejects it; the
|
|
6
|
+
* approved operation is then executed once (or fails). Pending requests expire after a TTL.
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* pending ──► approved ──► executed
|
|
10
|
+
* │ └──────► failed
|
|
11
|
+
* ├──► rejected
|
|
12
|
+
* ├──► cancelled
|
|
13
|
+
* └──► expired
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* App values (the decide permission and the TTL) are passed in through {@link createApprovalPolicy}.
|
|
17
|
+
*/
|
|
18
|
+
/** Every approval state. */
|
|
19
|
+
export const APPROVAL_STATES = ['pending', 'approved', 'rejected', 'cancelled', 'expired', 'executed', 'failed'];
|
|
20
|
+
const TRANSITIONS = Object.freeze({
|
|
21
|
+
pending: ['approved', 'rejected', 'cancelled', 'expired'],
|
|
22
|
+
approved: ['executed', 'failed'],
|
|
23
|
+
rejected: [],
|
|
24
|
+
cancelled: [],
|
|
25
|
+
expired: [],
|
|
26
|
+
executed: [],
|
|
27
|
+
failed: [],
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* True when `from → to` is an allowed move.
|
|
31
|
+
*
|
|
32
|
+
* @example canTransition('pending', 'approved') // true
|
|
33
|
+
*/
|
|
34
|
+
export function canTransition(from, to) {
|
|
35
|
+
return TRANSITIONS[from]?.includes(to) ?? false;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Throws unless `from → to` is allowed.
|
|
39
|
+
*
|
|
40
|
+
* @throws ConflictError (`APPROVAL_INVALID_TRANSITION`).
|
|
41
|
+
*/
|
|
42
|
+
export function assertTransition(from, to) {
|
|
43
|
+
if (!canTransition(from, to)) {
|
|
44
|
+
throw new ConflictError(`This request can no longer move from ${from} to ${to}`, { code: 'APPROVAL_INVALID_TRANSITION' });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** True when the state allows no further moves. */
|
|
48
|
+
export function isTerminalApprovalState(state) {
|
|
49
|
+
return TRANSITIONS[state].length === 0;
|
|
50
|
+
}
|
|
51
|
+
function assertPending(approval, now) {
|
|
52
|
+
if (approval.state !== 'pending') {
|
|
53
|
+
throw new ConflictError('This request has already been decided', { code: 'APPROVAL_NOT_PENDING' });
|
|
54
|
+
}
|
|
55
|
+
if (approval.expiresAt && new Date(approval.expiresAt).getTime() <= now.getTime()) {
|
|
56
|
+
throw new ConflictError('This request has expired', { code: 'APPROVAL_EXPIRED' });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Throws unless `approver` may approve or reject `approval`: it is pending and not expired, the approver
|
|
61
|
+
* is not the requester (even a super admin cannot approve their own request), and holds both the
|
|
62
|
+
* operation permission and `decidePermission`.
|
|
63
|
+
*
|
|
64
|
+
* @param approval The request.
|
|
65
|
+
* @param approver The deciding user.
|
|
66
|
+
* @param decidePermission The app's "may decide approvals" permission.
|
|
67
|
+
* @param now Clock (tests).
|
|
68
|
+
* @throws ConflictError (`APPROVAL_NOT_PENDING`, `APPROVAL_EXPIRED`) or ForbiddenError
|
|
69
|
+
* (`APPROVAL_SELF_DECISION`, `APPROVAL_FORBIDDEN`).
|
|
70
|
+
*/
|
|
71
|
+
export function assertCanDecide(approval, approver, decidePermission, now = new Date()) {
|
|
72
|
+
assertPending(approval, now);
|
|
73
|
+
if (approver.id === approval.requesterUserId) {
|
|
74
|
+
throw new ForbiddenError('You cannot decide your own request', { code: 'APPROVAL_SELF_DECISION' });
|
|
75
|
+
}
|
|
76
|
+
if (!hasAllPermissions(approver, approval.operationPermission, decidePermission)) {
|
|
77
|
+
throw new ForbiddenError('You do not have permission to decide this request', { code: 'APPROVAL_FORBIDDEN' });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Throws unless `user` may cancel `approval`: only the requester, and only while it is pending.
|
|
82
|
+
*
|
|
83
|
+
* @throws ConflictError (`APPROVAL_NOT_PENDING`) or ForbiddenError (`APPROVAL_NOT_REQUESTER`).
|
|
84
|
+
*/
|
|
85
|
+
export function assertCanCancel(approval, user) {
|
|
86
|
+
if (user.id !== approval.requesterUserId) {
|
|
87
|
+
throw new ForbiddenError('Only the person who made this request can cancel it', { code: 'APPROVAL_NOT_REQUESTER' });
|
|
88
|
+
}
|
|
89
|
+
if (approval.state !== 'pending') {
|
|
90
|
+
throw new ConflictError('This request has already been decided', { code: 'APPROVAL_NOT_PENDING' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* When a request made at `from` expires.
|
|
95
|
+
*
|
|
96
|
+
* @param ttlSeconds Positive lifetime in seconds.
|
|
97
|
+
* @param from Creation time (default now).
|
|
98
|
+
* @returns The expiry instant.
|
|
99
|
+
* @example approvalExpiresAt(3600, new Date('2026-01-01T00:00:00Z')) // 2026-01-01T01:00:00Z
|
|
100
|
+
*/
|
|
101
|
+
export function approvalExpiresAt(ttlSeconds, from = new Date()) {
|
|
102
|
+
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)
|
|
103
|
+
throw new RangeError('Approval TTL must be a positive number of seconds');
|
|
104
|
+
return new Date(from.getTime() + ttlSeconds * 1000);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Binds the approval rules to an app's decide permission and TTL.
|
|
108
|
+
*
|
|
109
|
+
* @param options Decide permission and TTL.
|
|
110
|
+
* @returns The bound policy.
|
|
111
|
+
* @example const approvals = createApprovalPolicy({ decidePermission: 'requests.decide', ttlSeconds: 86_400 });
|
|
112
|
+
*/
|
|
113
|
+
export function createApprovalPolicy(options) {
|
|
114
|
+
if (!options.decidePermission)
|
|
115
|
+
throw new RangeError('An approval policy needs a decide permission');
|
|
116
|
+
approvalExpiresAt(options.ttlSeconds); // validates the TTL up front
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
decidePermission: options.decidePermission,
|
|
119
|
+
ttlSeconds: options.ttlSeconds,
|
|
120
|
+
canTransition,
|
|
121
|
+
assertTransition,
|
|
122
|
+
assertCanDecide: (approval, approver, now) => assertCanDecide(approval, approver, options.decidePermission, now),
|
|
123
|
+
assertCanCancel,
|
|
124
|
+
expiresAt: (from) => approvalExpiresAt(options.ttlSeconds, from),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/approvals/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { maskPhone } from '../common/phone.js';
|
|
2
|
+
import { toUtcIso } from '../common/time.js';
|
|
3
|
+
import { createKeyClassifier, redactValue } from '../logging/redact.js';
|
|
4
|
+
/** User-agent strings are cut to this length. */
|
|
5
|
+
export const MAX_USER_AGENT_LENGTH = 255;
|
|
6
|
+
/** Stable JSON form for deep comparison (object keys sorted; Dates as ISO strings). */
|
|
7
|
+
function canonical(value) {
|
|
8
|
+
return JSON.stringify(value ?? null, (_key, v) => {
|
|
9
|
+
if (v && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date)) {
|
|
10
|
+
return Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b)));
|
|
11
|
+
}
|
|
12
|
+
return v;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function diff(before, after, classifier) {
|
|
16
|
+
const changes = {};
|
|
17
|
+
const redactedFields = [];
|
|
18
|
+
const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
|
|
19
|
+
for (const key of keys) {
|
|
20
|
+
const was = before[key] ?? null;
|
|
21
|
+
const now = after[key] ?? null;
|
|
22
|
+
if (canonical(was) === canonical(now))
|
|
23
|
+
continue;
|
|
24
|
+
if (classifier.isSecret(key)) {
|
|
25
|
+
redactedFields.push(key);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const clean = (value) => classifier.isPhone(key) && (typeof value === 'string' || typeof value === 'number') ? maskPhone(String(value)) : redactValue(value, classifier);
|
|
29
|
+
changes[key] = { before: clean(was), after: clean(now) };
|
|
30
|
+
}
|
|
31
|
+
return { changes, redactedFields };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Creates an audit-event builder with the app's extra redaction words.
|
|
35
|
+
*
|
|
36
|
+
* @param options Extra redaction words.
|
|
37
|
+
* @returns `buildAuditEvent` bound to that list.
|
|
38
|
+
* @example const buildAudit = createAuditEventBuilder({ redactKeys: ['nationalId'] });
|
|
39
|
+
*/
|
|
40
|
+
export function createAuditEventBuilder(options = {}) {
|
|
41
|
+
const classifier = createKeyClassifier(options.redactKeys ?? []);
|
|
42
|
+
return (input) => {
|
|
43
|
+
if (!input.action || !input.entityType)
|
|
44
|
+
throw new RangeError('Audit events need an action and an entity type');
|
|
45
|
+
const { changes, redactedFields } = diff(input.before ?? {}, input.after ?? {}, classifier);
|
|
46
|
+
return {
|
|
47
|
+
action: input.action,
|
|
48
|
+
entityType: input.entityType,
|
|
49
|
+
entityId: input.entityId,
|
|
50
|
+
scope: input.scope ? { key: input.scope.key, id: input.scope.id } : null,
|
|
51
|
+
actorUserId: input.actor?.id ?? null,
|
|
52
|
+
outcome: input.outcome ?? 'success',
|
|
53
|
+
occurredAt: toUtcIso(input.occurredAt ?? new Date()),
|
|
54
|
+
changes,
|
|
55
|
+
redactedFields,
|
|
56
|
+
ip: input.ip ?? null,
|
|
57
|
+
userAgent: input.userAgent ? input.userAgent.slice(0, MAX_USER_AGENT_LENGTH) : null,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Builds an audit event with the default redaction list: the diff holds only changed fields; secret
|
|
63
|
+
* fields are dropped (named in `redactedFields`); phone fields are masked to their last 4 digits.
|
|
64
|
+
*
|
|
65
|
+
* @param input Action, entity, scope, actor, before/after state and request facts.
|
|
66
|
+
* @returns The event.
|
|
67
|
+
* @example buildAuditEvent({ action: 'widget.updated', entityType: 'widget', entityId, scope: { key: 'tenant', id: tenantId }, actor: req.user ?? null, before, after, ip: req.ip, userAgent: req.get('user-agent') })
|
|
68
|
+
*/
|
|
69
|
+
export const buildAuditEvent = createAuditEventBuilder();
|
|
70
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/audit/index.ts"],"names":[],"mappings":"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"}
|
package/dist/esm/cache/index.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { Redis } from 'ioredis';
|
|
2
|
+
import { ConfigError } from '../config/index.js';
|
|
2
3
|
import { silentLogger } from '../logging/index.js';
|
|
4
|
+
export * from './keyspace.js';
|
|
5
|
+
/** Purpose segment for tag index sets: `{prefix}cache-tag:{tag}`. */
|
|
6
|
+
export const CACHE_TAG_PURPOSE = 'cache-tag';
|
|
7
|
+
/** Lower bound for a tag set's own TTL, so it outlives the keys it lists. */
|
|
8
|
+
const MIN_TAG_TTL_SEC = 86_400;
|
|
9
|
+
/**
|
|
10
|
+
* Throws unless `ttlSec` is a positive whole number of seconds.
|
|
11
|
+
*
|
|
12
|
+
* @param ttlSec Proposed TTL.
|
|
13
|
+
* @throws ConfigError for ttl ≤ 0, fractions, NaN or Infinity.
|
|
14
|
+
* @example assertTtl(60)
|
|
15
|
+
*/
|
|
16
|
+
export function assertTtl(ttlSec) {
|
|
17
|
+
if (!Number.isInteger(ttlSec) || ttlSec <= 0)
|
|
18
|
+
throw new ConfigError(`Redis TTL must be a positive whole number of seconds (got ${String(ttlSec)})`);
|
|
19
|
+
}
|
|
20
|
+
function assertOwned(keyspace, key) {
|
|
21
|
+
if (!keyspace.owns(key))
|
|
22
|
+
throw new ConfigError(`Cache key is outside the keyspace ${keyspace.prefix}`);
|
|
23
|
+
}
|
|
3
24
|
const NULL_SENTINEL = '__null__';
|
|
4
25
|
class MemoryStore {
|
|
5
26
|
max;
|
|
@@ -33,14 +54,23 @@ class MemoryStore {
|
|
|
33
54
|
* Redis-backed cache with tag invalidation. If Redis errors, reads fall through to the loader
|
|
34
55
|
* and writes go to a bounded in-memory store, so a cache outage degrades latency rather than
|
|
35
56
|
* availability.
|
|
57
|
+
*
|
|
58
|
+
* @param options Redis client (or null for memory only), the service keyspace, memory bound, logger.
|
|
59
|
+
* @returns The {@link Cache}.
|
|
60
|
+
* @example
|
|
61
|
+
* const cache = createCache({ redis, keyspace: ks, logger });
|
|
62
|
+
* const widget = await cache.wrap(ks.key('widget', id), () => repo.find(id), { ttlSec: 300 });
|
|
36
63
|
*/
|
|
37
64
|
export function createCache(options) {
|
|
38
65
|
const logger = options.logger ?? silentLogger;
|
|
39
|
-
const
|
|
66
|
+
const { keyspace } = options;
|
|
40
67
|
const memory = new MemoryStore(options.memoryMaxEntries ?? 5000);
|
|
41
68
|
const inflight = new Map();
|
|
42
|
-
const k = (key) =>
|
|
43
|
-
|
|
69
|
+
const k = (key) => {
|
|
70
|
+
assertOwned(keyspace, key);
|
|
71
|
+
return key;
|
|
72
|
+
};
|
|
73
|
+
const tagKey = (tag) => keyspace.key(CACHE_TAG_PURPOSE, tag);
|
|
44
74
|
let warned = false;
|
|
45
75
|
const redisFailed = (error) => {
|
|
46
76
|
if (!warned)
|
|
@@ -60,13 +90,14 @@ export function createCache(options) {
|
|
|
60
90
|
}
|
|
61
91
|
return memory.get(k(key));
|
|
62
92
|
};
|
|
63
|
-
const writeRaw = async (key, raw, opts
|
|
64
|
-
|
|
93
|
+
const writeRaw = async (key, raw, opts) => {
|
|
94
|
+
assertTtl(opts.ttlSec);
|
|
95
|
+
const ttl = opts.ttlSec;
|
|
65
96
|
if (options.redis) {
|
|
66
97
|
try {
|
|
67
98
|
const pipeline = options.redis.multi().set(k(key), raw, 'EX', ttl);
|
|
68
99
|
for (const tag of opts.tags ?? [])
|
|
69
|
-
pipeline.sadd(tagKey(tag), k(key)).expire(tagKey(tag), Math.max(ttl,
|
|
100
|
+
pipeline.sadd(tagKey(tag), k(key)).expire(tagKey(tag), Math.max(ttl, MIN_TAG_TTL_SEC));
|
|
70
101
|
await pipeline.exec();
|
|
71
102
|
return;
|
|
72
103
|
}
|
|
@@ -92,7 +123,8 @@ export function createCache(options) {
|
|
|
92
123
|
if (options.redis && keys.length)
|
|
93
124
|
await options.redis.del(...keys.map(k)).catch(redisFailed);
|
|
94
125
|
},
|
|
95
|
-
async wrap(key, loader, opts
|
|
126
|
+
async wrap(key, loader, opts) {
|
|
127
|
+
assertTtl(opts.ttlSec);
|
|
96
128
|
const raw = await readRaw(key);
|
|
97
129
|
if (raw !== undefined) {
|
|
98
130
|
if (raw === NULL_SENTINEL)
|
|
@@ -108,7 +140,7 @@ export function createCache(options) {
|
|
|
108
140
|
if (value !== null && value !== undefined)
|
|
109
141
|
await writeRaw(key, JSON.stringify(value), opts);
|
|
110
142
|
else if (opts.cacheNull)
|
|
111
|
-
await writeRaw(key, NULL_SENTINEL, { ...opts, ttlSec: Math.min(opts.ttlSec
|
|
143
|
+
await writeRaw(key, NULL_SENTINEL, { ...opts, ttlSec: Math.min(opts.ttlSec, 30) });
|
|
112
144
|
return value;
|
|
113
145
|
}
|
|
114
146
|
finally {
|
|
@@ -138,19 +170,30 @@ export function createCache(options) {
|
|
|
138
170
|
};
|
|
139
171
|
return cache;
|
|
140
172
|
}
|
|
141
|
-
/**
|
|
142
|
-
|
|
173
|
+
/**
|
|
174
|
+
* ioredis client that never crashes the process on connection errors and retries forever.
|
|
175
|
+
* It sets no `keyPrefix`: keys carry their full `{app}:{env}:{service}:` prefix from the keyspace.
|
|
176
|
+
*
|
|
177
|
+
* @param env Connection env (`REDIS_URL`, or host/port/password, and `REDIS_DB`).
|
|
178
|
+
* @param opts Logger and extra ioredis options.
|
|
179
|
+
* @returns The client.
|
|
180
|
+
* @example const redis = createRedisClient(config, { logger });
|
|
181
|
+
*/
|
|
182
|
+
export function createRedisClient(env, opts = {}) {
|
|
143
183
|
const logger = opts.logger ?? silentLogger;
|
|
144
184
|
const base = {
|
|
145
|
-
|
|
146
|
-
|
|
185
|
+
host: env.REDIS_HOST,
|
|
186
|
+
port: env.REDIS_PORT,
|
|
187
|
+
password: env.REDIS_PASSWORD || undefined,
|
|
188
|
+
db: env.REDIS_DB ?? 0,
|
|
147
189
|
lazyConnect: false,
|
|
148
190
|
enableOfflineQueue: false,
|
|
149
191
|
maxRetriesPerRequest: 1,
|
|
150
192
|
retryStrategy: (times) => Math.min(times * 500, 10_000),
|
|
151
193
|
...opts.options,
|
|
194
|
+
keyPrefix: undefined,
|
|
152
195
|
};
|
|
153
|
-
const client =
|
|
196
|
+
const client = env.REDIS_URL ? new Redis(env.REDIS_URL, base) : new Redis(base);
|
|
154
197
|
let lastError = '';
|
|
155
198
|
client.on('error', (error) => {
|
|
156
199
|
if (error.message !== lastError)
|
|
@@ -160,4 +203,18 @@ export function createRedisClient(opts) {
|
|
|
160
203
|
client.on('ready', () => { lastError = ''; logger.info('Redis connected', { db: base.db }); });
|
|
161
204
|
return client;
|
|
162
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* `SET key value EX ttlSec` for a keyspace key; refuses a TTL ≤ 0 so no key lives forever.
|
|
208
|
+
*
|
|
209
|
+
* @param redis Client (or anything with a compatible `set`).
|
|
210
|
+
* @param key Key from the service keyspace.
|
|
211
|
+
* @param value String value.
|
|
212
|
+
* @param ttlSec TTL in whole seconds (> 0).
|
|
213
|
+
* @throws ConfigError for an invalid TTL.
|
|
214
|
+
* @example await setWithTtl(redis, ks.key('session', id), payload, 900);
|
|
215
|
+
*/
|
|
216
|
+
export async function setWithTtl(redis, key, value, ttlSec) {
|
|
217
|
+
assertTtl(ttlSec);
|
|
218
|
+
await redis.set(key, value, 'EX', ttlSec);
|
|
219
|
+
}
|
|
163
220
|
//# sourceMappingURL=index.js.map
|