@heavybit/shared-lib 1.3.2 → 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 +261 -0
- 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/errors.js +44 -5
- package/dist/cjs/common/errors.js.map +1 -1
- 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 +63 -11
- package/dist/cjs/common/phone.js.map +1 -1
- package/dist/cjs/common/response.js +78 -5
- package/dist/cjs/common/response.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 +140 -20
- package/dist/cjs/config/index.js.map +1 -1
- package/dist/cjs/context/index.js +49 -0
- package/dist/cjs/context/index.js.map +1 -0
- package/dist/cjs/contracts/index.js +0 -2
- package/dist/cjs/contracts/index.js.map +1 -1
- package/dist/cjs/contracts/notification-dispatch.js +87 -63
- package/dist/cjs/contracts/notification-dispatch.js.map +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 +88 -41
- package/dist/cjs/express/auth.js.map +1 -1
- package/dist/cjs/express/index.js +3 -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/middleware.js +154 -34
- package/dist/cjs/express/middleware.js.map +1 -1
- package/dist/cjs/express/respond.js +47 -0
- package/dist/cjs/express/respond.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 +7 -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/index.js +76 -40
- package/dist/cjs/logging/index.js.map +1 -1
- package/dist/cjs/logging/redact.js +112 -0
- package/dist/cjs/logging/redact.js.map +1 -0
- 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 +3 -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 +36 -44
- package/dist/cjs/messaging/staff-notifier.js.map +1 -1
- package/dist/cjs/messaging/staff-recipients.js +57 -0
- package/dist/cjs/messaging/staff-recipients.js.map +1 -0
- 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/cjs/validation/index.js +27 -3
- package/dist/cjs/validation/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/errors.js +42 -4
- package/dist/esm/common/errors.js.map +1 -1
- 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 +63 -11
- package/dist/esm/common/phone.js.map +1 -1
- package/dist/esm/common/response.js +75 -5
- package/dist/esm/common/response.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 +136 -19
- package/dist/esm/config/index.js.map +1 -1
- package/dist/esm/context/index.js +44 -0
- package/dist/esm/context/index.js.map +1 -0
- package/dist/esm/contracts/index.js +0 -2
- package/dist/esm/contracts/index.js.map +1 -1
- package/dist/esm/contracts/notification-dispatch.js +85 -62
- package/dist/esm/contracts/notification-dispatch.js.map +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 +87 -40
- package/dist/esm/express/auth.js.map +1 -1
- package/dist/esm/express/index.js +3 -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/middleware.js +146 -27
- package/dist/esm/express/middleware.js.map +1 -1
- package/dist/esm/express/respond.js +42 -0
- package/dist/esm/express/respond.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 +7 -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/index.js +68 -39
- package/dist/esm/logging/index.js.map +1 -1
- package/dist/esm/logging/redact.js +106 -0
- package/dist/esm/logging/redact.js.map +1 -0
- 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 +3 -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 +36 -43
- package/dist/esm/messaging/staff-notifier.js.map +1 -1
- package/dist/esm/messaging/staff-recipients.js +53 -0
- package/dist/esm/messaging/staff-recipients.js.map +1 -0
- 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/esm/validation/index.js +27 -2
- package/dist/esm/validation/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/alerting/index.d.ts +1 -1
- package/dist/types/alerting/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/errors.d.ts +48 -7
- package/dist/types/common/errors.d.ts.map +1 -1
- 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 +45 -6
- package/dist/types/common/phone.d.ts.map +1 -1
- package/dist/types/common/response.d.ts +66 -5
- package/dist/types/common/response.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 +252 -28
- package/dist/types/config/index.d.ts.map +1 -1
- package/dist/types/context/index.d.ts +39 -0
- package/dist/types/context/index.d.ts.map +1 -0
- package/dist/types/contracts/index.d.ts +0 -2
- package/dist/types/contracts/index.d.ts.map +1 -1
- package/dist/types/contracts/notification-dispatch.d.ts +204 -86
- package/dist/types/contracts/notification-dispatch.d.ts.map +1 -1
- 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 +63 -11
- package/dist/types/express/auth.d.ts.map +1 -1
- package/dist/types/express/index.d.ts +3 -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/middleware.d.ts +86 -7
- package/dist/types/express/middleware.d.ts.map +1 -1
- package/dist/types/express/respond.d.ts +41 -0
- package/dist/types/express/respond.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 +7 -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/index.d.ts +30 -4
- package/dist/types/logging/index.d.ts.map +1 -1
- package/dist/types/logging/redact.d.ts +54 -0
- package/dist/types/logging/redact.d.ts.map +1 -0
- 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 +8 -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 +69 -38
- package/dist/types/messaging/staff-notifier.d.ts.map +1 -1
- package/dist/types/messaging/staff-recipients.d.ts +50 -0
- package/dist/types/messaging/staff-recipients.d.ts.map +1 -0
- 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 +62 -15
- package/dist/types/types/index.d.ts.map +1 -1
- package/dist/types/validation/index.d.ts +33 -2
- package/dist/types/validation/index.d.ts.map +1 -1
- package/package.json +3 -1
- package/dist/cjs/contracts/editorial-draft.js +0 -120
- package/dist/cjs/contracts/editorial-draft.js.map +0 -1
- package/dist/cjs/contracts/upload-import.js +0 -54
- package/dist/cjs/contracts/upload-import.js.map +0 -1
- package/dist/esm/contracts/editorial-draft.js +0 -117
- package/dist/esm/contracts/editorial-draft.js.map +0 -1
- package/dist/esm/contracts/upload-import.js +0 -51
- package/dist/esm/contracts/upload-import.js.map +0 -1
- package/dist/types/contracts/editorial-draft.d.ts +0 -610
- package/dist/types/contracts/editorial-draft.d.ts.map +0 -1
- package/dist/types/contracts/upload-import.d.ts +0 -96
- package/dist/types/contracts/upload-import.d.ts.map +0 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staff alert recipients (messaging layer helper): who receives an alert raised for a permission.
|
|
3
|
+
* Today the lists come from configuration; an app can plug in a lookup (e.g. its auth service)
|
|
4
|
+
* by implementing {@link StaffRecipientResolver}.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { DISPATCH_LIMITS } from '../contracts/notification-dispatch.js';
|
|
8
|
+
/**
|
|
9
|
+
* Recipients from configuration (env), keyed by the permission an alert targets.
|
|
10
|
+
*
|
|
11
|
+
* @param byPermission Map of permission → recipients, typically built with {@link staffEmailListSchema}.
|
|
12
|
+
* @example new ConfiguredStaffRecipients({ 'payouts.manage': env.FINANCE_ALERT_EMAILS })
|
|
13
|
+
*/
|
|
14
|
+
export class ConfiguredStaffRecipients {
|
|
15
|
+
byPermission;
|
|
16
|
+
constructor(byPermission) {
|
|
17
|
+
this.byPermission = byPermission;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* @param permission The permission an alert targets.
|
|
21
|
+
* @returns A copy of the configured recipients, or `[]` when none are configured.
|
|
22
|
+
*/
|
|
23
|
+
async resolve(permission) {
|
|
24
|
+
return [...(this.byPermission[permission] ?? [])];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const emailSchema = z.string().email().max(DISPATCH_LIMITS.email);
|
|
28
|
+
/**
|
|
29
|
+
* A comma-separated staff address list from env (`"a@x.io, b@y.io"`): trimmed, blanks dropped,
|
|
30
|
+
* de-duplicated case-insensitively. An invalid address fails validation, so a typo is caught at boot
|
|
31
|
+
* instead of dead-lettering in the notification-service when the alert finally fires.
|
|
32
|
+
*
|
|
33
|
+
* @returns (on parse) `DispatchRecipient[]` with `email` only.
|
|
34
|
+
* @example const config = z.object({ FINANCE_ALERT_EMAILS: staffEmailListSchema }).parse(process.env);
|
|
35
|
+
*/
|
|
36
|
+
export const staffEmailListSchema = z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.transform((csv) => {
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
return (csv ?? '')
|
|
42
|
+
.split(',')
|
|
43
|
+
.map((entry) => entry.trim())
|
|
44
|
+
.filter((email) => email && !seen.has(email.toLowerCase()) && seen.add(email.toLowerCase()));
|
|
45
|
+
})
|
|
46
|
+
.superRefine((emails, ctx) => {
|
|
47
|
+
for (const email of emails) {
|
|
48
|
+
if (!emailSchema.safeParse(email).success)
|
|
49
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Not a valid email address: ${email}` });
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.transform((emails) => emails.map((email) => ({ email })));
|
|
53
|
+
//# sourceMappingURL=staff-recipients.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staff-recipients.js","sourceRoot":"","sources":["../../../src/messaging/staff-recipients.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,eAAe,EAA0B,MAAM,uCAAuC,CAAC;AAehG;;;;;GAKG;AACH,MAAM,OAAO,yBAAyB;IACP;IAA7B,YAA6B,YAAoE;QAApE,iBAAY,GAAZ,YAAY,CAAwD;IAAG,CAAC;IAErG;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,UAAkB;QAC9B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACpD,CAAC;CACF;AAED,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;AAElE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC;KAClC,MAAM,EAAE;KACR,QAAQ,EAAE;KACV,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE;IACjB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC;SACf,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AACjG,CAAC,CAAC;KACD,WAAW,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE;IAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO;YAAE,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,8BAA8B,KAAK,EAAE,EAAE,CAAC,CAAC;IAC3I,CAAC;AACH,CAAC,CAAC;KACD,SAAS,CAAC,CAAC,MAAM,EAAuB,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -1,16 +1,51 @@
|
|
|
1
|
+
const SEGMENT = /^[a-z0-9][a-z0-9_-]*$/;
|
|
2
|
+
const PURPOSE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
3
|
+
function segment(label, value, pattern = SEGMENT) {
|
|
4
|
+
const v = value.trim();
|
|
5
|
+
if (!pattern.test(v))
|
|
6
|
+
throw new Error(`Topology ${label} must match ${pattern} (got "${value}")`);
|
|
7
|
+
return v;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Builds the naming scheme for one app and environment.
|
|
11
|
+
*
|
|
12
|
+
* @param options App slug and environment prefix.
|
|
13
|
+
* @returns The {@link Topology}.
|
|
14
|
+
* @throws Error when a segment is empty or has other characters.
|
|
15
|
+
* @example const topology = createTopology({ app: config.APP_SLUG, prefix: config.QUEUE_PREFIX });
|
|
16
|
+
*/
|
|
1
17
|
export function createTopology(options) {
|
|
2
|
-
const app = options.app
|
|
3
|
-
const prefix = options.prefix
|
|
4
|
-
if (!app || !prefix)
|
|
5
|
-
throw new Error('Topology requires both app and prefix');
|
|
18
|
+
const app = segment('app', options.app);
|
|
19
|
+
const prefix = segment('prefix', options.prefix);
|
|
6
20
|
return {
|
|
7
21
|
app,
|
|
8
22
|
prefix,
|
|
9
23
|
eventsExchange: `${app}.${prefix}.events`,
|
|
10
24
|
commandsExchange: `${app}.${prefix}.commands`,
|
|
11
25
|
queue: (name) => (name.startsWith(`${prefix}.`) ? name : `${prefix}.${name}`),
|
|
26
|
+
queueName: (service, purpose) => `${prefix}.${segment('service', service)}.${segment('purpose', purpose, PURPOSE)}`,
|
|
27
|
+
deadLetterExchange: (service) => `${app}.${prefix}.${segment('service', service)}.dlx`,
|
|
12
28
|
};
|
|
13
29
|
}
|
|
30
|
+
/** Retry queue for one delay: a fixed TTL, dead-lettering back into `queue`. */
|
|
14
31
|
export const retryQueueName = (queue, delayMs) => `${queue}.retry.${delayMs}`;
|
|
32
|
+
/** Dead-letter queue of `queue`. */
|
|
15
33
|
export const deadLetterQueueName = (queue) => `${queue}.dlq`;
|
|
34
|
+
/** Smallest delay bucket for {@link delayQueueName}. */
|
|
35
|
+
export const MIN_DELAY_BUCKET_MS = 1000;
|
|
36
|
+
/**
|
|
37
|
+
* Delay queue for `scheduleDelayed`: one per power-of-two bucket (1s, 2s, 4s …) at or above `delayMs`.
|
|
38
|
+
* Per-message TTLs only expire at the head of a queue, so bucketing bounds how long a short delay can
|
|
39
|
+
* wait behind a longer one (at most its own bucket size).
|
|
40
|
+
*
|
|
41
|
+
* @param queue Target queue (physical name).
|
|
42
|
+
* @param delayMs Requested delay in milliseconds.
|
|
43
|
+
* @returns `{queue}.delay.{bucketMs}`.
|
|
44
|
+
*/
|
|
45
|
+
export function delayQueueName(queue, delayMs) {
|
|
46
|
+
let bucket = MIN_DELAY_BUCKET_MS;
|
|
47
|
+
while (bucket < delayMs)
|
|
48
|
+
bucket *= 2;
|
|
49
|
+
return `${queue}.delay.${bucket}`;
|
|
50
|
+
}
|
|
16
51
|
//# sourceMappingURL=topology.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topology.js","sourceRoot":"","sources":["../../../src/messaging/topology.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"topology.js","sourceRoot":"","sources":["../../../src/messaging/topology.ts"],"names":[],"mappings":"AA+BA,MAAM,OAAO,GAAG,uBAAuB,CAAC;AACxC,MAAM,OAAO,GAAG,wBAAwB,CAAC;AAEzC,SAAS,OAAO,CAAC,KAAa,EAAE,KAAa,EAAE,OAAO,GAAG,OAAO;IAC9D,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IACvB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,YAAY,KAAK,eAAe,OAAO,UAAU,KAAK,IAAI,CAAC,CAAC;IAClG,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,OAAwB;IACrD,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,OAAO;QACL,GAAG;QACH,MAAM;QACN,cAAc,EAAE,GAAG,GAAG,IAAI,MAAM,SAAS;QACzC,gBAAgB,EAAE,GAAG,GAAG,IAAI,MAAM,WAAW;QAC7C,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC;QACrF,SAAS,EAAE,CAAC,OAAe,EAAE,OAAe,EAAE,EAAE,CAAC,GAAG,MAAM,IAAI,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QACnI,kBAAkB,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,MAAM,IAAI,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM;KAC/F,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAa,EAAE,OAAe,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,OAAO,EAAE,CAAC;AAC9F,oCAAoC;AACpC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC;AAErE,wDAAwD;AACxD,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAExC;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa,EAAE,OAAe;IAC3D,IAAI,MAAM,GAAG,mBAAmB,CAAC;IACjC,OAAO,MAAM,GAAG,OAAO;QAAE,MAAM,IAAI,CAAC,CAAC;IACrC,OAAO,GAAG,KAAK,UAAU,MAAM,EAAE,CAAC;AACpC,CAAC"}
|
|
@@ -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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"}
|