@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
package/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# @heavybit/shared-lib
|
|
2
|
+
|
|
3
|
+
Building blocks for Express/TypeScript microservices that any app can use: response envelope and
|
|
4
|
+
errors, gateway identity, access scoping, validation, logging, config, RabbitMQ
|
|
5
|
+
(events/commands/RPC, outbox, idempotent consumers, DLQs), cache-aside, rate limiting, sockets,
|
|
6
|
+
signing, metrics and scheduling.
|
|
7
|
+
|
|
8
|
+
## Rule: shared concerns only
|
|
9
|
+
|
|
10
|
+
This package must drop into **any** service of **any** app unchanged.
|
|
11
|
+
|
|
12
|
+
- **In:** plumbing and contracts that every service needs in the same form, such as the error
|
|
13
|
+
envelope, signed identity headers, the messaging envelope and key builders.
|
|
14
|
+
- **Out:** anything one app or one service owns, such as domain events, payload schemas, permission
|
|
15
|
+
names, money or tax rules, and app names. Those live in the app's services or in a thin app-level
|
|
16
|
+
package built on top of this one.
|
|
17
|
+
- App-specific values come in as **parameters or config** (app slug, key prefixes, queue prefixes,
|
|
18
|
+
default phone region), never as hardcoded constants.
|
|
19
|
+
- A change that only one app needs is not made here.
|
|
20
|
+
|
|
21
|
+
## Install and build
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm ci
|
|
25
|
+
npm run build # ESM + CJS + .d.ts into dist/
|
|
26
|
+
npm test # jest --runInBand
|
|
27
|
+
npx tsc --noEmit
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Export map
|
|
31
|
+
|
|
32
|
+
| Import path | What it holds |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `@heavybit/shared-lib` | Root: `types`, `common`, `identity`, `access`, `validation`, `logging`, `config`, `contracts`, `alerting` (no optional peers) |
|
|
35
|
+
| `@heavybit/shared-lib/express` | `correlationMiddleware`, `correlationId`, `validate`, `asyncHandler`, `errorHandler`, `notFoundHandler`, `ok`, `created`, `okPage`, gateway auth guards, `createInternalCallerGuards`, health and security middleware |
|
|
36
|
+
| `@heavybit/shared-lib/http` | SSRF-guarded `safeFetch` (service-to-service calls use root `createInternalClient`) |
|
|
37
|
+
| `@heavybit/shared-lib/messaging` | RabbitMQ connection, publisher, consumer, RPC, outbox, idempotency, topology |
|
|
38
|
+
| `@heavybit/shared-lib/cache` · `/database` · `/knex` · `/rate-limit` · `/socket` · `/security` · `/metrics` · `/scheduling` · `/testing` | As named |
|
|
39
|
+
|
|
40
|
+
### Response envelope and errors (`common`, `express`)
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createLogger, httpError, NotFoundError, TooManyRequestsError } from '@heavybit/shared-lib';
|
|
44
|
+
import { correlationMiddleware, errorHandler, notFoundHandler, ok, created, okPage } from '@heavybit/shared-lib/express';
|
|
45
|
+
|
|
46
|
+
const logger = createLogger('orders');
|
|
47
|
+
app.use(correlationMiddleware('orders')); // 1. first: correlation id + request context
|
|
48
|
+
app.use(express.json()); // 2. body parser
|
|
49
|
+
// 3. …routers…
|
|
50
|
+
app.use(notFoundHandler()); // 4.
|
|
51
|
+
app.use(errorHandler(logger)); // 5. last
|
|
52
|
+
|
|
53
|
+
ok(res, data); // 200 { success: true, message, data }
|
|
54
|
+
created(res, { id }); // 201
|
|
55
|
+
okPage(res, { data, meta }); // 200 with meta { limit, nextCursor, hasMore }
|
|
56
|
+
throw httpError(409, 'SLUG_TAKEN', 'That address is already in use');
|
|
57
|
+
throw new TooManyRequestsError(undefined, { retryAfterSeconds: 30 }); // sets Retry-After
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Error body: `{ success: false, code, message, errors?, requestId }`.
|
|
61
|
+
|
|
62
|
+
- 4xx responses keep their message.
|
|
63
|
+
- Every 5xx response, and anything that is not an `AppError`, gets a generic message and is logged in full.
|
|
64
|
+
- A thrown `ZodError` becomes 422 `VALIDATION_FAILED` with `errors` keyed by field path.
|
|
65
|
+
|
|
66
|
+
| Status | Default code | Class |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| 400 | `BAD_REQUEST` | `BadRequestError` |
|
|
69
|
+
| 401 | `UNAUTHENTICATED` | `UnauthorizedError` |
|
|
70
|
+
| 403 | `FORBIDDEN` | `ForbiddenError` |
|
|
71
|
+
| 404 | `NOT_FOUND` | `NotFoundError` |
|
|
72
|
+
| 409 | `CONFLICT` | `ConflictError` |
|
|
73
|
+
| 422 | `VALIDATION_FAILED` | `ValidationError`, `NonRetryableError` |
|
|
74
|
+
| 429 | `RATE_LIMITED` | `TooManyRequestsError` |
|
|
75
|
+
| 503 | `SERVICE_UNAVAILABLE` | `ServiceUnavailableError` |
|
|
76
|
+
| 5xx | `INTERNAL_ERROR` | anything else |
|
|
77
|
+
|
|
78
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
79
|
+
|
|
80
|
+
### Request context, validation and logging (`context`, `express`, `logging`)
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { createLogger, getRequestContext, runWithContext, updateRequestContext, idParam, z } from '@heavybit/shared-lib';
|
|
84
|
+
import { validate } from '@heavybit/shared-lib/express';
|
|
85
|
+
|
|
86
|
+
router.patch('/:id', validate({ params: idParam, query: listQuery, body: updateBody }), controller.update);
|
|
87
|
+
updateRequestContext({ userId: user.id, scopes: { tenant: tenantId } }); // auth middleware
|
|
88
|
+
logger.info('Order placed', { orderId }); // line carries correlationId, userId, scopes automatically
|
|
89
|
+
await runWithContext({ correlationId: msg.correlationId, service: 'worker' }, () => handle(msg)); // consumers/jobs
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- `correlationMiddleware` keeps an incoming `x-correlation-id` only if it is 8–64 of `[A-Za-z0-9-]`, else makes a ULID.
|
|
93
|
+
- `validate` replaces `req.params/query/body` with parsed values (unknown keys stripped; `.strict()` rejects them) and
|
|
94
|
+
throws one 422 with every part's field errors (`params.id`, `query.limit`, body fields unprefixed).
|
|
95
|
+
- `createLogger(service, { redactKeys?, allowKeys?, filePath?, stream? })` writes JSON lines
|
|
96
|
+
`{ level, time, msg, service, correlationId, userId, scopes, ...meta }`. Keys containing the words password,
|
|
97
|
+
pass, token, secret, code, otp, authorization, cookie, credential, pin, apikey, jwt or signature are
|
|
98
|
+
`[REDACTED]` at any depth (except `errorCode`, `statusCode`, `countryCode`, `currencyCode`, `postalCode`,
|
|
99
|
+
`zipCode`); phone numbers are masked to their last 4 digits.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
### Gateway identity v2 and route guards (`identity`, `express`)
|
|
103
|
+
|
|
104
|
+
The gateway signs each request's identity; services verify it before trusting anything.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { defineIdentityProtocol, defineStepUpScopes } from '@heavybit/shared-lib';
|
|
108
|
+
import { authenticateFromGateway, createStepUpGuards, requirePermission, requireOtpVerified } from '@heavybit/shared-lib/express';
|
|
109
|
+
|
|
110
|
+
// App binding (lives in the app's contracts package, not here):
|
|
111
|
+
const protocol = defineIdentityProtocol({
|
|
112
|
+
domain: 'myapp-gateway-identity:v2', // domain separator: two apps never verify each other
|
|
113
|
+
scopeHeaders: { tenant: 'x-tenant-id' }, // → req.user.scopes.tenant
|
|
114
|
+
attrHeaders: { tenantSlug: 'x-tenant-slug' }, // → req.user.attrs.tenantSlug
|
|
115
|
+
});
|
|
116
|
+
const stepUpScopes = defineStepUpScopes({ 'record-delete': {}, 'secret-rotate': { singleUse: true } });
|
|
117
|
+
const { requireStepUp, declaredStepUpRoutes } = createStepUpGuards({ catalogue: stepUpScopes }); // window 5 min
|
|
118
|
+
|
|
119
|
+
router.use(authenticateFromGateway({ protocol, logger, onRejected: publishSecurityEvent })); // secret = env GATEWAY_SIGNING_SECRET (≥ 32 chars)
|
|
120
|
+
router.delete('/records/:id', requirePermission('records.delete'), requireStepUp('record-delete', 'DELETE /records/:id'), controller.remove);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- **Signature:** `x-gateway-signature: v2=<hex>` = HMAC-SHA256 over `[domain, METHOD, path (no query), x-gateway-ts, …identity headers]`
|
|
124
|
+
(JSON-array encoded) in this order: `x-user-id, x-user-email, <mapped scope headers>, <mapped attr headers>,
|
|
125
|
+
x-user-roles, x-user-permissions, x-is-superadmin, x-otp-verified, x-step-up-at, x-session-id, x-user-scopes, x-user-attrs`.
|
|
126
|
+
Missing headers sign as empty strings. `x-gateway-ts` and `x-step-up-at` are epoch milliseconds.
|
|
127
|
+
- **Rejected (401 `UNAUTHENTICATED`):** missing signature, wrong version, timestamp > 30 s old or > 5 s ahead,
|
|
128
|
+
another method/path/secret/domain, any tampered header. No `x-user-id` → anonymous (`optionalAuthFromGateway` continues).
|
|
129
|
+
- **Guards:** `requirePermission(...)` (any), `requireAllPermissions([...])`, `requireRole`, `requireOtpVerified()`
|
|
130
|
+
→ 403 `FORBIDDEN`; a signed super admin passes every permission guard. `requireStepUp(scope)` → 403
|
|
131
|
+
`STEP_UP_REQUIRED` with `{ scope }` unless `stepUpAt` is within the window. Single-use proofs are consumed by the gateway.
|
|
132
|
+
- **Gateway side:** `stripProtectedHeaders(req.headers, protocol)` on ingress, then `signedIdentityHeaders({ user, method, path, secret, protocol })`.
|
|
133
|
+
- **Tests:** `identityHeaders({ user, secret, protocol, method, path })` from `/testing`.
|
|
134
|
+
|
|
135
|
+
### Internal service calls (`common`, `express`)
|
|
136
|
+
|
|
137
|
+
Each service has its own credential; each internal route lists its callers.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { assertInternalServiceAuthConfigured, createInternalClient } from '@heavybit/shared-lib';
|
|
141
|
+
import { createInternalCallerGuards } from '@heavybit/shared-lib/express';
|
|
142
|
+
|
|
143
|
+
assertInternalServiceAuthConfigured({ calls: true, serves: true }); // throws in staging/production if missing
|
|
144
|
+
|
|
145
|
+
// Serving side (credentials = env INTERNAL_SERVICE_CREDENTIALS, {"caller-name":"<secret>"}):
|
|
146
|
+
const { requireInternalCaller } = createInternalCallerGuards({ pathPrefix: '/internal', logger });
|
|
147
|
+
router.get('/internal/records/:id', requireInternalCaller(['billing-service']), controller.get);
|
|
148
|
+
|
|
149
|
+
// Calling side (caller = env INTERNAL_SERVICE_NAME + INTERNAL_SERVICE_CREDENTIAL):
|
|
150
|
+
const records = createInternalClient({ target: 'records-service', baseUrl: env.RECORDS_INTERNAL_URL, logger });
|
|
151
|
+
try {
|
|
152
|
+
const record = await records.get('/records/01J…', { schema: recordSchema });
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error instanceof records.UnavailableError) { /* down: degrade or 503 */ } else throw error; // 4xx: AppError with remote code
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- **Guard:** 401 `UNAUTHENTICATED` (missing/unknown name, wrong secret; constant-time), 403 `FORBIDDEN` (known caller
|
|
159
|
+
not listed, or any request outside `pathPrefix`). Sets `req.internalCaller`. Logs reason, caller and path only.
|
|
160
|
+
- **Client:** timeout 3 s per attempt; GET retried twice (100 ms, 400 ms) on network errors, timeouts and 502/503/504;
|
|
161
|
+
breaker opens after 5 unavailable calls for 30 s, then one trial. Network/timeout/5xx/open breaker/unreadable or
|
|
162
|
+
schema-mismatched body → `{Target}UnavailableError` (503). 4xx → `AppError` with the remote status, code, message, errors.
|
|
163
|
+
- **Headers sent:** `x-internal-service-name`, `x-internal-service-secret`, `x-correlation-id` (from the request
|
|
164
|
+
context), `idempotency-key` (when given) and `identityHeaders` unchanged. Gateway signatures are bound to method and
|
|
165
|
+
path, so re-sign the user's identity for the exact internal call with `signedIdentityHeaders`.
|
|
166
|
+
|
|
167
|
+
### Config, Redis keyspace and rate-limit keys (`config`, `cache`, `rate-limit`)
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { z } from 'zod';
|
|
171
|
+
import { loadEnv, createRedisKeyspace, rateLimitKey, sensitivePart } from '@heavybit/shared-lib';
|
|
172
|
+
import { createCache, createRedisClient, setWithTtl } from '@heavybit/shared-lib/cache';
|
|
173
|
+
|
|
174
|
+
// Base env (NODE_ENV, PORT, SERVICE_NAME, DB_*, REDIS_*, RABBITMQ_*, QUEUE_PREFIX, CACHE_KEY_PREFIX,
|
|
175
|
+
// GATEWAY_SIGNING_SECRET, LOG_LEVEL, INTERNAL_SERVICE_*) + the service's own. Bad config → names on stderr, exit 1.
|
|
176
|
+
export const config = loadEnv(z.object({ UPSTREAM_INTERNAL_URL: z.string().url() }));
|
|
177
|
+
|
|
178
|
+
const ks = createRedisKeyspace({ app: APP_SLUG, service: config.SERVICE_NAME, env: config.CACHE_KEY_PREFIX });
|
|
179
|
+
const redis = createRedisClient(config, { logger }); // no keyPrefix: the keyspace prefixes
|
|
180
|
+
const cache = createCache({ redis, keyspace: ks, logger });
|
|
181
|
+
await cache.set(ks.key('widget', id), widget, { ttlSec: 300 }); // TTL required, > 0
|
|
182
|
+
await setWithTtl(redis, ks.key('otp', sensitivePart(email)), hash, 600);
|
|
183
|
+
const key = rateLimitKey(ks, { scope: 'sign-in', ip: req.ip, email });
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- Keys read `{app}:{env}:{service}:{purpose}:…`; the app slug is the caller's parameter. In staging and
|
|
187
|
+
production a missing `CACHE_KEY_PREFIX` stops startup (both `loadEnv` and `createRedisKeyspace`).
|
|
188
|
+
- Parts are encoded (`:`, `*`, `?`, `[`, `]` cannot add segments or widen a SCAN). Parts that are long,
|
|
189
|
+
email-like, phone-like or wrapped in `sensitivePart` are hashed to `h.<sha256>`.
|
|
190
|
+
- No `KEYS`/`FLUSH*` helpers exist; scan with `ks.pattern(purpose)` and `SCAN` if you must.
|
|
191
|
+
- `createLock(redis, prefix)` takes a string prefix: pass `` `${ks.key('lock')}:` `` so its keys stay inside the
|
|
192
|
+
keyspace. `RedisIdempotencyStore(redis, keyspace)` builds its keys from the keyspace itself.
|
|
193
|
+
|
|
194
|
+
### Messaging: envelope, outbox, consumers, DLQ (`messaging`)
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
// container.ts — app slug, prefix and service always come from config
|
|
198
|
+
const messaging = createMessaging({ url: amqpUrlFromEnv(config), app: APP_SLUG, prefix: config.QUEUE_PREFIX, service: config.SERVICE_NAME });
|
|
199
|
+
const relay = new OutboxRelay({ db, bus: messaging.publisher, onMetrics: (m) => lagGauge.set(m.lagMs) });
|
|
200
|
+
|
|
201
|
+
// publish: stage in the same transaction as the change; the relay publishes after commit
|
|
202
|
+
await db.transaction(async (tx) => { await repo.insert(ctx, row, { tx }); await enqueueOutbox(tx, 'thing.created', { tenantId, thingId: row.id }); });
|
|
203
|
+
|
|
204
|
+
// consume: validated, de-duplicated in the handler's transaction, scope and correlation restored
|
|
205
|
+
await messaging.consumer.subscribe({ queue: 'thing-created', bindings: [{ exchange: 'events', routingKey: 'thing.created' }],
|
|
206
|
+
schema, dedupe: new ProcessedEventsDeduper(db), scopeFrom: (d) => ({ tenantId: d.tenantId }),
|
|
207
|
+
handler: async ({ data, tx, scope }) => service.onCreated(scope!, data, tx!) });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
- Envelope `{ routingKey, data: { ...payload, eventId, version, actorUserId? }, timestamp }`; AMQP `messageId` =
|
|
211
|
+
`eventId`, persistent JSON, header `x-correlation-id`.
|
|
212
|
+
- Names: exchanges `{app}.{prefix}.events` (topic) / `.commands` (direct); queues `{prefix}.{service}.{purpose}`;
|
|
213
|
+
per-service DLX `{app}.{prefix}.{service}.dlx` → `{queue}.dlq`; retry queues `{queue}.retry.{ms}`.
|
|
214
|
+
- Consumer: manual ack; invalid JSON/payload/scope → DLQ at once; handler errors retry after 2, 4, 8, 16 s
|
|
215
|
+
(`maxAttempts` 5) then DLQ; `NonRetryableError` → DLQ at once. `BaseConsumer` is the class form.
|
|
216
|
+
- Relay: batches of 100 `FOR UPDATE SKIP LOCKED`, marks `published_at` after the broker confirm, keeps unroutable
|
|
217
|
+
commands pending, backs off exponentially while the broker is down.
|
|
218
|
+
- `publisher.scheduleDelayed(queue, payload, delayMs)` for delayed delivery (bucketed per-message TTL).
|
|
219
|
+
|
|
220
|
+
### Data access: Drizzle, scopes, pagination, migrations (`database`, `knex`)
|
|
221
|
+
|
|
222
|
+
**Repositories use Drizzle through these bases; raw scoped queries and Knex in `src/` are forbidden; Knex is
|
|
223
|
+
for migrations and seeds only.**
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { createDb, defineScope, definePlatformScope, OwnerScope, ScopedRepository, knexConfig } from '@heavybit/shared-lib/database';
|
|
227
|
+
import { parseCursorQuery } from '@heavybit/shared-lib';
|
|
228
|
+
|
|
229
|
+
// container.ts — no module-level singleton
|
|
230
|
+
const { db, ping, close } = createDb({ env: config, schema });
|
|
231
|
+
|
|
232
|
+
// Scope bindings are app parameters (the app's contracts package defines its own)
|
|
233
|
+
const workspaceScope = defineScope({ name: 'workspace', column: 'workspace_id', contextKey: 'workspaceId' });
|
|
234
|
+
const ownerScope = new OwnerScope({ name: 'owner', column: 'user_id' });
|
|
235
|
+
const platformScope = definePlatformScope({ name: 'platform', column: 'workspace_id', predicate: (u: AuthUser) => u.isSuperAdmin });
|
|
236
|
+
|
|
237
|
+
class NoteRepository extends ScopedRepository<typeof notes, ScopeContext<'workspaceId'>> {
|
|
238
|
+
findByTitle(ctx: ScopeContext<'workspaceId'>, title: string, tx?: DbExecutor) {
|
|
239
|
+
return this.executor(tx).select().from(this.table).where(this.scoped(ctx, eq(this.table.title, title)));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const notesRepo = new NoteRepository(db, notes, workspaceScope);
|
|
243
|
+
const page = await notesRepo.findPage(ctx, { ...parseCursorQuery(req.query), orderBy: { column: 'created_at' } });
|
|
244
|
+
await db.transaction(async (tx) => notesRepo.insert(ctx, row, { tx }));
|
|
245
|
+
|
|
246
|
+
// knexfile.js
|
|
247
|
+
module.exports = knexConfig(config); // ./migrations, ./seeds, .cjs only
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
- `ScopedRepository` methods: `findById` (null when absent or out of scope), `findMany`, `findPage`, `count`,
|
|
251
|
+
`insert` (stamps the scope column; a row naming another scope → 403 `SCOPE_MISMATCH`), `update` (refuses to
|
|
252
|
+
change the scope column, `SCOPE_IMMUTABLE`) and `delete`. Every method takes the scope context first and an
|
|
253
|
+
optional `{ tx }` last. Subclasses build WHERE clauses only with `this.scoped(ctx, ...)`.
|
|
254
|
+
- A missing scope value is 403 `SCOPE_REQUIRED` (`OWNER_REQUIRED` for an owner scope). A platform grant comes only
|
|
255
|
+
from `platformScope.authorize(identity)` (403 `PLATFORM_SCOPE_DENIED` otherwise); its queries still name the scope
|
|
256
|
+
column (`IS NOT NULL`) and inserts must carry the scope value.
|
|
257
|
+
- Pagination: `parseCursorQuery` (limit 1–100, default 20), `encodeCursor`/`decodeCursor` (opaque base64url of
|
|
258
|
+
`{ sortKey, id }`; bad cursors → 422), `buildCursorPage`.
|
|
259
|
+
- Migrations (`/knex`): `outboxColumns(t, knex)` (incl. `exchange`, `correlation_id`), `processedEventsColumns(t, knex)` (unique `consumer` + `event_id`),
|
|
260
|
+
`schedulerRunsColumns(t, knex)` (unique `job` + `run_key`), `scopedIndex(t, scopeColumn, cols)` (always leads with
|
|
261
|
+
the scope column). Matching Drizzle tables: `outboxTable`, `processedEventsTable`, `schedulerRunsTable`.
|
package/dist/cjs/access/index.js
CHANGED
|
@@ -26,25 +26,33 @@ function grants(granted, required) {
|
|
|
26
26
|
return true;
|
|
27
27
|
return granted.endsWith('.*') && required.startsWith(granted.slice(0, -1));
|
|
28
28
|
}
|
|
29
|
-
/** True when the user holds at least one of `required`. */
|
|
29
|
+
/** True when the user holds at least one of `required`. A super admin holds every permission. */
|
|
30
30
|
function hasPermission(user, ...required) {
|
|
31
31
|
if (!user)
|
|
32
32
|
return false;
|
|
33
|
+
if (user.isSuperAdmin)
|
|
34
|
+
return true;
|
|
33
35
|
return required.some((perm) => user.permissions.some((g) => grants(g, perm)));
|
|
34
36
|
}
|
|
37
|
+
/** True when the user holds every one of `required`. A super admin holds every permission. */
|
|
35
38
|
function hasAllPermissions(user, ...required) {
|
|
36
39
|
if (!user)
|
|
37
40
|
return false;
|
|
41
|
+
if (user.isSuperAdmin)
|
|
42
|
+
return true;
|
|
38
43
|
return required.every((perm) => user.permissions.some((g) => grants(g, perm)));
|
|
39
44
|
}
|
|
45
|
+
/** True when the user has at least one of `roles`. Prefer permission checks; roles are coarse. */
|
|
40
46
|
function hasRole(user, ...roles) {
|
|
41
47
|
return !!user && roles.some((role) => user.roles.includes(role));
|
|
42
48
|
}
|
|
49
|
+
/** Returns the user, or throws UnauthorizedError (401) when there is none. */
|
|
43
50
|
function requireUser(user) {
|
|
44
51
|
if (!user)
|
|
45
52
|
throw new errors_js_1.UnauthorizedError();
|
|
46
53
|
return user;
|
|
47
54
|
}
|
|
55
|
+
/** Returns the user when they hold any of `required` (super admin always does); throws 401/403 otherwise. */
|
|
48
56
|
function assertPermission(user, ...required) {
|
|
49
57
|
const u = requireUser(user);
|
|
50
58
|
if (!hasPermission(u, ...required))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/access/index.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/access/index.ts"],"names":[],"mappings":";;;AAoBA,sCAIC;AAGD,8CAIC;AAGD,0BAEC;AAGD,kCAGC;AAGD,4CAIC;AAGD,4BAEC;AAMD,wDAKC;AAMD,sCAKC;AAOD,oDASC;AAMD,sDAGC;AAQD,kCAIC;AAjHD,mDAAwE;AAGxE;;;;;;GAMG;AACU,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,SAAS,MAAM,CAAC,OAAe,EAAE,QAAgB;IAC/C,IAAI,OAAO,KAAK,2BAAmB,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;AAID,iGAAiG;AACjG,SAAgB,aAAa,CAAC,IAAyC,EAAE,GAAG,QAAkB;IAC5F,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,IAAI,IAAI,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AAChF,CAAC;AAED,8FAA8F;AAC9F,SAAgB,iBAAiB,CAAC,IAAyC,EAAE,GAAG,QAAkB;IAChG,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,IAAI,IAAI,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AACjF,CAAC;AAED,kGAAkG;AAClG,SAAgB,OAAO,CAAC,IAAgD,EAAE,GAAG,KAAe;IAC1F,OAAO,CAAC,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;AACnE,CAAC;AAED,8EAA8E;AAC9E,SAAgB,WAAW,CAAC,IAAiC;IAC3D,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,6BAAiB,EAAE,CAAC;IACzC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,6GAA6G;AAC7G,SAAgB,gBAAgB,CAAC,IAAiC,EAAE,GAAG,QAAkB;IACvF,MAAM,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,aAAa,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC;QAAE,MAAM,IAAI,0BAAc,EAAE,CAAC;IAC/D,OAAO,CAAC,CAAC;AACX,CAAC;AAED,mFAAmF;AACnF,SAAgB,QAAQ,CAAC,IAAiD,EAAE,KAAa;IACvF,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,SAAgB,sBAAsB,CAAC,IAAiC,EAAE,WAAmB,EAAE,gBAAyB;IACtH,MAAM,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,CAAC,EAAE,KAAK,WAAW;QAAE,OAAO,CAAC,CAAC;IACnC,IAAI,gBAAgB,IAAI,aAAa,CAAC,CAAC,EAAE,gBAAgB,CAAC;QAAE,OAAO,CAAC,CAAC;IACrE,MAAM,IAAI,0BAAc,EAAE,CAAC;AAC7B,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa,CAAC,IAAiC,EAAE,KAAa,EAAE,eAAuB,EAAE,gBAAyB;IAChI,MAAM,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC;QAAE,OAAO,CAAC,CAAC;IAC3D,IAAI,gBAAgB,IAAI,aAAa,CAAC,CAAC,EAAE,gBAAgB,CAAC;QAAE,OAAO,CAAC,CAAC;IACrE,MAAM,IAAI,0BAAc,EAAE,CAAC;AAC7B,CAAC;AAED;;;;GAIG;AACH,SAAgB,oBAAoB,CAAC,IAAiC,EAAE,KAAa,EAAE,SAAyB;IAC9G,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;IAC/C,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC;YAAE,MAAM,IAAI,0BAAc,CAAC,wBAAwB,KAAK,EAAE,CAAC,CAAC;QACxF,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC,CAAC,CAAE,CAAC;IACrC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,0BAAc,CAAC,MAAM,KAAK,4BAA4B,CAAC,CAAC;IACxF,MAAM,IAAI,0BAAc,CAAC,YAAY,KAAK,YAAY,EAAE,EAAE,IAAI,EAAE,0BAA0B,EAAE,CAAC,CAAC;AAChG,CAAC;AAED;;;GAGG;AACH,SAAgB,qBAAqB,CAAC,IAAiC,EAAE,KAAa,EAAE,SAAyB;IAC/G,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;IAC/C,OAAO,oBAAoB,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAChE,CAAC;AAID;;;GAGG;AACH,SAAgB,WAAW,CAAC,IAAiC,EAAE,KAAa,EAAE,gBAAyB;IACrG,MAAM,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,gBAAgB,IAAI,aAAa,CAAC,CAAC,EAAE,gBAAgB,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IACjF,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.APPROVAL_STATES = void 0;
|
|
4
|
+
exports.canTransition = canTransition;
|
|
5
|
+
exports.assertTransition = assertTransition;
|
|
6
|
+
exports.isTerminalApprovalState = isTerminalApprovalState;
|
|
7
|
+
exports.assertCanDecide = assertCanDecide;
|
|
8
|
+
exports.assertCanCancel = assertCanCancel;
|
|
9
|
+
exports.approvalExpiresAt = approvalExpiresAt;
|
|
10
|
+
exports.createApprovalPolicy = createApprovalPolicy;
|
|
11
|
+
const index_js_1 = require("../access/index.js");
|
|
12
|
+
const errors_js_1 = require("../common/errors.js");
|
|
13
|
+
/**
|
|
14
|
+
* Two-person approval state machine. A requester asks for a sensitive operation; a different person who
|
|
15
|
+
* holds both the operation's permission and the app's "decide" permission approves or rejects it; the
|
|
16
|
+
* approved operation is then executed once (or fails). Pending requests expire after a TTL.
|
|
17
|
+
*
|
|
18
|
+
* ```
|
|
19
|
+
* pending ──► approved ──► executed
|
|
20
|
+
* │ └──────► failed
|
|
21
|
+
* ├──► rejected
|
|
22
|
+
* ├──► cancelled
|
|
23
|
+
* └──► expired
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* App values (the decide permission and the TTL) are passed in through {@link createApprovalPolicy}.
|
|
27
|
+
*/
|
|
28
|
+
/** Every approval state. */
|
|
29
|
+
exports.APPROVAL_STATES = ['pending', 'approved', 'rejected', 'cancelled', 'expired', 'executed', 'failed'];
|
|
30
|
+
const TRANSITIONS = Object.freeze({
|
|
31
|
+
pending: ['approved', 'rejected', 'cancelled', 'expired'],
|
|
32
|
+
approved: ['executed', 'failed'],
|
|
33
|
+
rejected: [],
|
|
34
|
+
cancelled: [],
|
|
35
|
+
expired: [],
|
|
36
|
+
executed: [],
|
|
37
|
+
failed: [],
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* True when `from → to` is an allowed move.
|
|
41
|
+
*
|
|
42
|
+
* @example canTransition('pending', 'approved') // true
|
|
43
|
+
*/
|
|
44
|
+
function canTransition(from, to) {
|
|
45
|
+
return TRANSITIONS[from]?.includes(to) ?? false;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Throws unless `from → to` is allowed.
|
|
49
|
+
*
|
|
50
|
+
* @throws ConflictError (`APPROVAL_INVALID_TRANSITION`).
|
|
51
|
+
*/
|
|
52
|
+
function assertTransition(from, to) {
|
|
53
|
+
if (!canTransition(from, to)) {
|
|
54
|
+
throw new errors_js_1.ConflictError(`This request can no longer move from ${from} to ${to}`, { code: 'APPROVAL_INVALID_TRANSITION' });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** True when the state allows no further moves. */
|
|
58
|
+
function isTerminalApprovalState(state) {
|
|
59
|
+
return TRANSITIONS[state].length === 0;
|
|
60
|
+
}
|
|
61
|
+
function assertPending(approval, now) {
|
|
62
|
+
if (approval.state !== 'pending') {
|
|
63
|
+
throw new errors_js_1.ConflictError('This request has already been decided', { code: 'APPROVAL_NOT_PENDING' });
|
|
64
|
+
}
|
|
65
|
+
if (approval.expiresAt && new Date(approval.expiresAt).getTime() <= now.getTime()) {
|
|
66
|
+
throw new errors_js_1.ConflictError('This request has expired', { code: 'APPROVAL_EXPIRED' });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Throws unless `approver` may approve or reject `approval`: it is pending and not expired, the approver
|
|
71
|
+
* is not the requester (even a super admin cannot approve their own request), and holds both the
|
|
72
|
+
* operation permission and `decidePermission`.
|
|
73
|
+
*
|
|
74
|
+
* @param approval The request.
|
|
75
|
+
* @param approver The deciding user.
|
|
76
|
+
* @param decidePermission The app's "may decide approvals" permission.
|
|
77
|
+
* @param now Clock (tests).
|
|
78
|
+
* @throws ConflictError (`APPROVAL_NOT_PENDING`, `APPROVAL_EXPIRED`) or ForbiddenError
|
|
79
|
+
* (`APPROVAL_SELF_DECISION`, `APPROVAL_FORBIDDEN`).
|
|
80
|
+
*/
|
|
81
|
+
function assertCanDecide(approval, approver, decidePermission, now = new Date()) {
|
|
82
|
+
assertPending(approval, now);
|
|
83
|
+
if (approver.id === approval.requesterUserId) {
|
|
84
|
+
throw new errors_js_1.ForbiddenError('You cannot decide your own request', { code: 'APPROVAL_SELF_DECISION' });
|
|
85
|
+
}
|
|
86
|
+
if (!(0, index_js_1.hasAllPermissions)(approver, approval.operationPermission, decidePermission)) {
|
|
87
|
+
throw new errors_js_1.ForbiddenError('You do not have permission to decide this request', { code: 'APPROVAL_FORBIDDEN' });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Throws unless `user` may cancel `approval`: only the requester, and only while it is pending.
|
|
92
|
+
*
|
|
93
|
+
* @throws ConflictError (`APPROVAL_NOT_PENDING`) or ForbiddenError (`APPROVAL_NOT_REQUESTER`).
|
|
94
|
+
*/
|
|
95
|
+
function assertCanCancel(approval, user) {
|
|
96
|
+
if (user.id !== approval.requesterUserId) {
|
|
97
|
+
throw new errors_js_1.ForbiddenError('Only the person who made this request can cancel it', { code: 'APPROVAL_NOT_REQUESTER' });
|
|
98
|
+
}
|
|
99
|
+
if (approval.state !== 'pending') {
|
|
100
|
+
throw new errors_js_1.ConflictError('This request has already been decided', { code: 'APPROVAL_NOT_PENDING' });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* When a request made at `from` expires.
|
|
105
|
+
*
|
|
106
|
+
* @param ttlSeconds Positive lifetime in seconds.
|
|
107
|
+
* @param from Creation time (default now).
|
|
108
|
+
* @returns The expiry instant.
|
|
109
|
+
* @example approvalExpiresAt(3600, new Date('2026-01-01T00:00:00Z')) // 2026-01-01T01:00:00Z
|
|
110
|
+
*/
|
|
111
|
+
function approvalExpiresAt(ttlSeconds, from = new Date()) {
|
|
112
|
+
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)
|
|
113
|
+
throw new RangeError('Approval TTL must be a positive number of seconds');
|
|
114
|
+
return new Date(from.getTime() + ttlSeconds * 1000);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Binds the approval rules to an app's decide permission and TTL.
|
|
118
|
+
*
|
|
119
|
+
* @param options Decide permission and TTL.
|
|
120
|
+
* @returns The bound policy.
|
|
121
|
+
* @example const approvals = createApprovalPolicy({ decidePermission: 'requests.decide', ttlSeconds: 86_400 });
|
|
122
|
+
*/
|
|
123
|
+
function createApprovalPolicy(options) {
|
|
124
|
+
if (!options.decidePermission)
|
|
125
|
+
throw new RangeError('An approval policy needs a decide permission');
|
|
126
|
+
approvalExpiresAt(options.ttlSeconds); // validates the TTL up front
|
|
127
|
+
return Object.freeze({
|
|
128
|
+
decidePermission: options.decidePermission,
|
|
129
|
+
ttlSeconds: options.ttlSeconds,
|
|
130
|
+
canTransition,
|
|
131
|
+
assertTransition,
|
|
132
|
+
assertCanDecide: (approval, approver, now) => assertCanDecide(approval, approver, options.decidePermission, now),
|
|
133
|
+
assertCanCancel,
|
|
134
|
+
expiresAt: (from) => approvalExpiresAt(options.ttlSeconds, from),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
//# 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,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildAuditEvent = exports.MAX_USER_AGENT_LENGTH = void 0;
|
|
4
|
+
exports.createAuditEventBuilder = createAuditEventBuilder;
|
|
5
|
+
const phone_js_1 = require("../common/phone.js");
|
|
6
|
+
const time_js_1 = require("../common/time.js");
|
|
7
|
+
const redact_js_1 = require("../logging/redact.js");
|
|
8
|
+
/** User-agent strings are cut to this length. */
|
|
9
|
+
exports.MAX_USER_AGENT_LENGTH = 255;
|
|
10
|
+
/** Stable JSON form for deep comparison (object keys sorted; Dates as ISO strings). */
|
|
11
|
+
function canonical(value) {
|
|
12
|
+
return JSON.stringify(value ?? null, (_key, v) => {
|
|
13
|
+
if (v && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date)) {
|
|
14
|
+
return Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b)));
|
|
15
|
+
}
|
|
16
|
+
return v;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function diff(before, after, classifier) {
|
|
20
|
+
const changes = {};
|
|
21
|
+
const redactedFields = [];
|
|
22
|
+
const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
|
|
23
|
+
for (const key of keys) {
|
|
24
|
+
const was = before[key] ?? null;
|
|
25
|
+
const now = after[key] ?? null;
|
|
26
|
+
if (canonical(was) === canonical(now))
|
|
27
|
+
continue;
|
|
28
|
+
if (classifier.isSecret(key)) {
|
|
29
|
+
redactedFields.push(key);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const clean = (value) => classifier.isPhone(key) && (typeof value === 'string' || typeof value === 'number') ? (0, phone_js_1.maskPhone)(String(value)) : (0, redact_js_1.redactValue)(value, classifier);
|
|
33
|
+
changes[key] = { before: clean(was), after: clean(now) };
|
|
34
|
+
}
|
|
35
|
+
return { changes, redactedFields };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Creates an audit-event builder with the app's extra redaction words.
|
|
39
|
+
*
|
|
40
|
+
* @param options Extra redaction words.
|
|
41
|
+
* @returns `buildAuditEvent` bound to that list.
|
|
42
|
+
* @example const buildAudit = createAuditEventBuilder({ redactKeys: ['nationalId'] });
|
|
43
|
+
*/
|
|
44
|
+
function createAuditEventBuilder(options = {}) {
|
|
45
|
+
const classifier = (0, redact_js_1.createKeyClassifier)(options.redactKeys ?? []);
|
|
46
|
+
return (input) => {
|
|
47
|
+
if (!input.action || !input.entityType)
|
|
48
|
+
throw new RangeError('Audit events need an action and an entity type');
|
|
49
|
+
const { changes, redactedFields } = diff(input.before ?? {}, input.after ?? {}, classifier);
|
|
50
|
+
return {
|
|
51
|
+
action: input.action,
|
|
52
|
+
entityType: input.entityType,
|
|
53
|
+
entityId: input.entityId,
|
|
54
|
+
scope: input.scope ? { key: input.scope.key, id: input.scope.id } : null,
|
|
55
|
+
actorUserId: input.actor?.id ?? null,
|
|
56
|
+
outcome: input.outcome ?? 'success',
|
|
57
|
+
occurredAt: (0, time_js_1.toUtcIso)(input.occurredAt ?? new Date()),
|
|
58
|
+
changes,
|
|
59
|
+
redactedFields,
|
|
60
|
+
ip: input.ip ?? null,
|
|
61
|
+
userAgent: input.userAgent ? input.userAgent.slice(0, exports.MAX_USER_AGENT_LENGTH) : null,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Builds an audit event with the default redaction list: the diff holds only changed fields; secret
|
|
67
|
+
* fields are dropped (named in `redactedFields`); phone fields are masked to their last 4 digits.
|
|
68
|
+
*
|
|
69
|
+
* @param input Action, entity, scope, actor, before/after state and request facts.
|
|
70
|
+
* @returns The event.
|
|
71
|
+
* @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') })
|
|
72
|
+
*/
|
|
73
|
+
exports.buildAuditEvent = createAuditEventBuilder();
|
|
74
|
+
//# 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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"}
|