@birtalanrobert/commerce 1.0.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +601 -0
  2. package/LICENSE +661 -0
  3. package/NOTICE +45 -0
  4. package/README.md +79 -0
  5. package/dist/deposits.d.ts +52 -0
  6. package/dist/deposits.d.ts.map +1 -0
  7. package/dist/deposits.js +71 -0
  8. package/dist/deposits.js.map +1 -0
  9. package/dist/index.d.ts +23 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +30 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/migrations/1789800000000-CreateCommerce.d.ts +38 -0
  14. package/dist/migrations/1789800000000-CreateCommerce.d.ts.map +1 -0
  15. package/dist/migrations/1789800000000-CreateCommerce.js +152 -0
  16. package/dist/migrations/1789800000000-CreateCommerce.js.map +1 -0
  17. package/dist/nestjs/commerce.service.d.ts +122 -0
  18. package/dist/nestjs/commerce.service.d.ts.map +1 -0
  19. package/dist/nestjs/commerce.service.js +337 -0
  20. package/dist/nestjs/commerce.service.js.map +1 -0
  21. package/dist/nestjs/index.d.ts +18 -0
  22. package/dist/nestjs/index.d.ts.map +1 -0
  23. package/dist/nestjs/index.js +27 -0
  24. package/dist/nestjs/index.js.map +1 -0
  25. package/dist/nestjs/payment.entity.d.ts +94 -0
  26. package/dist/nestjs/payment.entity.d.ts.map +1 -0
  27. package/dist/nestjs/payment.entity.js +181 -0
  28. package/dist/nestjs/payment.entity.js.map +1 -0
  29. package/dist/nestjs/payout-account.entity.d.ts +41 -0
  30. package/dist/nestjs/payout-account.entity.d.ts.map +1 -0
  31. package/dist/nestjs/payout-account.entity.js +83 -0
  32. package/dist/nestjs/payout-account.entity.js.map +1 -0
  33. package/dist/providers/port.d.ts +97 -0
  34. package/dist/providers/port.d.ts.map +1 -0
  35. package/dist/providers/port.js +16 -0
  36. package/dist/providers/port.js.map +1 -0
  37. package/dist/providers/stripe.d.ts +39 -0
  38. package/dist/providers/stripe.d.ts.map +1 -0
  39. package/dist/providers/stripe.js +221 -0
  40. package/dist/providers/stripe.js.map +1 -0
  41. package/nestjs/package.json +5 -0
  42. package/package.json +49 -0
  43. package/src/deposits.ts +96 -0
  44. package/src/index.ts +40 -0
  45. package/src/migrations/1789800000000-CreateCommerce.ts +156 -0
  46. package/src/nestjs/commerce.service.ts +476 -0
  47. package/src/nestjs/index.ts +24 -0
  48. package/src/nestjs/payment.entity.ts +150 -0
  49. package/src/nestjs/payout-account.entity.ts +56 -0
  50. package/src/providers/port.ts +108 -0
  51. package/src/providers/stripe.ts +274 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,601 @@
