@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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-caller service-to-service credentials.
|
|
3
|
+
*
|
|
4
|
+
* Layer: plumbing (no HTTP framework). Key rules:
|
|
5
|
+
* - Every calling service has its **own** secret (`INTERNAL_SERVICE_CREDENTIAL`) and sends it with its name
|
|
6
|
+
* (`INTERNAL_SERVICE_NAME`). A receiving service holds `INTERNAL_SERVICE_CREDENTIALS`, a JSON map of
|
|
7
|
+
* caller name → the secret it accepts from that caller, and each internal route lists the callers it allows.
|
|
8
|
+
* One leaked credential therefore reaches only the routes meant for that one caller.
|
|
9
|
+
* - Secrets are compared in constant time (SHA-256 digests + `timingSafeEqual`, so lengths don't leak) and
|
|
10
|
+
* are never logged or put in error messages.
|
|
11
|
+
* - Staging and production refuse to start without the credentials a service needs
|
|
12
|
+
* ({@link assertInternalServiceAuthConfigured}); development and test may run without them.
|
|
13
|
+
*
|
|
14
|
+
* App-independent: caller names are whatever the app's compose service names are.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
17
|
+
import { ConfigError } from '../config/index.js';
|
|
18
|
+
import { CONTEXT_HEADERS } from '../identity/index.js';
|
|
19
|
+
import { MIN_INTERNAL_CREDENTIAL_LENGTH, SERVICE_NAME_PATTERN } from './internal-constants.js';
|
|
20
|
+
/** Header carrying the calling service's name. */
|
|
21
|
+
export const INTERNAL_SERVICE_NAME_HEADER = CONTEXT_HEADERS.INTERNAL_SERVICE_NAME;
|
|
22
|
+
/** Header carrying the calling service's own secret. */
|
|
23
|
+
export const INTERNAL_SERVICE_SECRET_HEADER = CONTEXT_HEADERS.INTERNAL_SECRET;
|
|
24
|
+
export { MIN_INTERNAL_CREDENTIAL_LENGTH, SERVICE_NAME_PATTERN } from './internal-constants.js';
|
|
25
|
+
/** Environments where missing or weak internal credentials stop the service from starting. */
|
|
26
|
+
const STRICT_ENVIRONMENTS = new Set(['staging', 'production']);
|
|
27
|
+
const read = (env, key) => {
|
|
28
|
+
const value = env[key];
|
|
29
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Parses `INTERNAL_SERVICE_CREDENTIALS` (a JSON object of caller name → secret).
|
|
33
|
+
*
|
|
34
|
+
* @param raw The env value; empty or undefined gives `{}`.
|
|
35
|
+
* @returns A frozen map of caller name → secret.
|
|
36
|
+
* @throws ConfigError when the JSON is malformed, not an object, a name is not a valid service name, or a
|
|
37
|
+
* secret is not a non-empty string. The message names the bad key, never a secret.
|
|
38
|
+
* @example parseInternalServiceCredentials('{"alpha-service":"<secret>"}') // { 'alpha-service': '<secret>' }
|
|
39
|
+
*/
|
|
40
|
+
export function parseInternalServiceCredentials(raw) {
|
|
41
|
+
if (!raw?.trim())
|
|
42
|
+
return Object.freeze({});
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(raw);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new ConfigError('INTERNAL_SERVICE_CREDENTIALS must be a JSON object of service name to secret');
|
|
49
|
+
}
|
|
50
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
51
|
+
throw new ConfigError('INTERNAL_SERVICE_CREDENTIALS must be a JSON object of service name to secret');
|
|
52
|
+
}
|
|
53
|
+
const credentials = {};
|
|
54
|
+
for (const [name, secret] of Object.entries(parsed)) {
|
|
55
|
+
if (!SERVICE_NAME_PATTERN.test(name))
|
|
56
|
+
throw new ConfigError(`INTERNAL_SERVICE_CREDENTIALS has an invalid service name: "${name}"`);
|
|
57
|
+
if (typeof secret !== 'string' || !secret)
|
|
58
|
+
throw new ConfigError(`INTERNAL_SERVICE_CREDENTIALS has an empty or non-string secret for "${name}"`);
|
|
59
|
+
credentials[name] = secret;
|
|
60
|
+
}
|
|
61
|
+
return Object.freeze(credentials);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Reads `INTERNAL_SERVICE_NAME`, `INTERNAL_SERVICE_CREDENTIAL` and `INTERNAL_SERVICE_CREDENTIALS`.
|
|
65
|
+
*
|
|
66
|
+
* @param env process.env (default) or a parsed config object.
|
|
67
|
+
* @returns The settings; missing values are empty.
|
|
68
|
+
* @throws ConfigError when `INTERNAL_SERVICE_CREDENTIALS` is malformed.
|
|
69
|
+
* @example const internalAuth = internalServiceAuthConfigFromEnv();
|
|
70
|
+
*/
|
|
71
|
+
export function internalServiceAuthConfigFromEnv(env = process.env) {
|
|
72
|
+
return {
|
|
73
|
+
name: read(env, 'INTERNAL_SERVICE_NAME'),
|
|
74
|
+
credential: read(env, 'INTERNAL_SERVICE_CREDENTIAL'),
|
|
75
|
+
credentials: parseInternalServiceCredentials(read(env, 'INTERNAL_SERVICE_CREDENTIALS')),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The headers a service adds to every internal call it makes.
|
|
80
|
+
*
|
|
81
|
+
* @param caller This service's name and secret.
|
|
82
|
+
* @returns `{ 'x-internal-service-name', 'x-internal-service-secret' }`.
|
|
83
|
+
* @example fetch(url, { headers: { ...internalServiceHeaders({ name, secret }) } });
|
|
84
|
+
*/
|
|
85
|
+
export function internalServiceHeaders(caller) {
|
|
86
|
+
return { [INTERNAL_SERVICE_NAME_HEADER]: caller.name, [INTERNAL_SERVICE_SECRET_HEADER]: caller.secret };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Startup guard. In staging and production it refuses to start when a calling service lacks a valid name or a
|
|
90
|
+
* credential of at least 32 characters, or when a serving service has no accepted callers or any accepted
|
|
91
|
+
* secret shorter than 32 characters. In other environments it only checks that what is set parses.
|
|
92
|
+
*
|
|
93
|
+
* @param options Whether the service calls and/or serves internal routes.
|
|
94
|
+
* @throws ConfigError listing every problem (names only, never secret values).
|
|
95
|
+
* @example assertInternalServiceAuthConfigured({ calls: true, serves: true });
|
|
96
|
+
*/
|
|
97
|
+
export function assertInternalServiceAuthConfigured(options) {
|
|
98
|
+
const env = options.env ?? process.env;
|
|
99
|
+
const config = internalServiceAuthConfigFromEnv(env);
|
|
100
|
+
if (!STRICT_ENVIRONMENTS.has(options.nodeEnv ?? read(env, 'NODE_ENV')))
|
|
101
|
+
return;
|
|
102
|
+
const problems = [];
|
|
103
|
+
if (options.calls) {
|
|
104
|
+
if (!SERVICE_NAME_PATTERN.test(config.name))
|
|
105
|
+
problems.push('INTERNAL_SERVICE_NAME is missing or not a valid service name');
|
|
106
|
+
if (config.credential.length < MIN_INTERNAL_CREDENTIAL_LENGTH) {
|
|
107
|
+
problems.push(`INTERNAL_SERVICE_CREDENTIAL must be at least ${MIN_INTERNAL_CREDENTIAL_LENGTH} characters`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (options.serves) {
|
|
111
|
+
const callers = Object.keys(config.credentials);
|
|
112
|
+
if (!callers.length)
|
|
113
|
+
problems.push('INTERNAL_SERVICE_CREDENTIALS is empty: no internal caller can authenticate');
|
|
114
|
+
const weak = callers.filter((name) => (config.credentials[name] ?? '').length < MIN_INTERNAL_CREDENTIAL_LENGTH);
|
|
115
|
+
if (weak.length)
|
|
116
|
+
problems.push(`INTERNAL_SERVICE_CREDENTIALS has secrets shorter than ${MIN_INTERNAL_CREDENTIAL_LENGTH} characters for: ${weak.join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
if (problems.length)
|
|
119
|
+
throw new ConfigError(`Refusing to start: ${problems.join('; ')}`);
|
|
120
|
+
}
|
|
121
|
+
const digest = (value) => createHash('sha256').update(value).digest();
|
|
122
|
+
/** A fixed value to compare against when the caller is unknown, so unknown and known callers take the same time. */
|
|
123
|
+
const UNKNOWN_CALLER_DIGEST = digest('unknown-caller');
|
|
124
|
+
const header = (headers, name) => {
|
|
125
|
+
const value = headers[name];
|
|
126
|
+
return typeof value === 'string' ? value : '';
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Checks the internal headers against the accepted credentials and the route's caller list.
|
|
130
|
+
* 401 when the name or secret is missing, the name is unknown or the secret is wrong; 403 when a correctly
|
|
131
|
+
* authenticated caller is not in `allowed`.
|
|
132
|
+
*
|
|
133
|
+
* @param input Request headers, accepted credentials and the route's allowed callers.
|
|
134
|
+
* @returns A verdict; the caller name is included when one was sent (safe to log), the secret never is.
|
|
135
|
+
* @example verifyInternalCaller({ headers: req.headers, credentials, allowed: ['alpha-service'] });
|
|
136
|
+
*/
|
|
137
|
+
export function verifyInternalCaller(input) {
|
|
138
|
+
const caller = header(input.headers, INTERNAL_SERVICE_NAME_HEADER);
|
|
139
|
+
const secret = header(input.headers, INTERNAL_SERVICE_SECRET_HEADER);
|
|
140
|
+
if (!caller || !secret)
|
|
141
|
+
return { ok: false, status: 401, reason: 'missing-credentials', caller: caller || undefined };
|
|
142
|
+
const expected = Object.prototype.hasOwnProperty.call(input.credentials, caller) ? input.credentials[caller] : undefined;
|
|
143
|
+
const matches = timingSafeEqual(digest(secret), expected ? digest(expected) : UNKNOWN_CALLER_DIGEST) && expected !== undefined;
|
|
144
|
+
if (!matches)
|
|
145
|
+
return { ok: false, status: 401, reason: 'invalid-credentials', caller };
|
|
146
|
+
if (!input.allowed.includes(caller))
|
|
147
|
+
return { ok: false, status: 403, reason: 'caller-not-allowed', caller };
|
|
148
|
+
return { ok: true, caller };
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=internal-service-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-service-auth.js","sourceRoot":"","sources":["../../../src/common/internal-service-auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import Decimal from 'decimal.js';
|
|
2
|
+
/**
|
|
3
|
+
* Exact money arithmetic. Amounts travel and persist as decimal strings (`"1250.00"`) and are only ever
|
|
4
|
+
* computed with a decimal type, never JS numbers. The currency is always passed in: shared-lib has no
|
|
5
|
+
* default currency.
|
|
6
|
+
*
|
|
7
|
+
* Rounding is half-up (half away from zero) to the currency's minor unit, after every operation that can
|
|
8
|
+
* produce extra digits (`mul`), so a `Money` value always holds a whole number of minor units.
|
|
9
|
+
*/
|
|
10
|
+
/** A decimal.js constructor with enough precision for any realistic amount and half-up rounding. */
|
|
11
|
+
const Dec = Decimal.clone({ precision: 40, rounding: Decimal.ROUND_HALF_UP, toExpNeg: -40, toExpPos: 40 });
|
|
12
|
+
const CODE_PATTERN = /^[A-Z]{3}$/;
|
|
13
|
+
const AMOUNT_PATTERN = /^-?\d+(\.\d+)?$/;
|
|
14
|
+
/**
|
|
15
|
+
* Resolves a currency code or spec, validating both.
|
|
16
|
+
*
|
|
17
|
+
* @param currency ISO 4217 code (minor units from `Intl`) or `{ code, minorUnits }`.
|
|
18
|
+
* @returns The spec.
|
|
19
|
+
* @throws RangeError for a malformed code or minor-unit count.
|
|
20
|
+
* @example resolveCurrency('USD') // { code: 'USD', minorUnits: 2 }
|
|
21
|
+
*/
|
|
22
|
+
export function resolveCurrency(currency) {
|
|
23
|
+
if (typeof currency !== 'string') {
|
|
24
|
+
if (!CODE_PATTERN.test(currency.code))
|
|
25
|
+
throw new RangeError(`Invalid currency code "${currency.code}"`);
|
|
26
|
+
if (!Number.isInteger(currency.minorUnits) || currency.minorUnits < 0 || currency.minorUnits > 6) {
|
|
27
|
+
throw new RangeError(`Invalid minor units ${currency.minorUnits} for ${currency.code}`);
|
|
28
|
+
}
|
|
29
|
+
return { code: currency.code, minorUnits: currency.minorUnits };
|
|
30
|
+
}
|
|
31
|
+
if (!CODE_PATTERN.test(currency))
|
|
32
|
+
throw new RangeError(`Invalid currency code "${currency}"`);
|
|
33
|
+
const minorUnits = new Intl.NumberFormat('en', { style: 'currency', currency }).resolvedOptions().maximumFractionDigits ?? 2;
|
|
34
|
+
return { code: currency, minorUnits };
|
|
35
|
+
}
|
|
36
|
+
function toDecimal(value, what) {
|
|
37
|
+
if (typeof value === 'number' && !Number.isFinite(value))
|
|
38
|
+
throw new RangeError(`${what} must be finite`);
|
|
39
|
+
const text = String(value).trim();
|
|
40
|
+
if (!AMOUNT_PATTERN.test(text))
|
|
41
|
+
throw new RangeError(`${what} must be a decimal number, got "${value}"`);
|
|
42
|
+
return new Dec(text);
|
|
43
|
+
}
|
|
44
|
+
/** An immutable amount of one currency, always a whole number of minor units. */
|
|
45
|
+
export class Money {
|
|
46
|
+
value;
|
|
47
|
+
currency;
|
|
48
|
+
constructor(value,
|
|
49
|
+
/** The amount's currency. */
|
|
50
|
+
currency) {
|
|
51
|
+
this.value = value;
|
|
52
|
+
this.currency = currency;
|
|
53
|
+
Object.freeze(this);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parses a decimal string. Strict: no thousands separators, symbols or more decimals than the
|
|
57
|
+
* currency allows (D5), so a stored value is never silently rounded on the way in.
|
|
58
|
+
*
|
|
59
|
+
* @param amount e.g. `"1250.5"` or `"-3.25"`.
|
|
60
|
+
* @param currency Code or spec.
|
|
61
|
+
* @returns The amount.
|
|
62
|
+
* @throws RangeError for anything else.
|
|
63
|
+
* @example Money.fromDecimalString('19.99', 'USD')
|
|
64
|
+
*/
|
|
65
|
+
static fromDecimalString(amount, currency) {
|
|
66
|
+
const spec = resolveCurrency(currency);
|
|
67
|
+
if (typeof amount !== 'string')
|
|
68
|
+
throw new RangeError('Amount must be a decimal string');
|
|
69
|
+
const value = toDecimal(amount, 'Amount');
|
|
70
|
+
if (value.decimalPlaces() > spec.minorUnits) {
|
|
71
|
+
throw new RangeError(`Amount "${amount}" has more than ${spec.minorUnits} decimal places for ${spec.code}`);
|
|
72
|
+
}
|
|
73
|
+
return new Money(value, spec);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Builds an amount from an integer count of minor units (e.g. cents).
|
|
77
|
+
*
|
|
78
|
+
* @param units Integer (number within the safe range, or bigint).
|
|
79
|
+
* @param currency Code or spec.
|
|
80
|
+
* @returns The amount.
|
|
81
|
+
* @example Money.fromMinorUnits(1999, 'USD').toDecimalString() // '19.99'
|
|
82
|
+
*/
|
|
83
|
+
static fromMinorUnits(units, currency) {
|
|
84
|
+
const spec = resolveCurrency(currency);
|
|
85
|
+
if (typeof units === 'number' && !Number.isSafeInteger(units))
|
|
86
|
+
throw new RangeError('Minor units must be a safe integer');
|
|
87
|
+
return new Money(new Dec(units.toString()).div(new Dec(10).pow(spec.minorUnits)), spec);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Zero in a currency.
|
|
91
|
+
*
|
|
92
|
+
* @param currency Code or spec.
|
|
93
|
+
* @returns `0` of that currency.
|
|
94
|
+
*/
|
|
95
|
+
static zero(currency) {
|
|
96
|
+
return new Money(new Dec(0), resolveCurrency(currency));
|
|
97
|
+
}
|
|
98
|
+
/** The amount with exactly the currency's decimal places, e.g. `"1250.00"` — the wire and storage form. */
|
|
99
|
+
toDecimalString() {
|
|
100
|
+
return this.value.toFixed(this.currency.minorUnits);
|
|
101
|
+
}
|
|
102
|
+
/** The amount as an integer count of minor units. */
|
|
103
|
+
toMinorUnits() {
|
|
104
|
+
return BigInt(this.value.times(new Dec(10).pow(this.currency.minorUnits)).toFixed(0));
|
|
105
|
+
}
|
|
106
|
+
/** Serialises as the decimal string, so an amount never becomes a JSON number by accident. */
|
|
107
|
+
toJSON() {
|
|
108
|
+
return this.toDecimalString();
|
|
109
|
+
}
|
|
110
|
+
/** Same as {@link toDecimalString}. */
|
|
111
|
+
toString() {
|
|
112
|
+
return this.toDecimalString();
|
|
113
|
+
}
|
|
114
|
+
/** Sum of two amounts of the same currency. */
|
|
115
|
+
add(other) {
|
|
116
|
+
return new Money(this.value.plus(this.same(other).value), this.currency);
|
|
117
|
+
}
|
|
118
|
+
/** Difference of two amounts of the same currency. */
|
|
119
|
+
sub(other) {
|
|
120
|
+
return new Money(this.value.minus(this.same(other).value), this.currency);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Multiplies by a rate (tax, fee, quantity), rounding half-up to the minor unit.
|
|
124
|
+
*
|
|
125
|
+
* @param rate e.g. `"0.16"` or `3`.
|
|
126
|
+
* @returns The rounded product.
|
|
127
|
+
* @example Money.fromDecimalString('10.05', 'USD').mul('0.5').toDecimalString() // '5.03'
|
|
128
|
+
*/
|
|
129
|
+
mul(rate) {
|
|
130
|
+
return new Money(this.round(this.value.times(toDecimal(rate, 'Rate'))), this.currency);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Splits the amount in proportion to `ratios`; the parts always sum to the amount exactly. Each part is
|
|
134
|
+
* rounded down to the minor unit and the left-over units go, one each, to the parts with the largest
|
|
135
|
+
* remainders (earlier parts win ties). A negative amount is split like its absolute value, then negated.
|
|
136
|
+
*
|
|
137
|
+
* @param ratios Non-negative weights with a positive sum, e.g. `['1', '1', '1']` or `[70, 30]`.
|
|
138
|
+
* @returns One amount per ratio, in order.
|
|
139
|
+
* @throws RangeError for an empty list, a negative ratio or a zero sum.
|
|
140
|
+
* @example Money.fromDecimalString('100.00', 'USD').allocate([1, 1, 1]).map(String) // ['33.34', '33.33', '33.33']
|
|
141
|
+
*/
|
|
142
|
+
allocate(ratios) {
|
|
143
|
+
if (ratios.length === 0)
|
|
144
|
+
throw new RangeError('allocate needs at least one ratio');
|
|
145
|
+
const weights = ratios.map((r) => toDecimal(r, 'Ratio'));
|
|
146
|
+
if (weights.some((w) => w.isNegative()))
|
|
147
|
+
throw new RangeError('Ratios must not be negative');
|
|
148
|
+
const sum = weights.reduce((acc, w) => acc.plus(w), new Dec(0));
|
|
149
|
+
if (sum.isZero())
|
|
150
|
+
throw new RangeError('Ratios must not all be zero');
|
|
151
|
+
const scale = new Dec(10).pow(this.currency.minorUnits);
|
|
152
|
+
const total = this.value.abs().times(scale);
|
|
153
|
+
const exact = weights.map((w) => total.times(w).div(sum));
|
|
154
|
+
const floors = exact.map((e) => e.floor());
|
|
155
|
+
let leftover = total.minus(floors.reduce((acc, f) => acc.plus(f), new Dec(0))).toNumber();
|
|
156
|
+
const order = exact.map((e, i) => ({ i, rem: e.minus(floors[i]) })).sort((a, b) => b.rem.comparedTo(a.rem) || a.i - b.i);
|
|
157
|
+
for (const { i } of order) {
|
|
158
|
+
if (leftover <= 0)
|
|
159
|
+
break;
|
|
160
|
+
floors[i] = floors[i].plus(1);
|
|
161
|
+
leftover -= 1;
|
|
162
|
+
}
|
|
163
|
+
const sign = this.value.isNegative() ? -1 : 1;
|
|
164
|
+
return floors.map((units) => new Money(units.div(scale).times(sign), this.currency));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Rounds up (toward +∞) to a multiple of `unit`, e.g. whole units for a payment rail that rejects cents.
|
|
168
|
+
*
|
|
169
|
+
* @param unit Positive step in major units, e.g. `"1"` or `"0.05"`; defaults to `"1"`.
|
|
170
|
+
* @returns The rounded-up amount.
|
|
171
|
+
* @example Money.fromDecimalString('99.01', 'USD').ceilToUnit().toDecimalString() // '100.00'
|
|
172
|
+
*/
|
|
173
|
+
ceilToUnit(unit = '1') {
|
|
174
|
+
const step = toDecimal(unit, 'Unit');
|
|
175
|
+
if (step.lte(0))
|
|
176
|
+
throw new RangeError('Unit must be positive');
|
|
177
|
+
if (step.decimalPlaces() > this.currency.minorUnits)
|
|
178
|
+
throw new RangeError(`Unit is finer than the ${this.currency.code} minor unit`);
|
|
179
|
+
return new Money(this.value.div(step).ceil().times(step), this.currency);
|
|
180
|
+
}
|
|
181
|
+
/** -1, 0 or 1 comparing this amount with `other` (same currency). */
|
|
182
|
+
compare(other) {
|
|
183
|
+
return this.value.comparedTo(this.same(other).value);
|
|
184
|
+
}
|
|
185
|
+
/** True when both amounts have the same currency and value. */
|
|
186
|
+
equals(other) {
|
|
187
|
+
return this.currency.code === other.currency.code && this.value.equals(other.value);
|
|
188
|
+
}
|
|
189
|
+
/** True for zero. */
|
|
190
|
+
isZero() {
|
|
191
|
+
return this.value.isZero();
|
|
192
|
+
}
|
|
193
|
+
/** True below zero. */
|
|
194
|
+
isNegative() {
|
|
195
|
+
return this.value.isNegative() && !this.value.isZero();
|
|
196
|
+
}
|
|
197
|
+
same(other) {
|
|
198
|
+
if (other.currency.code !== this.currency.code) {
|
|
199
|
+
throw new RangeError(`Currency mismatch: ${this.currency.code} vs ${other.currency.code}`);
|
|
200
|
+
}
|
|
201
|
+
return other;
|
|
202
|
+
}
|
|
203
|
+
round(value) {
|
|
204
|
+
return value.toDecimalPlaces(this.currency.minorUnits, Decimal.ROUND_HALF_UP);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=money.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.js","sourceRoot":"","sources":["../../../src/common/money.ts"],"names":[],"mappings":"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"}
|
package/dist/esm/common/phone.js
CHANGED
|
@@ -1,33 +1,85 @@
|
|
|
1
1
|
import libphonenumber from 'google-libphonenumber';
|
|
2
|
+
import { BadRequestError } from './errors.js';
|
|
2
3
|
const util = libphonenumber.PhoneNumberUtil.getInstance();
|
|
3
4
|
const { PhoneNumberFormat } = libphonenumber;
|
|
4
5
|
/**
|
|
5
|
-
* Normalises a phone number to E.164
|
|
6
|
-
*
|
|
6
|
+
* Normalises a phone number to E.164. The region is required (no app default): local formats such as
|
|
7
|
+
* `0712345678` only make sense against a country, and that country is app configuration.
|
|
8
|
+
*
|
|
9
|
+
* @param phone Number as typed (local or international format).
|
|
10
|
+
* @param region ISO 3166-1 alpha-2 region used for local formats (e.g. `GB`, `US`).
|
|
11
|
+
* @returns E.164 (`+447400123456`), or null when not a valid number for that region.
|
|
12
|
+
* @example toE164('07400 123456', 'GB') // '+447400123456'
|
|
7
13
|
*/
|
|
8
|
-
export function toE164(phone,
|
|
14
|
+
export function toE164(phone, region) {
|
|
9
15
|
try {
|
|
10
|
-
const parsed = util.parseAndKeepRawInput(phone.trim(),
|
|
16
|
+
const parsed = util.parseAndKeepRawInput(phone.trim(), region);
|
|
11
17
|
return util.isValidNumber(parsed) ? util.format(parsed, PhoneNumberFormat.E164) : null;
|
|
12
18
|
}
|
|
13
19
|
catch {
|
|
14
20
|
return null;
|
|
15
21
|
}
|
|
16
22
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
const MOBILE_TYPES = new Set([libphonenumber.PhoneNumberType.MOBILE, libphonenumber.PhoneNumberType.FIXED_LINE_OR_MOBILE]);
|
|
24
|
+
/**
|
|
25
|
+
* E.164 without the plus sign (an MSISDN), as SMS gateways and mobile-money rails expect. Throws instead of
|
|
26
|
+
* returning null so a bad number can never be stored or sent by mistake; use {@link toE164} to test input.
|
|
27
|
+
*
|
|
28
|
+
* @param phone Number as typed (local or international format).
|
|
29
|
+
* @param region ISO 3166-1 alpha-2 region used for local formats (required; no app default).
|
|
30
|
+
* @param options Mobile-only and same-region checks.
|
|
31
|
+
* @returns Digits only, country code first.
|
|
32
|
+
* @throws BadRequestError (`INVALID_PHONE`) when the number is invalid or fails an option.
|
|
33
|
+
* @example toMsisdn('07400 123456', 'GB') // '447400123456'
|
|
34
|
+
*/
|
|
35
|
+
export function toMsisdn(phone, region, options = {}) {
|
|
36
|
+
const invalid = () => new BadRequestError('Enter a valid phone number', { code: 'INVALID_PHONE' });
|
|
37
|
+
if (typeof phone !== 'string' || !region)
|
|
38
|
+
throw invalid();
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = util.parseAndKeepRawInput(phone.trim(), region);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw invalid();
|
|
45
|
+
}
|
|
46
|
+
if (!util.isValidNumber(parsed))
|
|
47
|
+
throw invalid();
|
|
48
|
+
if (options.regionOnly && util.getRegionCodeForNumber(parsed) !== region.toUpperCase())
|
|
49
|
+
throw invalid();
|
|
50
|
+
if (options.mobileOnly && !MOBILE_TYPES.has(util.getNumberType(parsed)))
|
|
51
|
+
throw invalid();
|
|
52
|
+
return util.format(parsed, PhoneNumberFormat.E164).slice(1);
|
|
21
53
|
}
|
|
22
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The region a number belongs to.
|
|
56
|
+
*
|
|
57
|
+
* @param phone Number as typed.
|
|
58
|
+
* @param region ISO region used to interpret local formats.
|
|
59
|
+
* @returns ISO region code, or null when it cannot be determined.
|
|
60
|
+
* @example phoneRegion('+447400123456', 'US') // 'GB'
|
|
61
|
+
*/
|
|
62
|
+
export function phoneRegion(phone, region) {
|
|
23
63
|
try {
|
|
24
|
-
return util.getRegionCodeForNumber(util.parse(phone,
|
|
64
|
+
return util.getRegionCodeForNumber(util.parse(phone, region)) ?? null;
|
|
25
65
|
}
|
|
26
66
|
catch {
|
|
27
67
|
return null;
|
|
28
68
|
}
|
|
29
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Masks a phone number for logs, audit metadata and UI: every digit except the last 4 becomes `*`, so a
|
|
72
|
+
* full number never lands in a log line or on a screen. Fewer than 5 digits are fully masked
|
|
73
|
+
* (the same rule the logger applies to phone-like values).
|
|
74
|
+
*
|
|
75
|
+
* @param phone Any phone string (normalised or not).
|
|
76
|
+
* @returns The masked digits.
|
|
77
|
+
* @example maskPhone('+44 7400 123456') // '********3456'
|
|
78
|
+
*/
|
|
30
79
|
export function maskPhone(phone) {
|
|
31
|
-
|
|
80
|
+
const digits = String(phone ?? '').replace(/\D/g, '');
|
|
81
|
+
if (digits.length < 5)
|
|
82
|
+
return '*'.repeat(digits.length || 4);
|
|
83
|
+
return `${'*'.repeat(digits.length - 4)}${digits.slice(-4)}`;
|
|
32
84
|
}
|
|
33
85
|
//# sourceMappingURL=phone.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"phone.js","sourceRoot":"","sources":["../../../src/common/phone.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"phone.js","sourceRoot":"","sources":["../../../src/common/phone.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,19 +1,75 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Standard success envelope: `{ success, message, data }`.
|
|
3
|
+
*
|
|
4
|
+
* @param data Payload placed under `data`.
|
|
5
|
+
* @param message Human-readable message (default `Success`).
|
|
6
|
+
* @returns The envelope, ready for `res.json`.
|
|
7
|
+
* @example res.json(successResponse(order, 'Order placed'));
|
|
8
|
+
*/
|
|
2
9
|
export function successResponse(data, message = 'Success') {
|
|
3
10
|
return { success: true, message, data };
|
|
4
11
|
}
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Standard error envelope: `{ success: false, message, code?, errors?, requestId? }`.
|
|
14
|
+
* Prefer throwing an AppError and letting the error middleware answer; use this only where a
|
|
15
|
+
* middleware must answer directly. For a body with a guaranteed `code`, use {@link errorBody}.
|
|
16
|
+
*
|
|
17
|
+
* @param message Message for the caller (never an internal error message).
|
|
18
|
+
* @param errors Optional field errors.
|
|
19
|
+
* @param code Optional UPPER_SNAKE_CASE code.
|
|
20
|
+
* @param requestId Optional correlation id of the request.
|
|
21
|
+
* @returns An envelope with `success: false`.
|
|
22
|
+
*/
|
|
23
|
+
export function errorResponse(message, errors, code, requestId) {
|
|
24
|
+
return { success: false, message, ...(code ? { code } : {}), ...(errors ? { errors } : {}), ...(requestId ? { requestId } : {}) };
|
|
8
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The error body with a required `code`, as the error middleware sends it.
|
|
28
|
+
*
|
|
29
|
+
* @param code UPPER_SNAKE_CASE machine-readable code.
|
|
30
|
+
* @param message Message for the caller (generic for 5xx).
|
|
31
|
+
* @param options `errors` (field errors) and `requestId` (correlation id), both omitted when absent.
|
|
32
|
+
* @returns `{ success: false, code, message, errors?, requestId? }`.
|
|
33
|
+
*
|
|
34
|
+
* @example res.status(404).json(errorBody('NOT_FOUND', 'Resource not found', { requestId }));
|
|
35
|
+
*/
|
|
36
|
+
export function errorBody(code, message, options = {}) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
code,
|
|
40
|
+
message,
|
|
41
|
+
...(options.errors ? { errors: options.errors } : {}),
|
|
42
|
+
...(options.requestId ? { requestId: options.requestId } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Offset pagination meta. `totalPages` is at least 1 so an empty list still reads as "page 1 of 1".
|
|
47
|
+
*
|
|
48
|
+
* @param total Total matching rows.
|
|
49
|
+
* @param page 1-based page that was returned.
|
|
50
|
+
* @param limit Page size.
|
|
51
|
+
* @returns `{ page, limit, total, totalPages, hasNext, hasPrev }`.
|
|
52
|
+
* @example buildPaginationMeta(45, 2, 20) // { page: 2, totalPages: 3, hasNext: true, hasPrev: true, … }
|
|
53
|
+
*/
|
|
9
54
|
export function buildPaginationMeta(total, page, limit) {
|
|
10
55
|
const totalPages = limit > 0 ? Math.max(1, Math.ceil(total / limit)) : 1;
|
|
11
56
|
return { page, limit, total, totalPages, hasNext: page < totalPages, hasPrev: page > 1 };
|
|
12
57
|
}
|
|
13
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Offset-paginated envelope: `{ success, message, data: T[], meta }`.
|
|
60
|
+
*
|
|
61
|
+
* @returns The envelope with meta from {@link buildPaginationMeta}.
|
|
62
|
+
* @example res.json(paginatedResponse(rows, total, page, limit));
|
|
63
|
+
*/
|
|
14
64
|
export function paginatedResponse(data, total, page, limit, message = 'Success') {
|
|
15
65
|
return { success: true, message, data, meta: buildPaginationMeta(total, page, limit) };
|
|
16
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Offset page as a service returns it (data + meta, no envelope).
|
|
69
|
+
*
|
|
70
|
+
* @returns `{ data, meta }`.
|
|
71
|
+
* @example return buildPaginatedResult(rows, total, page, limit);
|
|
72
|
+
*/
|
|
17
73
|
export function buildPaginatedResult(data, total, page, limit) {
|
|
18
74
|
return { data, meta: buildPaginationMeta(total, page, limit) };
|
|
19
75
|
}
|
|
@@ -21,4 +77,18 @@ export function buildPaginatedResult(data, total, page, limit) {
|
|
|
21
77
|
export function pageOffset(page, limit) {
|
|
22
78
|
return Math.max(0, (Math.max(1, page) - 1) * limit);
|
|
23
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Cursor pagination meta. `hasMore` is derived from `nextCursor`, so the two can never disagree.
|
|
82
|
+
*
|
|
83
|
+
* @param limit The page size that was applied.
|
|
84
|
+
* @param nextCursor Opaque cursor for the next page, or null on the last page.
|
|
85
|
+
* @example buildCursorPageMeta(20, rows.length > 20 ? encodeCursor(rows[19]) : null)
|
|
86
|
+
*/
|
|
87
|
+
export function buildCursorPageMeta(limit, nextCursor) {
|
|
88
|
+
return { limit, nextCursor, hasMore: nextCursor !== null };
|
|
89
|
+
}
|
|
90
|
+
/** Cursor-paginated envelope: `{ success, message, data: T[], meta: { limit, nextCursor, hasMore } }`. */
|
|
91
|
+
export function cursorPaginatedResponse(page, message = 'Success') {
|
|
92
|
+
return { success: true, message, data: page.data, meta: page.meta };
|
|
93
|
+
}
|
|
24
94
|
//# sourceMappingURL=response.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response.js","sourceRoot":"","sources":["../../../src/common/response.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"response.js","sourceRoot":"","sources":["../../../src/common/response.ts"],"names":[],"mappings":"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"}
|
package/dist/esm/common/time.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** True for IANA zones the runtime knows (`
|
|
1
|
+
/** True for IANA zones the runtime knows (`Europe/Berlin`), false for garbage. */
|
|
2
2
|
export function isValidTimeZone(zone) {
|
|
3
3
|
try {
|
|
4
4
|
new Intl.DateTimeFormat('en-US', { timeZone: zone });
|
|
@@ -68,8 +68,19 @@ export function zonedWallTimeToInstant(year, month, day, minutesOfDay, zone) {
|
|
|
68
68
|
const estimate = wall - timeZoneOffsetMinutes(zone, new Date(wall)) * MS_PER_MINUTE;
|
|
69
69
|
return new Date(wall - timeZoneOffsetMinutes(zone, new Date(estimate)) * MS_PER_MINUTE);
|
|
70
70
|
}
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* The local calendar day containing `at` in `zone`, as a half-open `[start, end)` UTC range (23–25 h on
|
|
73
|
+
* DST days). Use it for "today" in reports, business days and daily limits.
|
|
74
|
+
*
|
|
75
|
+
* @param at Any instant in the day.
|
|
76
|
+
* @param zone IANA zone (required; no app default).
|
|
77
|
+
* @returns `{ start, end }` with `start` inclusive and `end` exclusive.
|
|
78
|
+
* @throws RangeError for an unknown zone.
|
|
79
|
+
* @example zonedDayBounds(new Date('2026-10-11T22:30:00Z'), 'Asia/Riyadh') // 2026-10-11T21:00Z → 2026-10-12T21:00Z
|
|
80
|
+
*/
|
|
72
81
|
export function zonedDayBounds(at, zone) {
|
|
82
|
+
if (!isValidTimeZone(zone))
|
|
83
|
+
throw new RangeError(`Unknown time zone "${zone}"`);
|
|
73
84
|
const { year, month, day } = localDate(at, zone);
|
|
74
85
|
return { start: zonedWallTimeToInstant(year, month, day, 0, zone), end: zonedWallTimeToInstant(year, month, day + 1, 0, zone) };
|
|
75
86
|
}
|
|
@@ -93,4 +104,18 @@ export function quietWindowEndAt(at, zone, start, end) {
|
|
|
93
104
|
const endsTomorrow = crossesMidnight && msOfDay >= startMs;
|
|
94
105
|
return zonedWallTimeToInstant(year, month, day + (endsTomorrow ? 1 : 0), endMin, zone);
|
|
95
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* The instant as a UTC ISO-8601 string (`2026-11-13T09:00:00.000Z`), the form every timestamp travels in.
|
|
109
|
+
*
|
|
110
|
+
* @param instant A Date, an epoch-milliseconds number or a string `Date` can parse.
|
|
111
|
+
* @returns The `toISOString()` form.
|
|
112
|
+
* @throws RangeError when the input is not a valid instant (never returns `"Invalid Date"`).
|
|
113
|
+
* @example toUtcIso('2026-11-13T12:00:00+03:00') // '2026-11-13T09:00:00.000Z'
|
|
114
|
+
*/
|
|
115
|
+
export function toUtcIso(instant) {
|
|
116
|
+
const date = instant instanceof Date ? instant : new Date(instant);
|
|
117
|
+
if (Number.isNaN(date.getTime()))
|
|
118
|
+
throw new RangeError(`Not a valid instant: "${String(instant)}"`);
|
|
119
|
+
return date.toISOString();
|
|
120
|
+
}
|
|
96
121
|
//# sourceMappingURL=time.js.map
|