1
+ # Changelog
2
+
3
+ Each package carries its own version. A release publishes only the packages
4
+ whose version is not yet on the registry; `pnpm release` asks npm and skips the
5
+ rest.
6
+
7
+ ## commerce 1.0.0
8
+
9
+ ### Added
10
+
11
+ - Taking money on a business's behalf, for projects 01, 02 and 11: payout
12
+ onboarding with a hard gate, card payments through Stripe Connect as
13
+ destination charges, holds that are captured only by a human decision,
14
+ manually recorded cash, terminal, voucher and transfer takings, partial
15
+ refunds with a required reason, and webhook verification.
16
+ - `depositFor` and `canTakeMoney` at the pure root entry point, because a
17
+ console shows both while somebody drags a slider.
18
+ - **We never hold anybody's funds** — the customer pays the business directly
19
+ and our cut is an application fee. Everything in the package follows from it.
20
+
21
+ ## phone 1.0.0
22
+
23
+ ### Added
24
+
25
+ - Telephone numbers as the durable identity of a customer: `normalisePhone`,
26
+ `formatPhone`, `dialable`, `isSearchablePhone`, for Romania and Hungary.
27
+ Extracted from project 12 when project 02 needed the same matching, which is
28
+ the second consumer the policy asks for.
29
+ - **Three forms, and they are not interchangeable**: as typed, normalised (a
30
+ search key) and dialable (E.164). Project 12 shipped a pumping check that
31
+ refused a perfectly good normalised number for having no plus, which is what
32
+ the distinction exists to prevent.
33
+ - No dependencies and nothing framework-shaped, so a browser bundle can decide
34
+ whether a lookup is worth making before the keystroke lands.
35
+
36
+ ## comms 1.3.0
37
+
38
+ ### Added
39
+
40
+ - `SmtpMessagePort`: email over SMTP, built on nodemailer. Every project's
41
+ Compose file runs Mailpit and nothing could reach it, so an invitation, a
42
+ receipt or a password reset could not be followed end to end on a developer's
43
+ machine without a vendor account. It is a production transport too — a
44
+ customer's own mail server, a relay offered instead of an API, a deployment
45
+ where mail may not leave the building.
46
+ - A recipient the server refuses after accepting the conversation is a failure
47
+ rather than a success. `sendMail` resolves in that case, and recording it as
48
+ sent writes "delivered" against a message the server explicitly refused.
49
+ - Certificates are verified by default. `allowSelfSignedCertificate` is for a
50
+ local catcher or a private relay, and named so nobody enables it casually.
51
+ - Its tests run a real SMTP server in-process rather than mocking the client:
52
+ neither the refused recipient nor the untrusted certificate can be asserted
53
+ against a mock.
54
+
55
+ ## messaging 1.0.1
56
+
57
+ ### Fixed
58
+
59
+ - **The entity pointed at the wrong table.** `MessageCreditEntry` was mapped to
60
+ `message_credits` while the migration creates `mortar_message_credits`, so
61
+ every read through the service failed with `relation "public.message_credits"
62
+ does not exist`. The migration and the raw SQL in the README were right; only
63
+ the decorator was wrong, which is why the package's own build and typecheck
64
+ had nothing to say about it.
65
+
66
+ ## messaging 1.0.0
67
+
68
+ Extracted from project 13 at its second consumer (project 12), which is the
69
+ rule: written once, moved when a second product needs it.
70
+
71
+ ### Added
72
+
73
+ - **`countSegments`** — what a message actually costs, counted the way a
74
+ provider counts rather than the way a person counts characters. A single
75
+ character outside GSM 03.38 changes the encoding for the whole message and
76
+ cuts capacity from 160 to 70, so `offenders` names the characters responsible:
77
+ "your ș and ț are doubling the cost" is something a person can act on.
78
+ - **Quiet hours** — `isQuiet`, `nextAllowed`, `localTime`, in the _business's_
79
+ zone rather than the recipient's. A phone number says nothing about where
80
+ somebody is sitting.
81
+ - **`assessSmsRisk`** — pumping detection. Fraud that costs money rather than
82
+ data, and visible only in the shape of recent traffic rather than in any one
83
+ message.
84
+ - **`MessageCreditsService`** and `mortar_message_credits` (`/nestjs`) — credit
85
+ as a ledger, append-only, with the balance summed from the entries rather than
86
+ kept in a column that can disagree with them. No foreign key to whatever the
87
+ segments were spent on, which is what lets two products share it.
88
+
89
+ The root entry point is pure — no database, no framework, no Node built-ins —
90
+ because a console counts segments on every keystroke and that has to run in a
91
+ browser. Everything needing TypeORM is behind `/nestjs`.
92
+
93
+ ## csv 1.0.0
94
+
95
+ Extracted at the second consumer: project 12 reads a shop's shelf out of a
96
+ spreadsheet, project 13 writes an access log a regulator will open. Both had
97
+ hand-written code, and both had a bug the other did not.
98
+
99
+ ### Added
100
+
101
+ - **`parseCsv`** — delimiter detected from the file. Every locale that uses a
102
+ comma as the decimal separator gets semicolon-separated files out of Excel,
103
+ still called CSV, and the obvious shortcut of honouring both at once splits a
104
+ field reading `screen cracked; battery dead` in two and shifts every column
105
+ after it, silently. Blank lines dropped; the mark Excel writes stripped, since
106
+ left in place it hides in the first header.
107
+ - **`toCsv` and `toCsvFrom`** — quoting that is not optional, empty cells rather
108
+ than the strings `null` and `undefined`, and a byte-order mark by default,
109
+ because Excel guesses a file's encoding by looking at it and reads an unmarked
110
+ UTF-8 file as the system code page — so `Ioană` opens as `Ioană` for exactly
111
+ the people whose names have diacritics. `toCsvFrom` takes the column order
112
+ rather than reading it off the first object's keys.
113
+
114
+ Pure: no database, no framework, no Node built-ins, so a console can preview an
115
+ upload before it happens. Deliberately not part of `files`, which carries S3,
116
+ virus scanning and PDF assembly.
117
+
118
+ ## jobs 1.1.1
119
+
120
+ ### Fixed
121
+
122
+ - **`JobsModule` now closes the Redis connection it opened.** It creates a
123
+ dedicated connection and hands it to BullMQ, and BullMQ closes connections it
124
+ created while leaving alone the ones it was given — correctly, since it does
125
+ not own them. Nothing closed this one. An application that finished its work
126
+ and called `app.close()` sat there with an open socket for ever: a seed script
127
+ that never returned, and a deployment step that hung waiting for it. The
128
+ connection is now provided under `MORTAR_QUEUE_CONNECTION` and closed in
129
+ `onApplicationShutdown`, after the workers and queues that use it.
130
+
131
+ ## redis 1.0.1
132
+
133
+ ### Fixed
134
+
135
+ - **A queue connection no longer carries a command timeout.** `createQueueConnection`
136
+ already cleared `maxRetriesPerRequest` for BullMQ, but left the five-second
137
+ `commandTimeout` in place — and a queue consumer waits for work with blocking
138
+ reads that are _designed_ to sit there for longer than any sensible deadline.
139
+ The result was an idle worker logging `Command timed out` every few seconds,
140
+ on every queue, for ever. Jobs still ran, which is what made it easy to read
141
+ as a sick Redis rather than a misconfigured client. `commandTimeoutMs` now
142
+ accepts `null` to mean "no deadline", and queue connections pass it.
143
+
144
+ ## comms 1.2.0
145
+
146
+ The vendors, behind the ports that were waiting for them (dossier D-10).
147
+
148
+ ### Added
149
+
150
+ - **`ResendMessagePort`** — email, on Resend's own SDK. A message may carry its
151
+ own `from` and `replyTo`, which is how it is branded as a customer without
152
+ their domain being one the provider can sign for: their name in the display
153
+ part, their address to reply to, so a client who replies reaches their
154
+ accountant rather than a mailbox nobody reads.
155
+ - **`TwilioMessagePort`** — SMS, on Twilio's SDK, preferring a messaging service
156
+ over a single number. The sender identity is a per-market question — an
157
+ alphanumeric sender ID is permitted in some countries, requires registration
158
+ in others, and cannot be replied to anywhere — and a messaging service is what
159
+ lets it change without a deployment. It refuses to be constructed with no
160
+ sender at all, because the alternative is finding out twelve days into a
161
+ reminder cadence.
162
+ - **The segment count comes back from the provider**, not from our estimate.
163
+ `countSegments` decides whether a message is worth sending; the ledger is
164
+ debited by what was actually charged, and the two differing is the case a
165
+ ledger exists to catch — one accented character downgrades a message to UCS-2
166
+ and doubles its cost without changing a word.
167
+ - **`ResendInbound`** — verifying the provider's webhook and fetching the
168
+ message it names. The webhook carries metadata and no body, so the original is
169
+ fetched and returned as **raw MIME** for `parseMime` to read: the parser stays
170
+ ours, and the day the provider changes nothing above it moves. Verification is
171
+ the vendor's own (Standard Webhooks) and takes the **raw** request body — a
172
+ parsed object re-serialised has different bytes and fails.
173
+
174
+ ### Notes
175
+
176
+ - **The vendors' SDKs rather than their REST APIs**, which is the arrangement
177
+ `files` already has with `@aws-sdk/client-s3`. Both were first written against
178
+ the published REST documentation, and the SDK types caught a field this got
179
+ wrong — a received message's download URL. Fewer lines, and the shapes are
180
+ right by construction.
181
+ - Both ports **throw** on refusal rather than returning a failure, carrying the
182
+ provider's own sentence. `CommsService` records it in the message log, which
183
+ is what support reads — a port that swallowed the reason would leave "it did
184
+ not send" and nothing else.
185
+ - `ResendMessagePort` imposes its own **timeout**: the SDK sets none, and
186
+ something is usually waiting on a message — a professional who has just
187
+ pressed send should not hold a response open until a socket gives up.
188
+ - Every port takes an optional `client`, so a deployment can share one and a
189
+ test can fake the vendor at its own surface rather than stubbing `fetch`.
190
+
191
+ ## context 1.1.0
192
+
193
+ An actor can be an operator.
194
+
195
+ ### Added
196
+
197
+ - **`Actor.type` accepts `'operator'`** — one of _us_, working inside a
198
+ customer's account with their consent. Separate from `user` because the audit
199
+ trail has to be able to say which it was: support access recorded as the
200
+ customer's own action is worse than no record, being a confident answer to
201
+ "who opened this?" that names the wrong person. Thirteen of the seventeen
202
+ specifications describe back-office impersonation, so the type belongs here
203
+ rather than in each of them.
204
+ - `impersonatedBy` is now documented as the _other_ shape — an operator acting
205
+ as a named user — with a note that acting as oneself inside the customer's
206
+ account is the safer one, because nothing is disguised.
207
+
208
+ ## comms 1.1.0
209
+
210
+ Attachments, so a completed set of documents can be delivered by email (dossier
211
+ F-174).
212
+
213
+ ### Added
214
+
215
+ - **`OutboundMessage.attachments`**, and `MAX_ATTACHMENT_BYTES` at 10 MB.
216
+ Providers differ — many refuse at 10, most at 25 — and base64 inflates an
217
+ attachment by a third, so the useful limit sits well under the smallest of
218
+ them.
219
+ - **Refused before the provider sees it.** A receiving server bounces an
220
+ oversized attachment silently and late, which becomes "they never got it and
221
+ nobody knows why". The log records a failure with a sentence instead, and
222
+ nothing is handed to the port.
223
+ - The message log records **how many files and how many bytes**, never their
224
+ names: the log is read by support, and a client's filenames are not theirs to
225
+ read.
226
+
227
+ ### Fixed
228
+
229
+ - **`NoopMessagePort` ids are now unique across processes.** They counted from
230
+ one, and the message log has a unique index on
231
+ `(direction, provider_message_id)` — so the second test run against the same
232
+ database collided, and `CommsService` reported it as a message the provider
233
+ refused. The failure surfaced in whatever was being tested rather than in the
234
+ double, and only on the second run.
235
+
236
+ ## files 1.2.0
237
+
238
+ ZIP archives and provider-enforced retention (dossier F-170, F-178): a completed
239
+ request leaves as one file whose folders and names the receiving firm can file
240
+ without opening it. A ZIP of `IMG_4471.jpg` is worthless; one of
241
+ `Ion_Popescu/03_Bank_statement.pdf` is already filed.
242
+
243
+ ### Added
244
+
245
+ - **`createZip`.** Hand-written over `node:zlib` rather than taken from a
246
+ dependency — the essential format is two hundred lines and has not changed
247
+ since 1993, and every library that writes it brings a stream stack and a
248
+ supply chain with it.
249
+ - Deterministic when given a `modified` date, so a delivery retry produces the
250
+ file the destination already has rather than a second copy.
251
+ - Zip-slip paths (`/etc/passwd`, `../../secrets`) are stripped rather than
252
+ trusted to the extractor; duplicate paths are refused rather than left for the
253
+ extractor to resolve; names are flagged UTF-8 so a Romanian filename survives.
254
+ - Entries are deflated, and stored instead when deflate would make them bigger —
255
+ which is every photograph and most PDFs.
256
+ - Verified against `unzip` in the tests, not only against its own reader: an
257
+ archive only this package can read is not an archive.
258
+ - **`S3Storage.applyLifecycle` / `describeLifecycle`.** Provider-enforced expiry
259
+ as a backstop under the application's own retention. The failure it covers is
260
+ the one the application cannot: a sweep broken for a month leaves documents in
261
+ a bucket and nothing in the application says so. An empty rule list removes
262
+ the configuration, because S3 refuses one with zero rules.
263
+ - **`S3Storage` now has integration tests**, against MinIO rather than a mocked
264
+ SDK — whether a presigned URL is actually accepted, what a missing object
265
+ answers, and whether a lifecycle configuration is written in a shape a
266
+ provider takes are all things a mock cannot speak to. Mortar's development
267
+ stack gained a MinIO service on 3052/3053 for it.
268
+ - **`MemoryStorage` gained `has`, `clear`, `failOn` and `stopFailing`.** A suite
269
+ shares one instance across a file, so without `clear` every object from every
270
+ earlier test is still there and an assertion about what a cleanup removed
271
+ silently starts passing for the wrong reason. `failOn` exists because real
272
+ buckets fail one object at a time, and what matters is what the caller does
273
+ about it: a retention sweep must not abandon thirty-nine other firms because
274
+ one object would not delete.
275
+
276
+ ## files 1.1.0
277
+
278
+ Single-PDF assembly (dossier F-090): several photographed pages become one
279
+ document, which is what a professional actually wants — three separate JPEGs of
280
+ a statement means three files to open in an order only knowable from filenames
281
+ the client did not choose.
282
+
283
+ ### Added
284
+
285
+ - **`assemblePdf`.** JPEG and PNG are embedded natively, `DCTDecode` and
286
+ `FlateDecode`, so a photograph reaches the professional as the bytes the
287
+ camera produced rather than a generational copy. Pages are sized to their
288
+ image rather than floated on a fixed A4, scaled down but never up.
289
+ - HEIC is refused. A phone produces it, no PDF reader opens it, and converting
290
+ it needs a decoder this package is not going to carry.
291
+ - No producer or creation date is written: these are a client's bank statements,
292
+ and the defaults name the software that touched them. It also makes the output
293
+ deterministic, which a test asserts.
294
+
295
+ ### A dependency, and why this one
296
+
297
+ `pdf-lib` is a real dependency in a package that has argued against them —
298
+ `@birtalanrobert/comms` writes its own MIME parser, and the ClamAV adapter
299
+ speaks the protocol directly. The distinction is where a failure shows up. A
300
+ MIME parser that gets something wrong loses an attachment, visibly, immediately.
301
+ **A malformed PDF is invisible until a professional cannot open it**, days
302
+ later, with a client who has already put the paper away — and PDF is a format
303
+ with enough subtlety that hand-rolling a writer is a wager on being right about
304
+ all of it.
305
+
306
+ ### A bug found while writing the tests
307
+
308
+ `pdf-lib` reads an image's **whole backing `ArrayBuffer` and ignores the view's
309
+ `byteOffset`**. Node allocates every Buffer under 4 KB from a shared 8 KB pool,
310
+ so a small page — a compressed scan, or anything fetched from storage — arrives
311
+ at a non-zero offset, and the embedder parses whatever sits at the pool's start.
312
+
313
+ It is a nasty shape of bug: whether it fires depends on what else the process
314
+ has allocated, so the first several runs passed by reading a stale copy of the
315
+ same image left at position 0. An offset-aware view does not fix it, because it
316
+ shares the ArrayBuffer. `assemblePdf` copies the bytes, and a test builds a
317
+ pooled buffer deliberately.
318
+
319
+ ## http 2.0.0 — and a minor for everything that depends on it
320
+
321
+ `@birtalanrobert/http` root entry point is now framework-free.
322
+
323
+ ### Why a major
324
+
325
+ The root exported the exception filter, the context middleware, the validation
326
+ pipe, the health controller, `HttpModule` and `@PublicRoute()` — so importing
327
+ `NotFoundError` imported NestJS. Every package that raises a mortar error
328
+ inherited that, which is a framework in an edge bundle for the sake of a type
329
+ guard.
330
+
331
+ Those six now live at `@birtalanrobert/http/nestjs`. **The error classes,
332
+ problem serialisation, header names, locale negotiation and the health registry
333
+ have not moved**, so most files need no change; an application module and a
334
+ bootstrap file need one line each.
335
+
336
+ ### Also changed
337
+
338
+ - **`toProblemDetails` recognises a Nest `HttpException` by shape rather than
339
+ by `instanceof`.** That removes the last runtime import, and it is the more
340
+ correct check: two copies of `@nestjs/common` in one install — routine in a
341
+ monorepo — make `instanceof` false for the framework's own exceptions, so its
342
+ validation errors would silently fall through to the generic 500 branch. The
343
+ function is documented as total; recognising the contract is what makes that
344
+ true.
345
+ - **`REQUEST_ID_HEADER`, `CORRELATION_ID_HEADER` and `negotiateLocale` moved to
346
+ their own module** so a Next.js middleware can read the same header names
347
+ without the middleware class that uses them.
348
+
349
+ ### auth 1.1.0, idempotency 1.1.0, tenancy 1.1.0, workflow 1.1.0
350
+
351
+ No API change. Each depends on `http`, and each is republished so its dependency
352
+ range moves to `^2.0.0` — otherwise an application installing `http@2` would end
353
+ up with a second copy at `1.x` underneath these, and `isMortarError` is an
354
+ `instanceof` check that two copies quietly break.
355
+
356
+ `workflow` also gains the `mortar.entries` field it was missing, so its
357
+ `nestjs/` subpath stub is regenerated by the build instead of surviving only
358
+ because nothing had deleted it.
359
+
360
+ ### Every package README now documents its wiring
361
+
362
+ What to import, whether it is `forRoot` or `forRootAsync`, where it goes in the
363
+ imports array and what breaks if it goes elsewhere, which entities and
364
+ migrations to register, and what needs no module at all — `context`, `money` and
365
+ the root half of `http` are imported directly.
366
+
367
+ Two scripts check the result rather than trusting it: one resolves every
368
+ documented import against the built `.d.ts` files, the other checks every
369
+ `Module.forRoot…()` shown actually exists. Both found real errors — a
370
+ `RedisService.remember` that does not exist (it is `redis.cache.getOrSet`), a
371
+ `workers.handle` that is `workers.register`, an `envBool` that is `envBoolean`,
372
+ and column helpers documented in the wrong package.
373
+
374
+ ## files 1.0.0, comms 1.0.0
375
+
376
+ The two Tier 2 packages dossier's Phase 2 needs: somewhere for an uploaded
377
+ document to go, and a way for a client to forward one they already have.
378
+
379
+ Built now rather than up front because this is the phase that first needs them —
380
+ and built partially, on purpose. `files` has no PDF assembly, thumbnailing or
381
+ ZIP packaging; `comms` has no templates, quiet hours or credit ledger. Those
382
+ belong to the phases that need them, and writing them now would be guessing at
383
+ requirements three projects away.
384
+
385
+ ### `files`
386
+
387
+ - **Pre-signed direct upload.** The browser uploads to storage without touching
388
+ the API. Proxying the bytes costs a request-sized chunk of memory per
389
+ concurrent upload and puts the API's timeout between a client on a train and
390
+ finishing. The cost is real rows in `pending`, which `sweepAbandoned` clears.
391
+ - **The type is read from the bytes, never from the header.** A `Content-Type`
392
+ and a filename extension are claims made by whoever uploaded the file.
393
+ - **One bucket, tenant id as the first path segment**, so a bucket policy can
394
+ name it. `assertTenantOwns` before every read, delete and signature: nothing
395
+ governs a bucket except the key handed to it.
396
+ - **Envelope encryption for erasure, not confidentiality.** The provider already
397
+ encrypts at rest. Destroying one wrapped key is the difference between an
398
+ erasure request honoured in seconds and one that cannot honestly be honoured,
399
+ because backups exist. The object key is bound in as AAD, so a ciphertext
400
+ moved under another tenant's prefix fails to open.
401
+ - **`RefusingScanner` is the default.** A misconfiguration that silently
402
+ disables virus scanning is indistinguishable from working software until it
403
+ matters; one that refuses uploads is noticed in minutes.
404
+ - **`MemoryStorage` is exported.** Every service consuming `StoragePort` lives
405
+ in another repository and needs to test its upload flow without a bucket.
406
+
407
+ ### `comms`
408
+
409
+ - **Signed per-request inbound addresses.** The address is the credential, so it
410
+ carries an HMAC tag; without one a predictable local part lets a stranger post
411
+ documents into a firm's workflow. Its own secret, because an address lives for
412
+ years in sent folders while a link expires in days.
413
+ - **A MIME parser rather than a dependency.** Inbound mail is the most hostile
414
+ input the system accepts. Eighty readable lines tested against what actually
415
+ arrives is a smaller permanent surface than a parser that knows every corner
416
+ of MIME in order to be asked about six.
417
+ - **A partial unique index on the provider's message id.** Providers redeliver;
418
+ without it a forwarded bank statement is attached three times. A constraint
419
+ rather than a check, because two redeliveries can arrive at once.
420
+ - **The message body is never logged.** A reminder is innocuous; inbound mail
421
+ here is bank statements.
422
+ - **Ports only for sending.** Providers are Phase 5; the seam exists now so the
423
+ one thing that needs sending sooner has somewhere to go.
424
+
425
+ ### A defect in the scaffolding, found by the editor
426
+
427
+ `scripts/new-package.mjs` generated a single `tsconfig.json` that both emitted
428
+ to `dist` and excluded `*.test.ts` — so a new package's tests belonged to no
429
+ project and were type-checked by nothing. The build passed while the editor
430
+ showed errors, which is how three genuine type errors in `envelope.test.ts`
431
+ survived a green run.
432
+
433
+ `files`, `comms` and `workflow` now carry the standard pair the other twelve
434
+ packages already had, and the scaffold writes both. Nothing published changes:
435
+ `dist` never contained tests either way.
436
+
437
+ ### A bug this found
438
+
439
+ The inbound tag was base64url at first, and every address failed to verify
440
+ itself. `parse` lowercases the address on the way in — correctly, because
441
+ providers lowercase local parts — which destroys a case-sensitive tag. Hex
442
+ costs a few characters in an address nobody types by hand.
443
+
444
+ ## observability 1.0.1
445
+
446
+ Never published. An interrupted publish left `1.0.0` partially staged and npm
447
+ rejected a retry, so the version was stepped over — and then the staged upload
448
+ finalised on npm's side after all. `1.0.0` is the real release; `1.0.1` does
449
+ not exist.
450
+
451
+ ## observability 1.1.0, jobs 1.1.0
452
+
453
+ Everything a worker needs to be observable. Found by building `starter-worker`,
454
+ whose specification asks for queue depth, job duration, failure rate and
455
+ scanner lag — none of which anything recorded.
456
+
457
+ ### Added
458
+
459
+ - **`JobWorkers` records `job_duration_ms`, `jobs_total` (labelled by outcome)
460
+ and `jobs_dead_lettered_total`.** In the runner rather than in each handler:
461
+ how many ran, how many failed and how long they took are properties of the
462
+ runner and identical in every service. One counter with a `status` label
463
+ rather than two counters, because failure rate is a ratio and both halves
464
+ must share their labels. Defaults to a no-op registry.
465
+
466
+ - **`WindowScanner` records `scanner_scan_duration_ms`, `scanner_items_total`
467
+ and `scanner_last_success_timestamp_ms`.** The last is the one worth alerting
468
+ on: a scanner that has stopped logs nothing and errors nothing, it simply
469
+ stops finding work, and the first anyone hears is a customer asking why they
470
+ were never reminded. A timestamp rather than an age, because a gauge written
471
+ only on success cannot grow while the scanner is dead.
472
+
473
+ - **`JobQueues` rejects a job id containing `:`**, naming the job and the id.
474
+ BullMQ uses the colon as a key separator and refuses such an id with an error
475
+ that mentions neither — and `` `reminder:${id}` `` is the natural thing to
476
+ write, so that error is reached often and explains nothing.
477
+
478
+ - **`JobsModule` passes the container's metrics registry** to the worker
479
+ registry, so this costs a consumer nothing to switch on.
480
+
481
+ - **`InMemoryMetrics.snapshot()`**, returning every series held. A `/metrics`
482
+ endpoint has to enumerate what exists, and `value()` could only answer about
483
+ a name the caller already knew. Histograms report count, sum, min and max;
484
+ bucketing is a presentation decision belonging to whatever scrapes it.
485
+
486
+ ### Fixed
487
+
488
+ - **Histogram labels are stored beside their observations** rather than
489
+ recovered by parsing the storage key. A label value containing `=` or `,`
490
+ would not have survived the round trip.
491
+
492
+ ## 1.0.0
493
+
494
+ The version numbers become meaningful.
495
+
496
+ Until now every package shared one version and all twelve were republished
497
+ together. That does not survive contact with per-package releases while the
498
+ major is `0`: under semver a `^0.2.0` range excludes `0.3.0`, so changing one
499
+ package and releasing only it leaves every dependent pinned to the old copy —
500
+ and npm resolves that by installing both. Two copies of `observability` means
501
+ two distinct `MORTAR_LOGGER` symbols, and dependency injection stops working
502
+ with an error that names neither.
503
+
504
+ At `1.x` a caret range accepts later minors, so a package can be released on
505
+ its own and its dependents pick it up on their next install. From here:
506
+
507
+ - **patch** — a fix that changes no signature
508
+ - **minor** — anything added
509
+ - **major** — anything removed or changed in shape
510
+
511
+ ### Added
512
+
513
+ - **`DatabaseModule` can run migrations at boot** — `migrationsRun: true`.
514
+
515
+ Guarded by a Postgres advisory lock, so several replicas starting at once are
516
+ safe: one applies while the others wait, then find nothing pending. TypeORM
517
+ takes no lock of its own, and without one the second replica to reach a
518
+ `CREATE TABLE` fails and that container crash-loops. Also exported directly
519
+ as `runMigrationsWithLock` for release-step scripts.
520
+
521
+ - **`LoggerModule` provides `NestLoggerAdapter` and `LoggingInterceptor`.**
522
+ Both were exported but never registered, so `app.get(NestLoggerAdapter)` and
523
+ `{ provide: APP_INTERCEPTOR, useExisting: LoggingInterceptor }` — the two
524
+ documented ways to use them — both failed. Constructing them by hand still
525
+ works.
526
+
527
+ - **`PUBLIC_ROUTE_KEY` and `PublicRoute()` in `@birtalanrobert/http`**, and the
528
+ health controller now carries them. `@birtalanrobert/auth` re-exports the key
529
+ as `PUBLIC_KEY`, unchanged, so `PermissionsGuard` and `@Public()` behave
530
+ exactly as before — but a globally registered guard no longer 401s the
531
+ readiness probe, which previously left pods that never joined the load
532
+ balancer.
533
+
534
+ - **`auditEntities` and `idempotencyEntities`**, so every package that ships
535
+ entities exports them as an array the same way it exports its migrations.
536
+
537
+ ### Fixed
538
+
539
+ - **A circular import between `logger.module.ts` and the two classes it now
540
+ provides** left `MORTAR_LOGGER` `undefined` at decorator evaluation time, so
541
+ `@Inject(MORTAR_LOGGER)` silently degraded to reflected-type injection and
542
+ Nest reported that it could not resolve `Function`. The tokens moved to a
543
+ leaf module. Under CommonJS this class of bug fails at wiring time, never at
544
+ build time.
545
+
546
+ ### Testing
547
+
548
+ `@nestjs/testing` and `unplugin-swc` are now dev dependencies, and the Nest
549
+ modules are exercised by building a real container rather than by inspecting
550
+ the `DynamicModule` object. Every defect above was invisible to a test that
551
+ asserts on `module.providers` and obvious to one that calls `moduleRef.get()`.
552
+
553
+ ## 0.2.0
554
+
555
+ Composing the packages into a real application surfaced three problems that
556
+ package-level tests could not.
557
+
558
+ ### Added
559
+
560
+ - **`forRootAsync` on every configurable module** — `LoggerModule`,
561
+ `DatabaseModule`, `RedisModule`, `HttpModule`, `TenancyModule`, `AuthModule`,
562
+ `IdempotencyModule` and `JobsModule`.
563
+
564
+ Previously each module took its options synchronously, which meant a consumer
565
+ had to read `process.env` at import time — before anything had validated it —
566
+ to configure a database URL or a Redis connection. That defeats having a
567
+ configuration layer at all. Options can now come from any provider, including
568
+ the validated config.
569
+
570
+ - **`ConfigModule.token()`**, so a wiring site can write
571
+ `inject: [ConfigModule.token()]` rather than importing the raw symbol.
572
+
573
+ - **`AsyncModuleOptions<T>`** in `@birtalanrobert/context`: the shared shape for
574
+ the above.
575
+
576
+ ### Fixed
577
+
578
+ - **`HttpModule` and `TenancyModule` no longer hold module options in static
579
+ fields.** Both middlewares now receive their options through dependency
580
+ injection. The previous arrangement meant a second `forRoot()` call silently
581
+ overwrote the first — which is exactly what happens when a test suite builds
582
+ more than one application in a process.
583
+
584
+ - **`@birtalanrobert/http` accepts `class-validator` 0.15**, which is current.
585
+ The peer range previously stopped at 0.14 and produced an unmet-peer warning
586
+ on every install.
587
+
588
+ - **Internal dependencies publish as `^x.y.z` rather than an exact pin.** Exact
589
+ pins across a family released together make npm install several copies of the
590
+ same package as soon as two versions coexist in one tree.
591
+
592
+ ### Note on compatibility
593
+
594
+ `HttpModule.contextOptions` and `TenancyModule.resolvers` are no longer present
595
+ as static properties. They were declared `private` and were never part of the
596
+ documented surface — TypeScript consumers could not reach them — but a
597
+ JavaScript consumer reading them would break. Nothing else changed shape.
598
+
599
+ ## 0.1.0
600
+
601
+ First release.