@birtalanrobert/game-economy 0.0.0-stage → 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 (116) hide show
  1. package/CHANGELOG.md +2252 -0
  2. package/LICENSE +661 -0
  3. package/NOTICE +45 -0
  4. package/README.md +117 -2
  5. package/dist/allocate.d.ts +12 -0
  6. package/dist/allocate.d.ts.map +1 -0
  7. package/dist/allocate.js +30 -0
  8. package/dist/allocate.js.map +1 -0
  9. package/dist/balance-of.d.ts +4 -0
  10. package/dist/balance-of.d.ts.map +1 -0
  11. package/dist/balance-of.js +10 -0
  12. package/dist/balance-of.js.map +1 -0
  13. package/dist/errors.d.ts +18 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/errors.js +39 -0
  16. package/dist/errors.js.map +1 -0
  17. package/dist/index.d.ts +8 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +19 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/is-refundable.d.ts +9 -0
  22. package/dist/is-refundable.d.ts.map +1 -0
  23. package/dist/is-refundable.js +13 -0
  24. package/dist/is-refundable.js.map +1 -0
  25. package/dist/kinds.d.ts +16 -0
  26. package/dist/kinds.d.ts.map +1 -0
  27. package/dist/kinds.js +16 -0
  28. package/dist/kinds.js.map +1 -0
  29. package/dist/migrations/1791401023940-CreateGameEconomy.d.ts +24 -0
  30. package/dist/migrations/1791401023940-CreateGameEconomy.d.ts.map +1 -0
  31. package/dist/migrations/1791401023940-CreateGameEconomy.js +124 -0
  32. package/dist/migrations/1791401023940-CreateGameEconomy.js.map +1 -0
  33. package/dist/nestjs/check-request.d.ts +13 -0
  34. package/dist/nestjs/check-request.d.ts.map +1 -0
  35. package/dist/nestjs/check-request.js +34 -0
  36. package/dist/nestjs/check-request.js.map +1 -0
  37. package/dist/nestjs/economy-balance.entity.d.ts +12 -0
  38. package/dist/nestjs/economy-balance.entity.d.ts.map +1 -0
  39. package/dist/nestjs/economy-balance.entity.js +45 -0
  40. package/dist/nestjs/economy-balance.entity.js.map +1 -0
  41. package/dist/nestjs/economy-draw.entity.d.ts +9 -0
  42. package/dist/nestjs/economy-draw.entity.d.ts.map +1 -0
  43. package/dist/nestjs/economy-draw.entity.js +38 -0
  44. package/dist/nestjs/economy-draw.entity.js.map +1 -0
  45. package/dist/nestjs/economy-entry.entity.d.ts +27 -0
  46. package/dist/nestjs/economy-entry.entity.d.ts.map +1 -0
  47. package/dist/nestjs/economy-entry.entity.js +95 -0
  48. package/dist/nestjs/economy-entry.entity.js.map +1 -0
  49. package/dist/nestjs/economy-lot.entity.d.ts +14 -0
  50. package/dist/nestjs/economy-lot.entity.d.ts.map +1 -0
  51. package/dist/nestjs/economy-lot.entity.js +58 -0
  52. package/dist/nestjs/economy-lot.entity.js.map +1 -0
  53. package/dist/nestjs/economy-views.d.ts +38 -0
  54. package/dist/nestjs/economy-views.d.ts.map +1 -0
  55. package/dist/nestjs/economy-views.js +3 -0
  56. package/dist/nestjs/economy-views.js.map +1 -0
  57. package/dist/nestjs/entry-view-of.d.ts +21 -0
  58. package/dist/nestjs/entry-view-of.d.ts.map +1 -0
  59. package/dist/nestjs/entry-view-of.js +25 -0
  60. package/dist/nestjs/entry-view-of.js.map +1 -0
  61. package/dist/nestjs/game-economy-options.types.d.ts +12 -0
  62. package/dist/nestjs/game-economy-options.types.d.ts.map +1 -0
  63. package/dist/nestjs/game-economy-options.types.js +3 -0
  64. package/dist/nestjs/game-economy-options.types.js.map +1 -0
  65. package/dist/nestjs/game-economy.module.d.ts +10 -0
  66. package/dist/nestjs/game-economy.module.d.ts.map +1 -0
  67. package/dist/nestjs/game-economy.module.js +33 -0
  68. package/dist/nestjs/game-economy.module.js.map +1 -0
  69. package/dist/nestjs/game-economy.service.d.ts +73 -0
  70. package/dist/nestjs/game-economy.service.d.ts.map +1 -0
  71. package/dist/nestjs/game-economy.service.js +253 -0
  72. package/dist/nestjs/game-economy.service.js.map +1 -0
  73. package/dist/nestjs/index.d.ts +17 -0
  74. package/dist/nestjs/index.d.ts.map +1 -0
  75. package/dist/nestjs/index.js +22 -0
  76. package/dist/nestjs/index.js.map +1 -0
  77. package/dist/nestjs/movement-request.types.d.ts +29 -0
  78. package/dist/nestjs/movement-request.types.d.ts.map +1 -0
  79. package/dist/nestjs/movement-request.types.js +3 -0
  80. package/dist/nestjs/movement-request.types.js.map +1 -0
  81. package/dist/nestjs/to-amount.d.ts +7 -0
  82. package/dist/nestjs/to-amount.d.ts.map +1 -0
  83. package/dist/nestjs/to-amount.js +16 -0
  84. package/dist/nestjs/to-amount.js.map +1 -0
  85. package/dist/spend-order.d.ts +12 -0
  86. package/dist/spend-order.d.ts.map +1 -0
  87. package/dist/spend-order.js +17 -0
  88. package/dist/spend-order.js.map +1 -0
  89. package/dist/types.d.ts +24 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +3 -0
  92. package/dist/types.js.map +1 -0
  93. package/nestjs/package.json +5 -0
  94. package/package.json +63 -3
  95. package/src/allocate.ts +34 -0
  96. package/src/balance-of.ts +12 -0
  97. package/src/errors.ts +35 -0
  98. package/src/index.ts +13 -0
  99. package/src/is-refundable.ts +11 -0
  100. package/src/kinds.ts +17 -0
  101. package/src/migrations/1791401023940-CreateGameEconomy.ts +135 -0
  102. package/src/nestjs/check-request.ts +44 -0
  103. package/src/nestjs/economy-balance.entity.ts +21 -0
  104. package/src/nestjs/economy-draw.entity.ts +16 -0
  105. package/src/nestjs/economy-entry.entity.ts +54 -0
  106. package/src/nestjs/economy-lot.entity.ts +29 -0
  107. package/src/nestjs/economy-views.ts +40 -0
  108. package/src/nestjs/entry-view-of.ts +40 -0
  109. package/src/nestjs/game-economy-options.types.ts +12 -0
  110. package/src/nestjs/game-economy.module.ts +22 -0
  111. package/src/nestjs/game-economy.service.ts +394 -0
  112. package/src/nestjs/index.ts +19 -0
  113. package/src/nestjs/movement-request.types.ts +29 -0
  114. package/src/nestjs/to-amount.ts +12 -0
  115. package/src/spend-order.ts +14 -0
  116. package/src/types.ts +26 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,2252 @@
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
+ ## game-economy 1.0.0
8
+
9
+ ### Added
10
+
11
+ - **A game's premium currency as an append-only ledger.** Every purchase,
12
+ grant, spend and refund is an entry with the balance it left; the balance is
13
+ a cache behind `CHECK (balance >= 0)`, and `reconcile` says whether it, the
14
+ entries and the credits still agree. Each credit is a lot that debits draw
15
+ on in the game's own `spendOrder` — required, never assumed — so whether a
16
+ purchase is still whole, and so refundable, is a fact. Writes join the
17
+ caller's transaction, take turns per holder and currency, and are
18
+ idempotent by key: the same key answers with its entry, and the same key for
19
+ a different entry is refused (`ledger_key_reused`). Entries and draws refuse
20
+ updates and deletes alike. Designed against projects 14, 15, 16 and 17;
21
+ first consumed by Holdfast's quest rewards.
22
+
23
+ ## workflow 1.6.0
24
+
25
+ ### Added
26
+
27
+ - **`appendOnlySql(table, { mutable })`: columns that are somebody's marks on
28
+ a record rather than the record.** A delivered report is evidence and must
29
+ not be rewritten, while when its recipient read it changes, and so do the
30
+ labels they give it. Until now the marks had to move to a table of their own,
31
+ joined on every list and every unread count, or the trigger had to go. An
32
+ update is now permitted when every column outside `mutable`, and outside
33
+ `redactable`, which keeps its own rule, is byte-for-byte what it was.
34
+
35
+ Beside a mutable column a redactable one may stay as it was or be erased,
36
+ where alone it must be erased by every update. That is the only way a mark
37
+ can change on a row whose redactable column still holds its value. A table
38
+ with only redactable columns, or none, keeps exactly the function it had.
39
+
40
+ A column named both ways is refused as the SQL is made. A name the table
41
+ does not have is refused when the trigger runs, naming the column, instead of
42
+ leaving its real namesake guarded and every update to it blamed on the
43
+ record.
44
+
45
+ ## idempotency 1.2.0
46
+
47
+ ### Fixed
48
+
49
+ - **A refused request held its key for five minutes.** The interceptor claimed
50
+ the key, completed it when the handler answered, and did nothing when the
51
+ handler threw: `release()` existed, documented as "called when the work
52
+ failed", and nothing called it. A command refused for a reason the caller
53
+ could fix — not enough of something, a validation error — left the key
54
+ `in_progress`, and the retry under it was answered "already in progress"
55
+ until the lock timed out. A handler that fails before committing a write now
56
+ frees its key.
57
+
58
+ Freeing it on _every_ failure would have been wrong, and is why this is more
59
+ than one line: a handler can fail after its work has committed — a callback
60
+ run after the commit, failing — and a key freed then lets the retry do the
61
+ work twice.
62
+
63
+ - **The key was not marked done with the work.** The README said the
64
+ completion commits in the handler's own transaction; it committed after the
65
+ handler had answered, in a statement of its own, so a crash between the two
66
+ left the work done and the key free to do it again once the claim was taken
67
+ for abandoned. While the handler runs, the claim now joins every transaction
68
+ that commits (`joinCommits`, database 1.2.0), and the first to commit a write
69
+ marks the key done as its last statement. A transaction that wrote nothing
70
+ does not count: Postgres gives one an id only when it writes. The response is
71
+ stored once the handler answers; a repeat before that, or after a crash that
72
+ never stored it, is answered with the route's status and no body.
73
+ - **The status recorded was the response's default, not the route's.** Nest
74
+ sets a response's status after every interceptor has finished, so a `201` or
75
+ a `204` route was recorded as `200`. It is now read from the route's
76
+ `@HttpCode`, or Nest's default for the method.
77
+ - **`@nestjs/core` is declared** as an optional peer dependency. The
78
+ interceptor has always imported it; it resolved through hoisting.
79
+
80
+ ### Changed
81
+
82
+ - **A route's parameters are part of the request a key stands for.** The scope
83
+ is the route's pattern, so one key sent to `DELETE /orders/a` and then to
84
+ `DELETE /orders/b` had the second answered with the first's response. The
85
+ parameters are now fingerprinted with the body, and the second is refused as
86
+ a key reused for a different request. A route without parameters is
87
+ fingerprinted by its body alone, as before; a key claimed on a route with
88
+ parameters before upgrading, and retried after, is refused rather than
89
+ replayed.
90
+ - **Needs Postgres 13 or later.**
91
+
92
+ ### Added
93
+
94
+ - `IdempotencyService.markDone(record, status, manager)`: marks a claim done
95
+ inside the given transaction, before its response is known.
96
+
97
+ ## database 1.2.0
98
+
99
+ ### Added
100
+
101
+ - **`joinCommits(participant, work)`**: `participant` joins every outermost
102
+ transaction committed while `work` runs — called inside each, with its
103
+ manager, just before the commit — so what it writes commits with that work or
104
+ not at all; it may answer with a callback run once the commit has happened.
105
+ Savepoints and transactions bound with `bindTransactionManager` are not
106
+ joined. `CommitParticipant` is its type.
107
+
108
+ ## jobs 1.2.0
109
+
110
+ ### Fixed
111
+
112
+ - **`TaskScheduler` ran a task once per replica, not once per fleet.** Each
113
+ replica ticked one interval after it had started, and the lock was released
114
+ when a run finished. Replicas start at different moments, so their ticks fell
115
+ at different points in each interval, and each one found the lock free:
116
+ three replicas ran a fifteen-minute task three times every fifteen minutes,
117
+ and a nightly task three times a night. The lock only ever stopped two runs
118
+ at the same instant. The integration test that claimed otherwise started five
119
+ schedulers in the same instant, which a fleet never does.
120
+
121
+ Each run now **claims its interval**. The key is never released, only left to
122
+ expire after the interval has passed, so a replica reaching the same interval
123
+ later finds it taken. The lock held while a run is going still keeps a run
124
+ that overruns into the next interval from being joined by a second.
125
+
126
+ ### Changed
127
+
128
+ - **Ticks fall at the start of each interval, counted from the epoch**: 12:00,
129
+ 12:15, 12:30 for fifteen minutes, on every replica. Before, a replica ticked
130
+ every interval from whenever it had started. Each tick is set afresh from the
131
+ clock, so a late timer never carries its lateness forward. `runOnStart` runs
132
+ at once only if the current interval has not run anywhere in the fleet.
133
+ - **An interval must be a whole, positive number of milliseconds**, or
134
+ `register` throws.
135
+
136
+ ### Added
137
+
138
+ - **`new TaskScheduler(locks, logger, { now })`**: the clock intervals are
139
+ counted on, for tests.
140
+
141
+ ## database 1.1.1
142
+
143
+ ### Fixed
144
+
145
+ - **`afterCommit` callbacks registered inside a savepoint that rolled back ran
146
+ anyway.** Every level of a transaction pushed its callbacks onto one shared
147
+ list, the outermost level's. If a savepoint's work was rolled back, and the
148
+ caller caught the error and let the transaction commit, the side effects of
149
+ the undone work still ran: an email for an account whose creation was undone,
150
+ a job for a row that was never written. Each level now keeps its own list. A
151
+ savepoint hands its list to its parent when it is released and drops it when
152
+ it rolls back. Callbacks still run once, after the outermost commit, in the
153
+ order they were registered.
154
+
155
+ - **`afterCommit` inside `bindTransactionManager` dropped its callback without a
156
+ word.** The code that opened the transaction commits it, out of this module's
157
+ sight, so the list those callbacks went onto was never run. `afterCommit` now
158
+ throws there, and says where to register the side effect instead.
159
+
160
+ ## realtime 2.2.0
161
+
162
+ Four ways a client could miss events without being told. Each is fixed below
163
+ and covered by a test that fails on 2.1.0.
164
+
165
+ ### Fixed
166
+
167
+ - **A channel that expired and was used again skipped events on pages already
168
+ open.** `RedisBacklog` expires a quiet channel's counter with its log, so the
169
+ channel's next event was numbered 1 again. A page left open overnight still
170
+ stood at, say, 7. It skipped events 1 to 7 as duplicates, and nothing told it.
171
+ A channel now starts at the Redis server's clock in milliseconds, so a channel
172
+ used again is numbered above anything it handed out before, unless it averaged
173
+ more than one event a millisecond for its whole life. An open page sees a jump
174
+ and resynchronises. Where the channel's current run began is kept beside it,
175
+ and `BacklogPort.bounds` reports it as `first`.
176
+
177
+ - **A client that joined an empty channel lost what arrived while it was
178
+ away.** It stood at 0, and `resume` read 0 as "new here", so it answered from
179
+ now. A phone opened on a quiet channel locked, a timer finished, and the phone
180
+ came back to nothing. `resume` now tells no position (`undefined`: join from
181
+ now) apart from a position of 0 (seen nothing, so replay everything since the
182
+ channel's first event). The socket server and `pollSince` pass a missing
183
+ position through as `undefined`. `ChannelCursor` takes the first event after
184
+ 0 whatever it is numbered, and `has(channel)` tells 0 apart from no position.
185
+
186
+ - **A polling client ignored `gaps`.** A channel it had fallen too far behind in
187
+ was named in `gaps`. The client kept its position, asked from it again, was
188
+ told the same, and never delivered another event on that channel. The socket
189
+ never came back to repair it, because the fallback is for networks that eat
190
+ sockets. It now does what a `gap` frame does: it starts again where the
191
+ channel stands and calls `onResync`.
192
+
193
+ - **A polling client that joined an empty channel never took a position.** Its
194
+ next poll was a join again, answered from now, and the channel's first event
195
+ was stepped over. It now takes 0.
196
+
197
+ - **A client standing further along than a channel has ever been is told to
198
+ start again.** This happens after a `MemoryBacklog` restarts with its
199
+ process. The client used to be answered with nothing, and it skipped
200
+ everything up to its old position.
201
+
202
+ ### Changed
203
+
204
+ - **`RedisBacklog` numbers a new channel from the server's clock, not from 1.**
205
+ Sequences stay monotonic, gap-free and per channel, and `MemoryBacklog` still
206
+ starts at 1. A channel created before this version carries on from where it
207
+ stands.
208
+ - **`resume(backlog, channel, since)` takes `since: number | undefined`.** A
209
+ direct caller that passed 0 to mean "new here" now passes `undefined`.
210
+ `parsePollQuery` already leaves an absent channel out, so `pollSince` needs no
211
+ change.
212
+
213
+ ## redis 1.1.1
214
+
215
+ ### Fixed
216
+
217
+ - **`RedisService.subscriber` connects on its first `SUBSCRIBE`, not when it
218
+ is read.** `RedisBroadcast` takes the subscriber in its constructor. A process
219
+ that only sends, such as a worker publishing into a gateway's backlog, builds
220
+ one and never calls `listen`, and 1.1.0 gave that process an idle connection
221
+ for its whole life. `close()` disconnects a subscriber that never subscribed,
222
+ rather than sending `QUIT`, which would open the connection only to close
223
+ it.
224
+
225
+ ## redis 1.1.0
226
+
227
+ ### Added
228
+
229
+ - **`RedisService.subscriber`**: the connection a process listens on for
230
+ pub/sub. It is opened on first use as a copy of `client`'s options, named
231
+ `<client name>-subscriber` in `CLIENT LIST`, and shared by everything in the
232
+ process that listens.
233
+
234
+ A subscribed connection may issue nothing but subscriptions, so pub/sub
235
+ always needed a second connection, and there was nowhere to get one. Each
236
+ product built its own from the URL, then had to remember to quit it. One
237
+ product built three. A subscriber nothing quits keeps a process alive after
238
+ `SIGTERM`.
239
+
240
+ - **`RedisService.close()`**: quits the client and, if one was opened, the
241
+ subscriber. `RedisModule` now calls it on shutdown instead of quitting only
242
+ the client.
243
+
244
+ ## realtime 2.1.0
245
+
246
+ ### Fixed
247
+
248
+ - **A subscription that could not be answered no longer takes the process
249
+ down.** `RealtimeSocketServer` handed each frame to an async handler and
250
+ dropped its promise. When `authorise` threw, or the backlog did, the rejection
251
+ had nowhere to go. An unhandled rejection ends a Node process by default, so a
252
+ database blip in a product's `authorise` could stop every connection on that
253
+ replica.
254
+
255
+ The connection is now closed with `1011`. The client polls, reconnects and
256
+ resumes from where it stood, so nothing is lost. Carrying on was not an
257
+ option: a subscription that failed part-way leaves the connection holding some
258
+ channels with no `welcome`, and the client would believe in channels it does
259
+ not have. The error goes to the new `onError`.
260
+
261
+ ### Added
262
+
263
+ - **`admit`**: whether a connection is opened at all, asked once before the
264
+ upgrade. Until now every upgrade on the path was accepted. A request with no
265
+ credential was upgraded and granted nothing by `authorise`. The heartbeat then
266
+ kept that socket alive for as long as the caller answered, so anybody who
267
+ could reach the port could hold sockets for free. `admit` is also where a
268
+ product checks `Origin`: a browser sends its cookies on a WebSocket upgrade
269
+ from any page, so a gateway that authenticates by cookie must refuse pages it
270
+ does not serve.
271
+
272
+ A refusal is answered `403 Forbidden`, the status RFC 6455 names. A throw is
273
+ answered `503 Service Unavailable`, because a check that could not be made
274
+ does not mean the caller was refused. It is asked only after `ws` has
275
+ validated the handshake and matched the path.
276
+
277
+ - **`maxPayload`**: the largest frame a client may send, in bytes. A heavier
278
+ frame closes the connection with `1009`.
279
+
280
+ - **`onError`**: told when an `admit` threw or a subscription failed, so the
281
+ product can log it. A reporter that throws is contained.
282
+
283
+ ### Changed
284
+
285
+ - **A client frame may weigh 64 KiB by default.** Before, the limit was `ws`'s
286
+ own default of 100 MiB, which let any connection make the process buffer and
287
+ parse a hundred megabytes of JSON. The heaviest frame a client sends is a
288
+ subscription. One naming a few hundred channels, with its position in each, is
289
+ a few kilobytes.
290
+
291
+ ## observability 1.5.0
292
+
293
+ ### Fixed
294
+
295
+ - **`InMemoryMetrics` no longer keeps every histogram observation.** It pushed
296
+ each one onto an array for the life of the process, and it is the registry
297
+ `LoggerModule` gives a process unless told otherwise — so an API whose
298
+ `LoggingInterceptor` times every request grew by one number per request, for
299
+ months, in production. Its `snapshot()` then spread them all into `Math.min`
300
+ and `Math.max`, which throws a `RangeError` past about a hundred thousand: the
301
+ `/metrics` route started failing on its own after enough traffic.
302
+
303
+ A histogram is now what Prometheus keeps — a count per bucket, the count, the
304
+ sum, the minimum and the maximum — fixed in size by its buckets, plus a ring
305
+ of its most recent observations for `observations()`: a thousand, or
306
+ `new InMemoryMetrics({ recentObservations })`.
307
+
308
+ ### Added
309
+
310
+ - **Buckets.** `histogram(name, help, buckets)` now uses the `buckets` it was
311
+ always offered (`DEFAULT_BUCKETS_MS` unless given). The first caller to name
312
+ a histogram fixes them; naming it again without buckets is the same
313
+ histogram, and naming it again with different ones throws, because one metric
314
+ counted into two sets of buckets is two metrics under one name.
315
+ - **`HistogramSeries.buckets`**, cumulative, filled by `InMemoryMetrics` and
316
+ optional in the type, so a snapshot assembled by hand is still one.
317
+ - **`toPrometheus` renders them** as `_bucket` series up to `+Inf` before
318
+ `_count`, `_sum` and `_max`, which is what `histogram_quantile` reads a
319
+ percentile from. A snapshot with no buckets renders exactly as before.
320
+
321
+ ### Changed
322
+
323
+ - **`observations()` returns the most recent observations**, oldest first, not
324
+ all of them. A test that records fewer than a thousand sees no difference.
325
+
326
+ ## config 1.3.0
327
+
328
+ ### Fixed
329
+
330
+ - **A password inside a URL is redacted, whatever the key is called.** Keys
331
+ were redacted by name, and `DATABASE_URL`, `REDIS_URL` and `SMTP_URL` name no
332
+ secret although the password in each is one — for an email relay it is the
333
+ provider's API key — so every boot banner printed them in full, in every
334
+ environment. `redactConfig`, and therefore `describeConfig` and `logOnBoot`,
335
+ now mask the password of any URL in any string value or list with `***`,
336
+ keeping the scheme, the user and the host, so a banner still says which role
337
+ connected to where.
338
+
339
+ ## comms 1.10.0
340
+
341
+ ### Added
342
+
343
+ - **`sentTo`, `forget` and `purge`**: what a product needs from a message log
344
+ when somebody asks what is held about them, asks to be forgotten, and when a
345
+ published retention schedule comes due.
346
+
347
+ `sentTo(tenantId, address)` answers "what have you ever sent _me_", which no
348
+ subject id can: one person's messages are spread across every booking they
349
+ ever made. `history` still answers the other question, "what happened about
350
+ this booking".
351
+
352
+ `forget(tenantId, address)` replaces the address and clears the heading,
353
+ keeping the row — the same trade `FilesService.erase` makes: a record saying a
354
+ message was delivered on a date is worth more than a gap where it used to be,
355
+ and an erasure is about the person rather than about the fact that somebody
356
+ was written to. **Suppressions are deliberately untouched**: an address that
357
+ said "stop" has to keep being refused, and forgetting that is how an erased
358
+ person is emailed again the next time a business imports a list.
359
+
360
+ `purge(before, limit)` deletes rows past their retention — deleted rather than
361
+ emptied, because a row that old has nothing left worth keeping and a table of
362
+ hollowed rows still grows for ever. Bounded, and returns what it deleted, so a
363
+ sweep runs again until it returns zero.
364
+
365
+ Both are here rather than in each product because a product doing this for
366
+ itself has to know which columns hold a person, in a table it does not own.
367
+
368
+ ## billing 3.2.0
369
+
370
+ ### Added
371
+
372
+ - **`recount` and `usageBetween`**: the second shape of metered usage, for a
373
+ product that counts a _table_ on a schedule rather than an _event_ as it
374
+ happens.
375
+
376
+ `record` adds, which is the only correct arithmetic for a text message sent or
377
+ a pack bought. A ticketing platform meters from the rows that record the
378
+ tickets, read every night — and there adding is wrong in both directions: a
379
+ second run doubles the day, and a ticket refunded after the first run never
380
+ comes back off. Expressing that with `record` would mean the product
381
+ remembering what it last counted, which means reading this package's table
382
+ from outside this package.
383
+
384
+ An unchanged figure keeps its `reported_at`. Clearing it on every recount
385
+ would owe the provider the same day every night for as long as the
386
+ aggregation keeps finding the same answer, which is every night after the
387
+ first.
388
+
389
+ `usageBetween` exists for the same reason the read inside `reportUsage` had to
390
+ move: `mortar_usage_records` carries `FORCE ROW LEVEL SECURITY`, so a product
391
+ summing a period for itself would read nothing and report success.
392
+
393
+ ## wallet 1.3.0
394
+
395
+ ### Added
396
+
397
+ - **Google event tickets**: `buildEventTicketClass` and `buildEventTicketObject`,
398
+ and `saveLink` now carries either an event ticket or a loyalty object.
399
+
400
+ This package was designed against two specifications — a stamp card and an
401
+ event ticket — and the Apple side needed nothing: a `.pkpass` is one format
402
+ and `PassContent` already said `eventTicket`. Google models the two as
403
+ **different classes with different fields**, so a ticket sent as a loyalty
404
+ object installs with a balance where its seat should be. Google keys the save
405
+ token's payload by kind, which is why carrying both, or neither, is now
406
+ refused rather than silently producing a pass nobody wanted.
407
+
408
+ Two things the ticket shape needed that a card did not. The class is **per
409
+ performance rather than per production**, because Google puts the date, the
410
+ venue and the name on the class and an object cannot override them — a season
411
+ sharing one class shows every holder the first night's date. And the times are
412
+ written **with the venue's own offset** rather than as UTC: `17:30Z` and
413
+ `19:30+02:00` are the same moment and only the second is what the ticket says,
414
+ so a holder shown the first arrives after the interval. The offset is computed
415
+ per date rather than per venue, because a performance on the last Sunday in
416
+ October is an hour from one the week before.
417
+
418
+ `multipleDevicesAndHoldersAllowedStatus` differs deliberately: a loyalty card
419
+ is `MULTIPLE_HOLDERS`, because one that vanished when somebody changed phone
420
+ looks broken; a ticket is `ONE_USER_ALL_DEVICES`, because a pass several
421
+ people can hold at once is the screenshot problem with Google's blessing.
422
+ Passing one on is the product's act, where the old code is revoked as the new
423
+ one is issued.
424
+
425
+ ## files 1.10.0
426
+
427
+ ### Added
428
+
429
+ - **`PrintableCard.lines`** — detail lines under the caption, above the
430
+ footnote. A table tent needs none of them; a ticket needs several, and they
431
+ are what tells two otherwise identical pieces of paper apart: the date, the
432
+ seat, whose name is on it. The second consumer of `printableCards` wanted a
433
+ card with six things on it rather than three, which is the difference between
434
+ a heading and an identity.
435
+
436
+ Each is one line and is not wrapped — a line too long for the card is shrunk
437
+ to fit, like every other string here. Where a sentence breaks is a decision
438
+ about the caller's own words, and a layout that guessed would guess in three
439
+ languages.
440
+
441
+ ## commerce 4.4.0
442
+
443
+ ### Added
444
+
445
+ - **Disputes.** `ProviderEvent` gains a `dispute` kind carrying the dispute's
446
+ own id, the bank's reason, a status reduced to the four that change what to do
447
+ — open, under review, won, lost — and **the deadline evidence has to beat**,
448
+ which is the field that actually matters: missing it loses the money whatever
449
+ the evidence would have said. Stripe's seven statuses collapse into those
450
+ four, and an unfamiliar one reads as `lost`, because the safe default is the
451
+ one that makes a product act rather than file the money as recovered.
452
+
453
+ `settle` deliberately does not record it: the money has already moved and
454
+ there is nothing on a payment row to update. What a dispute needs is evidence
455
+ assembled from what was _bought_, which lives in the product rather than here.
456
+
457
+ - **`fingerprint`** on a charge result and on a payment event — the provider's
458
+ own handle on "this is the same card as that one", neither a number nor
459
+ reversible. It is the only thing a product can count _per card_ with, and a
460
+ cap per card is the anti-scalping control that gets asked for first: an
461
+ address and an email cost nothing to invent, and a card does not.
462
+
463
+ ## commerce 4.3.0
464
+
465
+ ### Added
466
+
467
+ - **`ProviderEvent.id`** — the provider's own identifier for a webhook
468
+ delivery, so a repeat can be recognised as one. Every provider retries for
469
+ hours on any response it does not like, including the ones it never received
470
+ because the process was restarting; without an id for the _delivery_ a caller
471
+ either does the work twice or has to make every handler idempotent by hand.
472
+ The second is only possible where the work is an assignment rather than an
473
+ act: marking a payment captured twice is harmless, sending a buyer two tickets
474
+ is not. `externalId` cannot stand in — one payment produces several events,
475
+ and deduplicating on it would drop the later ones.
476
+
477
+ ### Fixed
478
+
479
+ - **A refund made in Stripe's own dashboard changed nothing.** `charge.refunded`
480
+ carried no tenant, and `settle` ignores an event that cannot say whose it is —
481
+ deliberately, because every table it would read is under row-level security
482
+ and an unbound lookup returns nothing at all. So the customer had their money
483
+ back and the books still said captured, which is the direction of error a
484
+ business finds out about from its accountant. The charge's metadata carries
485
+ the tenant (Stripe copies a payment intent's onto the charge it creates), and
486
+ it is read now.
487
+
488
+ ## observability 1.3.0
489
+
490
+ ### Changed
491
+
492
+ - **The HTTP status decides the log level, not whether something was thrown.**
493
+ Every refusal reaches the interceptor as an exception — that is how a
494
+ framework says "no" — and all of them were logged at `error` with a full
495
+ stack. A ticketing product's busiest, most correct minute then looks exactly
496
+ like an outage: four hundred `error` lines a second, each carrying a stack,
497
+ for four hundred buyers being told somebody else got the seat. It floods the
498
+ alerting rule that is watching for the thing it can now no longer see, and
499
+ serialising a stack per request is real work on the one event loop that is
500
+ already the bottleneck under load.
501
+
502
+ A 4xx is now `warn`, logged as `request refused` with the three fields worth
503
+ grouping on — the error's class, its application code and the sentence the
504
+ caller was given — and without the stack, which describes our frames rather
505
+ than their mistake. A 5xx is unchanged: ours, and keeps everything.
506
+
507
+ ## config 1.2.0
508
+
509
+ ### Added
510
+
511
+ - **`envText`** — a string that may be empty, which is how a feature is switched
512
+ off. `envString` is "this service does not run without it"; this is "an
513
+ address nobody published means the thing behind it is not there". A live
514
+ channel, an analytics endpoint, a support address: each has a page that must
515
+ render perfectly without one.
516
+
517
+ ### Fixed
518
+
519
+ - **`envString('')` built a schema that could never pass.** The default was
520
+ substituted and then failed the `min(1)` it had just been given, so a variable
521
+ documented as optional took the whole surface down the first time it was
522
+ actually left unset — every page, at boot, with a validation error naming a
523
+ variable the operator had deliberately omitted. It now throws where the
524
+ mistake is made, naming `envText`.
525
+
526
+ ## realtime 2.0.1
527
+
528
+ ### Fixed
529
+
530
+ - **The polling fallback could never deliver a first event.** A client that
531
+ falls back to HTTP starts with an empty cursor and asks `since: {}`; `resume`
532
+ reads that as "I am new here" and answers with `latest` and no events, which
533
+ is deliberate — a newcomer does not want the whole backlog. But the polling
534
+ loop advanced its cursor only from events it received, so it never left zero:
535
+ the next poll asked `{}` again, and the one after that, for ever. A page that
536
+ polls perfectly and is never told a thing.
537
+
538
+ It now seeds from `latest`, after applying the events and only for a channel
539
+ still standing at zero — the same two conditions the socket path has always
540
+ applied to the `start` frame. Seeding a channel that has just been handed a
541
+ resume would throw that resume away.
542
+
543
+ The socket half was never affected, which is why this survived two releases:
544
+ the fallback is what a venue with a hostile network gets, and nobody had
545
+ watched one work.
546
+
547
+ - **`connecting` was reported for every retry behind a working fallback.** Once
548
+ polling is running the page is connected — over HTTP — and the socket attempt
549
+ behind it is background work. Each attempt moved the state back, so a seat map
550
+ that was updating perfectly said "connecting…" for as long as it was open, and
551
+ `polling` showed only for the instant between a socket dying and the next try.
552
+ On a network that eats WebSockets that is every few seconds. The state a
553
+ product shows is now the state it is in.
554
+
555
+ ## billing 3.1.0
556
+
557
+ ### Added
558
+
559
+ - **`assign`** — puts a business on a plan without sending anybody to a payment
560
+ page. A chain is sold to rather than checked out, a pilot runs on somebody's
561
+ word, and a business migrating from a competitor is put on the plan it agreed
562
+ to before a card is entered.
563
+
564
+ It is also what a deployment with **no provider configured** needs to have a
565
+ subscription at all: every path to a subscription row went through a hosted
566
+ checkout, so until a Stripe account existed there was nothing for a billing
567
+ screen to show and no way to demonstrate the product's own commercial
568
+ behaviour. Project 06's back office is the second consumer of that need; the
569
+ first was project 05's, worked around at the time.
570
+
571
+ It refuses to touch a subscription the provider owns — once a card is being
572
+ charged on a schedule, a plan code written beside it makes our screen and
573
+ their invoice disagree, and the customer believes whichever they saw first.
574
+ Changing _how many_ stays `setQuantity`, which tells the provider.
575
+
576
+ - **`nextPeriodEnd`** — when the period being paid for ends, for a subscription
577
+ this deployment keeps itself. The day of the month is clamped rather than
578
+ rolled over: a month after the thirty-first of January is the twenty-eighth of
579
+ February, because otherwise a business billed on the thirty-first drifts
580
+ forward through the calendar and the two months with the drift in them are
581
+ charged twice.
582
+
583
+ ## links 1.0.0
584
+
585
+ New package: **signed, expiring links** — how somebody reaches a page without an
586
+ account.
587
+
588
+ It was written inside `stamped-enrol`, with a comment saying it was the
589
+ extraction candidate and would move at its second consumer. Phase 4 of project
590
+ 06 is the API starting to _mint_ them, which is that second consumer — and the
591
+ two halves had to move together, because they must agree exactly on the payload
592
+ encoding and two implementations that agree today will not agree in a year.
593
+
594
+ `verifyLink` reports `malformed`, `invalid` or `expired` separately, because
595
+ those have three different remedies. The signature is checked before the expiry,
596
+ so an expired token nobody signed reads as invalid rather than as merely
597
+ expired — which would otherwise invite somebody to keep trying with a fresher
598
+ timestamp.
599
+
600
+ Web Crypto rather than `node:crypto`, so the same code runs in an edge
601
+ middleware, a browser and a Nest service. No dependencies.
602
+
603
+ ## wallet 1.2.0
604
+
605
+ ### Added
606
+
607
+ - **`onRegistered`**, beside the `onDeregistered` that was already there. A
608
+ registration is the only signal either platform gives that a pass was actually
609
+ _installed_: issuing one is a file leaving a server, and nothing else
610
+ distinguishes the two. A product measuring an enrolment funnel, or holding a
611
+ welcome bonus back until there is a phone to show it on, has this and nothing
612
+ else to go on.
613
+
614
+ `created` says whether the device already had it, so a retry is not counted as
615
+ an installation. Whether a _re_-installation counts is left to the product,
616
+ because that is where the answer is known.
617
+
618
+ ### Fixed
619
+
620
+ - **A removal that removed nothing no longer reports a withdrawal of consent.**
621
+ `unregisterDevice` answers 200 whether or not a row went — correctly, because
622
+ a device retrying has not made a mistake — and the Nest layer was reading that
623
+ status as "a holder removed their pass". A device that had nothing registered,
624
+ or somebody with a serial and no pass, would have suppressed a customer's
625
+ messages by asking twice.
626
+
627
+ `ProtocolResult` now carries `changed`, which is what the status deliberately
628
+ hides, and both hooks read it.
629
+
630
+ ## files 1.9.0
631
+
632
+ ### Added
633
+
634
+ - **A fixed frame for a derivative.** `DerivativeSpec` takes an optional
635
+ `height`, with `fit` (`contain` or `cover`) and `background`. Given both
636
+ dimensions the derivative comes out at exactly that size.
637
+
638
+ The pipeline was written for photographs, where asking for a width and letting
639
+ the height follow is right. An _asset_ has no shape of its own to keep: a
640
+ square avatar, and a wallet pass's icon, which Apple and Google both reject at
641
+ any size but the one they name. There is no way to produce either by naming a
642
+ width.
643
+
644
+ `contain` never enlarges: a small upload is centred and padded rather than
645
+ blown up, because a stretched logo on a customer's card is worse than a small
646
+ one. Padding is transparent unless told otherwise, which is what a logo laid
647
+ over an unknown colour needs; a format without an alpha channel renders that
648
+ as black, so a JPEG derivative of a padded image should name a background.
649
+
650
+ `cover` does enlarge, and has to. A frame that is not filled is not a cover,
651
+ and refusing would hand back a derivative the size of the _upload_ rather than
652
+ the size asked for — a 40-pixel strip image where a pass wanted 375 by 123,
653
+ which is a file the device refuses at a counter rather than an error here.
654
+
655
+ ## wallet 1.1.0
656
+
657
+ ### Added
658
+
659
+ - **Apple's update web service**, as a protocol rather than a controller. The
660
+ five handlers, the registration table and the push live here; the product
661
+ supplies a `PassSource` — three methods that say what a pass _is_ — and writes
662
+ its own controller, because where the routes sit, which guard marks them
663
+ public and what rate limit they carry are the product's decisions. Expressing
664
+ any of them here would mean this package depending on a product's
665
+ authentication.
666
+ - **`/nestjs`**: `WalletModule`, `WalletWebService`, `WalletRegistrationsService`,
667
+ and the three tables — registrations, push deliveries and the device log.
668
+ - **APNs** behind `ApnsPort`, with `RecordingApns` and an HTTP/2 client.
669
+ - **Google** behind `GoogleWalletPort`, with `RecordingGoogleWallet` and a real
670
+ client.
671
+
672
+ ### Three decisions worth recording
673
+
674
+ - **The per-pass authentication token is derived, not stored.** Every pass
675
+ carries a secret the device sends back, and the obvious implementation keeps a
676
+ column of them. `passAuthenticationToken(secret, subject)` computes it from one
677
+ deployment secret instead: nothing secret is in the database, a rebuilt pass
678
+ carries the same token — which a stored hash could not produce — and rotation
679
+ is incrementing a number on the row. The cost is that the secret is the whole
680
+ scheme: change it and every outstanding pass stops being able to update, which
681
+ is right after a leak and a catastrophe by accident.
682
+
683
+ - **The updated-since tag comes from a monotonic sequence and never a clock.**
684
+ Two updates inside the same tick share a clock value; the device stores it,
685
+ asks again, is told nothing changed, and keeps a pass that has quietly stopped
686
+ matching the database. `webservice.test.ts` asserts both of two updates made in
687
+ the same instant are reported.
688
+
689
+ - **Pushes are coalesced per device, not per pass.** The push carries nothing —
690
+ no serial, no payload — and the device answers it by asking which of _its_
691
+ passes changed. A holder whose two cards both changed needs one notification;
692
+ sending two makes a phone buzz twice for one question it will ask once. Five
693
+ stamps on five customers is still five pushes.
694
+
695
+ ### Smaller things that are easy to get wrong, and are handled
696
+
697
+ - `Last-Modified` is compared **loosely**: HTTP dates carry one second, so an
698
+ equal timestamp serves the pass rather than answering 304. At worst one
699
+ redundant fetch, never a missed update.
700
+ - Re-registering a device **takes the new push token**. A device that reinstalls
701
+ the pass calls with the same identifiers and a different token, and keeping the
702
+ old one means pushing into the void for ever while recording every one as
703
+ delivered.
704
+ - `410 Unregistered` **removes the registration** rather than only being logged.
705
+ It is the only signal there is that somebody deleted their card without the
706
+ deregistration arriving.
707
+ - Registration answers **201 the first time and 200 the next**, which Apple
708
+ documents and devices rely on.
709
+ - The updated-since query answers **204**, not 200 with an empty array.
710
+ - `POST /v1/log` is implemented. It is the only diagnostic Apple sends anywhere,
711
+ and a programme with no device of its own has more use for it than most.
712
+ - The three tables **carry no row-level security policy**, deliberately and for
713
+ the same reason `platform_operators` does not: a device sends an opaque
714
+ identifier and a serial and nothing else, so every lookup happens before any
715
+ tenant is known — and a bound read of a `FORCE`-secured table returns nothing
716
+ while reporting success.
717
+
718
+ ## wallet 1.0.0
719
+
720
+ New package. Apple Wallet and Google Wallet passes — one content model, two
721
+ renderers, and a conformance suite.
722
+
723
+ ### Why it is here rather than inside a product
724
+
725
+ Project 06's specification asks for "a module within the API, isolated behind a
726
+ clean interface, so that it can be lifted into project 1 without modification".
727
+ The extraction policy says the opposite and is right: two of the seventeen
728
+ specifications need wallet passes — loyalty cards and tickets — which is the
729
+ threshold, and an interface designed against one of them acquires a
730
+ loyalty-shaped assumption in its first week. "Liftable later" is a promise
731
+ nobody has ever kept.
732
+
733
+ So the vocabulary is `PassContent` and `PassField` rather than `stamps` and
734
+ `balance`, and both specifications were read before the first type was written.
735
+
736
+ ### What it does
737
+
738
+ - **`buildPkPass`** — validate, render `pass.json`, hash every file, sign the
739
+ manifest, zip. Deterministic given `builtAt`, because the manifest hashes the
740
+ content and a build that varied would make every rebuild look to a device like
741
+ a change. Refuses rather than signing a pass that breaks a rule.
742
+ - **`verifyPkPass`** — the conformance check. Reads the archive back **from its
743
+ bytes** rather than from whatever the builder thought it wrote, re-derives
744
+ every hash in both directions, verifies the detached PKCS#7 against a chain,
745
+ and applies the rules to the `pass.json` actually in the file.
746
+ - **`apple/rules.ts`** — every rule in one file with a citation each, plus
747
+ `RULES_REVIEWED`.
748
+ - **`readSigningCertificate`** — reads the pass type and team identifiers out of
749
+ the certificate rather than taking them from configuration beside it, because
750
+ a device refuses a pass whose two disagree with its signature and says nothing
751
+ useful about why. Reports `selfSigned`.
752
+ - **`buildLoyaltyClass` / `buildLoyaltyObject` / `saveLink`** — Google's half,
753
+ from the same content.
754
+ - **`/testing`** — `testSigner`, `sampleContent`, `sampleAssets`, `solidPng`.
755
+ No network, no keychain, no openssl binary.
756
+ - **`wallet:verify`**, with a `--live` mode that uses real credentials and names
757
+ what it did not check instead of passing quietly.
758
+
759
+ ### The honest part
760
+
761
+ There is no device in this programme and there will not be one, so this package
762
+ cannot prove that a lock screen renders a pass, that APNs delivers, or that
763
+ Apple accepts our certificate. Those are stated in the README as unprovable here
764
+ rather than implied by a green run, and `--live` exists now so that one command
765
+ answers them the day an Apple account does.
766
+
767
+ The substitute for a canary is `RULES_REVIEWED` — a date a human moves by
768
+ re-reading Apple's published requirements. It is weaker than a canary and is
769
+ written down as weaker.
770
+
771
+ ### Decisions worth recording
772
+
773
+ - **`pkijs` for CMS, `node:crypto` for JWTs.** ASN.1 is a solved problem with
774
+ decades behind it and shelling out to `openssl smime` would make the build
775
+ depend on a binary's version. A JWT with a fixed algorithm is not: everything
776
+ dangerous about JWT is on the verifying side, both tokens here are read by
777
+ somebody else, and `jose` is ESM-only in a CommonJS monorepo. `verifyJwt`
778
+ takes the algorithm as an **argument** rather than reading it from the header,
779
+ which is the decision that makes the difference.
780
+ - **Assets are bytes, not keys or URLs.** A package that signs a file for
781
+ somebody else's operating system should not also need an S3 client to be
782
+ tested.
783
+ - **`sharingProhibited` defaults on for store cards and coupons**, which is the
784
+ opposite of the platform default. A shareable stamp card is a screenshot in a
785
+ group chat — the same failure that makes a static counter QR code unusable.
786
+
787
+ ### Found while writing it
788
+
789
+ - **A DER serial number with an unconditional leading zero parsed about half the
790
+ time.** DER permits the padding byte only where the next byte would set the
791
+ sign bit and forbids it otherwise, so `generateDevelopmentCertificate`
792
+ produced a certificate OpenSSL rejected as "illegal padding" whenever the
793
+ first random byte happened to be below 0x80. One run of one certificate would
794
+ have passed three times in five; there is now a test that generates
795
+ twenty-five.
796
+ - **`pkijs` cannot emit a distinguished name with separate relative names.** It
797
+ parses them into a flat list and re-encodes them as one multi-valued name, so
798
+ a generated certificate reads as `CN=… + OU=… + O=…` while Apple's reads as
799
+ three lines. The shape used in production is therefore the shape no fixture
800
+ can build — which is why `parseDistinguishedName` is its own module with its
801
+ own tests covering both renderings, rather than a private helper exercised
802
+ only by the fixture.
803
+
804
+ ## files 1.8.0
805
+
806
+ ### Added
807
+
808
+ - **A `./zip` subpath**, exporting `createZip` and its types on their own.
809
+
810
+ The writer was already here and already deterministic, which is what a
811
+ `.pkpass` needs — Apple's archive is checked by hash, so a second build of the
812
+ same content has to produce the same bytes. `@birtalanrobert/wallet` needs
813
+ exactly that and nothing else from this package: it takes pass assets as
814
+ bytes and never touches storage, because a package that signs a file for
815
+ somebody else's operating system should not also need an S3 client to be
816
+ tested.
817
+
818
+ Importing the root would have given it one. `index.ts` exports `S3Storage`
819
+ and the `StoredFile` entity, so `import { createZip } from
820
+ '@birtalanrobert/files'` loads the AWS SDK and TypeORM to write a zip. The
821
+ subpath is the same code with none of that in its import graph.
822
+
823
+ ## billing 3.0.0
824
+
825
+ ### Fixed
826
+
827
+ - **`reportUsage` reported nothing, every night, and said so.** It read
828
+ `mortar_usage_records` unbound, with a docblock explaining that a sweep is
829
+ about every tenant and the row itself says whose it is. The table carries
830
+ `FORCE ROW LEVEL SECURITY`, which applies to the table owner too — so the
831
+ `SELECT` returned no rows and reported success, and a night with unreported
832
+ usage was indistinguishable from a night with none.
833
+
834
+ Nothing anywhere would have said so. The method returns a count, the count was
835
+ zero, and zero is what a quiet night looks like. Found in project 10 while
836
+ building the metering that calls it.
837
+
838
+ ### Changed
839
+
840
+ - **`reportUsage(tenants, before?)` now takes the tenants to sweep**, and this is
841
+ a breaking change rather than an optional parameter on purpose: an optional one
842
+ would leave the silent version reachable, and the silent version is the whole
843
+ defect.
844
+
845
+ There is no way for this package to enumerate tenants for itself — the only
846
+ table it could read them from is the one behind the policy. Which is the right
847
+ answer anyway: the product owns the register of who exists, and this owns what
848
+ they used. It is the same shape every sweep in this programme has ended up
849
+ with.
850
+
851
+ - The `UPDATE` marking a row reported is bound too, for the same reason: without
852
+ it, it matches nothing and the same day is reported again on the next sweep.
853
+
854
+ ### Added
855
+
856
+ - `usage.integration.test.ts`, against a real PostgreSQL with the real
857
+ migration. Every unit test passed throughout the defect's life, because a
858
+ policy needs a database to bite — and `synchronize` does not create one.
859
+
860
+ ## workflow 1.4.0
861
+
862
+ ### Added
863
+
864
+ - **`PublicLinkService` and a 37-character signed link**, for links that are
865
+ printed, scanned or sent in a text message.
866
+
867
+ `signLink` carries its claims inside the token, which is the right trade for
868
+ an email: a forgery is rejected with no database involved. It costs length —
869
+ a subject, a tenant, an expiry and a token id, as JSON, in base64, with a
870
+ SHA-256 signature, comes to **over three hundred characters**. Project 10
871
+ measured what that does to the thing the product is bought for: its "ready for
872
+ collection" SMS came to **seven segments**, of which the URL was five, against
873
+ a specification that says one. As a QR code on a shop window the same token is
874
+ a dense square a phone reads badly across a counter.
875
+
876
+ So where a link has to be short, the claims move to a row and the token
877
+ becomes a handle plus a truncated signature: `1` + 22 base64url characters of
878
+ random handle + 14 of tag. The 128-bit handle is what makes it unguessable;
879
+ the tag is what lets a crawler walking `/s/<rubbish>` be refused by an HMAC
880
+ instead of a query. The cost is one indexed lookup, which the page was making
881
+ anyway — it has to read the subject to render it, and revocation is a row
882
+ whichever format is used.
883
+
884
+ Neither format supersedes the other. They trade length against a round trip,
885
+ in opposite directions, and both say so in their own docblocks.
886
+
887
+ - **`mortar_public_link` carries no row-level security policy, deliberately.** A
888
+ handle arrives from a stranger with a URL and nothing else — no session, no
889
+ tenant, nothing a policy could bind to — so the lookup that turns it into a
890
+ tenant cannot itself require one. This is the fourth time in this programme
891
+ that a public handle has had to live in an unpolicied table, after `tenants`
892
+ and project 10's `intake_slug`; the row holds no secrets, and the caller binds
893
+ the tenant it returns before reading anything.
894
+
895
+ - `mintHandle`, `signHandle`, `verifyHandle`, `isHandle`, `LINK_TOKEN_LENGTH`
896
+ and `LINK_HANDLE_LENGTH` on the pure entry point, so a Next.js server
897
+ component can reject a bad token before opening a connection.
898
+
899
+ ### Changed
900
+
901
+ - The base64url, HMAC and constant-time comparison helpers moved to
902
+ `links/encoding.ts` and are shared by both token formats. A second
903
+ `toBase64Url` is a second opinion about what bytes a signature covers, and two
904
+ such opinions produce signatures that verify inconsistently — months later,
905
+ for one customer, on one link.
906
+
907
+ ## messaging 1.2.0
908
+
909
+ ### Added
910
+
911
+ - **`transliterateToGsm`**, the other half of `countSegments`.
912
+
913
+ `countSegments` names the characters that forced the expensive encoding.
914
+ Naming them is not much use when they are in the shop's own name: one `ă` in
915
+ `Cofetăria Mierla` moves every message that shop ever sends into UCS-2 and
916
+ cuts capacity from 160 characters to 70, and the shop cannot rename itself.
917
+
918
+ Two rules, and both matter. **Marks that are already in the GSM alphabet are
919
+ left alone** — `é`, `ü`, `à`, `ñ` and `Ö` cost one place each, and "strip every
920
+ accent" damages a French or German name to save nothing. **Nothing is ever
921
+ silently dropped**: Greek, Cyrillic and ideographs have no faithful Latin
922
+ equivalent, so they are kept and _reported_, and the product can say "this
923
+ still costs double" rather than claim a saving it did not make.
924
+
925
+ It is deliberately not applied on anyone's behalf. A business's name is
926
+ theirs; the product offers the transliteration and shows what it saves.
927
+
928
+ - `isGsmCharacter`, exported so transliteration asks exactly the question the
929
+ counter asks. Two definitions of the alphabet would mean a firm shown one
930
+ number and billed against another.
931
+
932
+ ## workflow 1.3.1
933
+
934
+ ### Documented
935
+
936
+ - **`occurred_at` must default to `clock_timestamp()`, not `now()`** — and the
937
+ base entity now says so where somebody writing a `CREATE TABLE` will read it.
938
+ In PostgreSQL `now()` is the _transaction's_ start time, identical for every
939
+ row written inside it, and products record two moves in one transaction on
940
+ ordinary paths: an order taken across a counter is opened and confirmed in one
941
+ breath. Both rows then carry the same microsecond, and `reverse` — which finds
942
+ the last move with `ORDER BY occurred_at DESC, id DESC` — is left choosing
943
+ between two random UUIDs. It undoes one of them, and says nothing.
944
+
945
+ Found in project 10, where an order's history displayed out of order one run
946
+ in two. Every product that wrote its own transition table used `now()`;
947
+ `dossier` and `workbench` still do.
948
+
949
+ - **No guard was added, and that is deliberate.** One was written and removed.
950
+ TypeORM hands `occurredAt` back as a JavaScript `Date`, which is
951
+ millisecond-precision, so comparing two of them reports a tie for rows that
952
+ are genuinely microseconds apart — it refused reversals that were perfectly
953
+ well-defined and broke two of this package's own passing tests. Turning a rare
954
+ silent fault into a frequent loud one is not an improvement. Detecting it
955
+ honestly needs the comparison done in SQL, or an insertion-order column on the
956
+ table, and the second is a schema change for every consumer.
957
+
958
+ ## csv 1.3.0
959
+
960
+ ### Added
961
+
962
+ - **`readXlsx` and `sheetsIn` on `@birtalanrobert/csv/xlsx`** — the reading half
963
+ of a subpath that could previously only write. Project 04 takes a
964
+ distributor's catalogue in whatever shape their system exports, and a workbook
965
+ is one of the five shapes its format layer has to cover; projects 03, 05, 07,
966
+ 08, 09, 10 and 12 all import a spreadsheet the customer already has, because
967
+ re-keying it by hand at signup is where a trial dies.
968
+ - **Every cell comes back a string, and that is the contract.** A workbook
969
+ stores a guess about what each cell _is_, made by whichever program wrote it
970
+ under whichever locale — so a price arriving as the number `1234.56` has
971
+ already been read under a convention nobody declared, and a reference of
972
+ `0042` has already become forty-two. Handing over what is written lets that
973
+ decision be made once, explicitly, by a mapping that can be previewed. The one
974
+ exception is a date cell, which holds a serial number and has no text to hand
975
+ over; it comes back as `YYYY-MM-DD`, the format no locale reinterprets.
976
+ - `read-excel-file`, by the same author as the writer, both MIT and five MIT
977
+ packages between them. The note in this file's own source about ExcelJS still
978
+ stands: its reading path reaches `unzipper` → `binary` → `buffers`, which
979
+ declares no licence at all.
980
+
981
+ ## quantity 1.0.0
982
+
983
+ ### Added
984
+
985
+ - **`@birtalanrobert/quantity` — a decimal quantity with a unit, and an explicit
986
+ factor table.** The extraction project 03's deferred register has been holding
987
+ for project 04, and designed from both rather than from the one in hand. 03
988
+ converts _between_ dimensions using per-ingredient physics — a litre of oil is
989
+ not a kilogram of oil, and one onion is 150 grams because somebody typed that
990
+ in. 04 needs a factor chain _within_ one dimension: a case is four trays and a
991
+ tray is six bottles. Read side by side the shared part is small and exact — a
992
+ quantity that carries its unit, a factor table, conversion to and from a base,
993
+ and a refusal that is a sentence rather than a stack trace.
994
+ - **The table resolves at definition time**, which is the design rather than an
995
+ optimisation: every factor is absolute before anybody can read one, so a cycle
996
+ is impossible by construction and a pallet converts to a bottle in one
997
+ multiplication rather than by walking the chain and rounding at each step. A
998
+ cycle is refused where it is defined, naming the loop —
999
+ `"case" is defined in terms of itself: case → tray → case.`
1000
+ - **A unit worth nothing is refused**, because dividing by it surfaces as
1001
+ `Infinity` inside a total rather than as an error anybody can trace back.
1002
+ - **The decimal configuration every consumer shares** — 34 significant digits,
1003
+ half-up, no exponential notation — with `decimal.js` as a _peer_ dependency,
1004
+ because the configuration is global to the module instance a bundler resolved
1005
+ and a second copy silently gets its own defaults. That is the trap that once
1006
+ registered the forint in the wrong copy and rendered every HUF price a
1007
+ hundredth of its value.
1008
+ - `isMoreThanZero`, because **`Decimal.isPositive()` is true for zero** and
1009
+ every caller in this programme that wrote it meant "is there any of it".
1010
+
1011
+ **Deliberately not in it:** densities and piece weights (03's, and meaningless
1012
+ to a wholesaler), minimum order quantities and increments (04's, and meaningless
1013
+ to a kitchen), and anything that knows what a product is.
1014
+
1015
+ ## csv 1.2.0
1016
+
1017
+ ### Added
1018
+
1019
+ - **`@birtalanrobert/csv/mapping` — turning the columns somebody else's system
1020
+ wrote into the fields a product understands.** Four specifications ask for the
1021
+ same thing in four different words: a price-list wizard (03), a payroll export
1022
+ profile saved per bookkeeper (07), a spreadsheet import of years of candidates
1023
+ (08), and an ERP feed whose layout is configured rather than coded (04). The
1024
+ first consumer was not a specification but 764 lines of working code in
1025
+ project 03, which is the strongest position this library has extracted from.
1026
+ - **A field specification that explains itself**, because the sentence beside a
1027
+ dropdown is shown to a support person who has never seen this file before —
1028
+ and the same sentence is quoted back by a refusal:
1029
+ `Say which column holds the price per pack.`
1030
+ - **Conventions are configuration and never guessed.** `1.234,56` and
1031
+ `1,234.56` are the same number under two conventions, both arrive from the
1032
+ same country, and read under the wrong one a price is wrong by a factor of a
1033
+ thousand **while passing every validation a sensible person writes**. The
1034
+ thousands separator is stripped before the decimal one, which is the order
1035
+ that does not turn `1.234,56` into `1.234.56`.
1036
+ - **A decimal comes back as a string**, never a number. A float here is the bug
1037
+ the module exists to avoid.
1038
+ - **A problem carries the line it was on**, counting the header as line 1, so a
1039
+ report says "row 1 842" and somebody can go and look at row 1 842. Products
1040
+ add their own through `row.reject`, so "no product has that code" and "that is
1041
+ not a number" arrive in one list.
1042
+ - Booleans in the conventions these markets actually write — `da/nu`,
1043
+ `igen/nem`, `Y/N`, `1/0` — and per-file overrides where a system has its own.
1044
+ Dates in ISO, `DD.MM.YYYY`, `DD/MM/YYYY`, `MM/DD/YYYY` and `YYYYMMDD`, each
1045
+ round-tripped so that `31.02.2026` is refused rather than accepted for
1046
+ matching the shape.
1047
+ - A trailing minus and a parenthesised negative, both unambiguous, both written
1048
+ by older systems, and both otherwise rejected as "not a number".
1049
+
1050
+ ## files 1.5.0
1051
+
1052
+ ### Added
1053
+
1054
+ - **`@birtalanrobert/files/print` — print-ready cards with a QR code on each.**
1055
+ The artefact that makes a product exist in a shop: a venue that has signed up
1056
+ and not printed its table tents has not started, and "design twenty cards with
1057
+ a different code on each" is the step where they stop. Three of the seventeen
1058
+ specifications ask for exactly this — project 11's table tents and counter
1059
+ posters, project 06's enrolment posters in A4, A5 and tent, project 01's
1060
+ ticket with a code on it — so it is designed from all three rather than from
1061
+ the one in hand: a code, a heading, a caption and a footnote laid out on paper
1062
+ that folds and cuts where the marks say. **What any of them say is the
1063
+ product's**, on the same line the conventions already draw around notification
1064
+ templates: the machinery is shared, the content is not.
1065
+
1066
+ Three things in it are decisions rather than details:
1067
+
1068
+ - **The code is drawn as vectors, not as an image.** Merged into horizontal
1069
+ runs it is a couple of hundred rectangles rather than a thousand, and it
1070
+ stays sharp at whatever resolution the printer has. A rasterised code scaled
1071
+ to a 60 mm square on a 1200 dpi printer is a blurred one, and a blurred code
1072
+ at the third attempt is a guest who gives up and asks for a menu.
1073
+ - **A tent is one sheet with the card on it twice, the upper half turned
1074
+ through half a turn.** Folded, the two faces read from both sides of the
1075
+ table. Printed the obvious way one of them is upside down, and the venue
1076
+ finds out after printing twenty.
1077
+ - **The typeface is a required argument.** PDF's built-in fonts are
1078
+ WinAnsi-encoded, which has no `ș`, no `ț` and no `ő` — a default would work
1079
+ in development and throw on the first Romanian venue name.
1080
+
1081
+ The font is embedded **whole**: `@pdf-lib/fontkit`'s subsetter drops glyphs
1082
+ from ordinary static TrueType fonts, so `Masa 12` prints as `M 2` while the
1083
+ text layer still reads `Masa 12` — it copies, searches and extracts correctly
1084
+ and is wrong only on paper. Nothing that counts pages sees it. The cost is the
1085
+ typeface once per document rather than once per card.
1086
+
1087
+ Adds `qrcode` and `@pdf-lib/fontkit`, both behind the `./print` subpath so a
1088
+ consumer that only uploads files never loads either.
1089
+
1090
+ ## realtime 2.0.0
1091
+
1092
+ ### Changed
1093
+
1094
+ - **`RealtimeSocketServer` moved to `@birtalanrobert/realtime/nestjs/socket`.**
1095
+ It is the only thing in the package that needs `ws`, and importing a barrel
1096
+ loads everything in it — so a process that publishes and holds no sockets was
1097
+ made to install a WebSocket library to reach a Redis backlog, which is exactly
1098
+ what declaring `ws` an optional peer was meant to avoid. Found the moment a
1099
+ second publisher appeared: project 11's worker releases a held course when its
1100
+ timer runs out, publishes it into the same backlog the API serves, and could
1101
+ not boot.
1102
+
1103
+ One import to change per gateway process; nothing else moves.
1104
+
1105
+ ### Fixed
1106
+
1107
+ - **A process no longer receives its own broadcast.** Redis pub/sub delivers to
1108
+ every subscriber on the channel, the sender included, so a gateway that both
1109
+ sends and listens — which is every replica — handed each of its own events to
1110
+ its sockets twice: once locally, once on the way back. Clients survived it,
1111
+ because a repeated sequence number is `skip`; what nobody would have noticed
1112
+ is that every screen in the building was being sent twice what it needed, over
1113
+ venue wifi, on a tablet. Each message now carries the id of the process that
1114
+ sent it and is dropped on arrival at that same process.
1115
+
1116
+ The id is generated rather than configured, deliberately: it exists only to
1117
+ recognise a message coming back, and a value an operator could set is a value
1118
+ two replicas can be given identically — which would make each drop the other's
1119
+ events, the one failure the fan-out exists to prevent.
1120
+
1121
+ ## printing 1.0.1
1122
+
1123
+ ### Fixed
1124
+
1125
+ - **A queued job's callback now belongs to that job.** `enqueue`'s `onDone` was
1126
+ held by the drain loop rather than by the job, so anything queued while the
1127
+ printer was busy had its outcome handed to the _previous_ job's caller. One
1128
+ routed order is three tickets queued in a row, which made this the normal case
1129
+ rather than a race: a product recording "printed" then recorded it against the
1130
+ wrong ticket, and the one that actually failed was marked as fine — worse than
1131
+ recording nothing, because the whole point of the callback is knowing which
1132
+ ticket has no paper.
1133
+
1134
+ - **One caller's callback throwing no longer stops the queue.** The remaining
1135
+ tickets sat in a queue that had quietly stopped draining, which is the harder
1136
+ failure to notice: nothing errored, and a kitchen simply received less than it
1137
+ was sent.
1138
+
1139
+ - **`wrap` no longer eats a leading indent.** Splitting on spaces turned
1140
+ `' Extra sauce'` into three empty words that were discarded, so an indented
1141
+ block printed flush left. That indent is not decoration: it is what separates
1142
+ a modifier from the _next_ dish's name on a kitchen ticket, and without it a
1143
+ cook reads "no onions" as belonging to the wrong plate. The indent is now kept
1144
+ and re-applied to continuation lines, so a modifier long enough to wrap stays
1145
+ under its dish instead of sliding back to the margin.
1146
+
1147
+ ## printing 1.0.0
1148
+
1149
+ ### Added
1150
+
1151
+ - **`@birtalanrobert/printing`** — ESC/POS rendering, a raw port 9100 transport,
1152
+ a retrying queue that escalates, and a printer that keeps what it was sent.
1153
+ Three products need paper: project 11, where it is a **v1 requirement**
1154
+ because a meaningful share of prospects will not buy without it; project 12's
1155
+ intake receipt; and project 01's box-office stock. The layout stays in each
1156
+ product — a kitchen ticket and a repair receipt share nothing but the wire.
1157
+
1158
+ `Ticket` makes four mistakes impossible, each of which has printed wrong in
1159
+ somebody's kitchen. **The printer is initialised first**, because it holds
1160
+ whatever the last ticket left it in — the classic symptom being every ticket
1161
+ after a heading printing double height until somebody power-cycles it.
1162
+ **Styles are turned off again.** **`cut()` feeds the paper past the blade
1163
+ first**, since the blade sits above the print head and a cut without a feed
1164
+ takes the last three items with it. And **accented text is encoded for the
1165
+ printer's own character table**, defaulting to Windows-1250: the American
1166
+ table most printers boot into turns "Ciorbă de burtă" into something a cook
1167
+ misreads at a glance. Text wraps rather than truncating, because the end of a
1168
+ line on a kitchen ticket is where the modifiers are.
1169
+
1170
+ `send` resolving means the bytes left this machine and nothing more — raw port
1171
+ 9100 has no acknowledgement, and a product reading "printed" as "on paper" is
1172
+ reading something the protocol never says.
1173
+
1174
+ `PrintQueue` retries three times, prints serially (two jobs at once interleave
1175
+ into one ticket with half of each on it), and **escalates**: `onFailure` is
1176
+ not decoration, because a queue that swallows a failure is a kitchen with no
1177
+ ticket that never learns it has none. A printer that does not exist fails
1178
+ immediately — a configuration mistake answers the same way every time.
1179
+
1180
+ Deliberately **not** BullMQ. A print job is worthless a minute after it was
1181
+ created, so it lives in memory beside the process that made it; after a
1182
+ restart what a kitchen needs is the _current_ tickets, which the display and
1183
+ the database already have.
1184
+
1185
+ ## realtime 1.1.0
1186
+
1187
+ ### Added
1188
+
1189
+ - **`@birtalanrobert/realtime/nestjs`** — the server half: a WebSocket server, a
1190
+ Redis-backed backlog, a Redis fan-out between gateway processes, a polling
1191
+ handler and a Nest module.
1192
+
1193
+ `RedisBacklog` assigns the sequence number and stores the event **in one Lua
1194
+ script**, because two round trips can come apart: a process that dies between
1195
+ `INCR` and `ZADD` has handed out 413 and stored nothing, and no later care can
1196
+ fill that hole — every client that reaches it is told the backlog starts at
1197
+ 414 and reloads, for ever, for an event that never existed.
1198
+
1199
+ `RedisBroadcast` is fire-and-forget, and that is acceptable _here and nowhere
1200
+ else_: pub/sub does not deliver to a process that is not connected, but the
1201
+ event is already durable, so a process that missed the broadcast serves it
1202
+ from the resume the moment any client asks. The socket is the fast path; the
1203
+ backlog is the truth.
1204
+
1205
+ `RealtimeSocketServer` sends a client's **replay before its welcome**. The
1206
+ welcome says where a channel stands; sending it first would let a client with
1207
+ no position adopt the latest sequence and then reject the replay it is about
1208
+ to receive as already seen.
1209
+
1210
+ Authorisation is the product's, and returns a _subset_ rather than a boolean —
1211
+ a display asking for two stations it may see and one it may not gets the two,
1212
+ rather than a connection that fails for a reason nobody can see.
1213
+
1214
+ Nine integration tests against a real socket and a real Redis, including
1215
+ twenty concurrent publishes asserting the numbers are 1 to 20 with nothing
1216
+ repeated and nothing skipped.
1217
+
1218
+ ## redis 1.0.2
1219
+
1220
+ ### Fixed
1221
+
1222
+ - **`flushTestRedis` deleted nothing.** `SCAN` returns keys with the client's
1223
+ prefix already on them, and every write through that client _adds_ the prefix
1224
+ — so passing the scanned keys straight to `DEL` removed `prefix:prefix:key`,
1225
+ which exists nowhere. The prefix is now stripped before deleting.
1226
+
1227
+ Nothing failed, which is the point: suites using it shared state between
1228
+ tests and passed anyway, until one counted something and found five events
1229
+ where it had published four. There is now a test that sets a key, flushes, and
1230
+ asserts it is gone.
1231
+
1232
+ ## realtime 1.0.0
1233
+
1234
+ ### Added
1235
+
1236
+ - **`@birtalanrobert/realtime`** — channels with sequence numbers, gap
1237
+ detection, resume, bidirectional heartbeats and a polling fallback that is
1238
+ built and tested rather than described. Five specifications call for a live
1239
+ connection — project 11's kitchen displays and staff app, and the four game
1240
+ clients in 14 to 17 — so it is designed from those five rather than from the
1241
+ one in hand.
1242
+
1243
+ **What makes it a package is gap detection, not socket handling.** A client
1244
+ that can say "I last saw 412" and be told what it missed is the difference
1245
+ between _probably fine_ and _provably complete_; a display quietly one ticket
1246
+ behind looks exactly like a kitchen with no orders.
1247
+
1248
+ Three decisions carry the guarantees. **Sequence numbers are per channel**: a
1249
+ global counter would make every subscriber's gap detection depend on traffic
1250
+ it cannot see, so a kitchen display would think it had missed the messages a
1251
+ guest's phone received. **Publishing is append then fan out**, in that order —
1252
+ a subscriber told before the event was durable learns about something a
1253
+ reconnecting client could not be given. And **the backlog is bounded**, so it
1254
+ can fail to answer: a client that was away too long is sent a `gap` frame and
1255
+ reloads, rather than a partial replay that looks complete.
1256
+
1257
+ The **polling fallback starts immediately** when a socket will not open, with
1258
+ the socket retried behind it — a venue whose network eats WebSockets gets a
1259
+ working display rather than a spinner and an exponential backoff. It speaks
1260
+ the same protocol and is answered by the same `resume`, which is what keeps it
1261
+ a fallback rather than a second implementation that differs on the day it is
1262
+ needed.
1263
+
1264
+ The root entry is pure — wire format, gap logic, client — because four browser
1265
+ bundles import it.
1266
+
1267
+ Authorisation, acknowledgement, presence and moderation are deliberately
1268
+ absent: who may subscribe to a channel is the product's decision, and whether
1269
+ a ticket was _acted on_ is a row in its database rather than a frame on a
1270
+ socket.
1271
+
1272
+ ## files 1.4.0
1273
+
1274
+ ### Added
1275
+
1276
+ - **`PutOptions.cacheControl`**, honoured by `S3Storage` on both `put` and
1277
+ `presignUpload` and recorded by `MemoryStorage` (readable through a new
1278
+ `optionsFor(key)`, so a test can assert what an object will be served under).
1279
+
1280
+ The header belongs on the object because the thing that serves it is a CDN the
1281
+ application never speaks to. A rendered derivative's key names its content, so
1282
+ it is immutable and deserves `public, max-age=31536000, immutable` — and
1283
+ without it a guest's phone re-downloads every photograph on a menu on their
1284
+ second visit, which is the whole budget the image pipeline was added to
1285
+ protect. On a presigned upload it is signed in, so it appears in `headers` and
1286
+ a browser that omits it gets a signature mismatch rather than an object
1287
+ quietly missing the header.
1288
+
1289
+ Never set it on an original somebody uploaded: that key can be reused.
1290
+
1291
+ ### Fixed
1292
+
1293
+ - **A presigned upload carrying metadata was refused by S3.** `presignUpload`
1294
+ returned the metadata as `x-amz-meta-*` headers _as well as_ letting the SDK
1295
+ encode it into the query string, and a presigned PUT is rejected outright if
1296
+ it carries an `x-amz-*` header the signature does not cover: "there were
1297
+ headers present in the request which were not signed". Every product's direct
1298
+ upload sets metadata — the tenant and the scope, so an operator can read a
1299
+ bucket during an incident without a database — so every one of them was
1300
+ broken. The returned headers are now exactly the ones the browser must send:
1301
+ `content-type`, `cache-control` (both applied only when sent) and
1302
+ `content-disposition` (signed as a header, so omitting it breaks the
1303
+ signature). The metadata still arrives; it is in the URL.
1304
+
1305
+ The failure had no witness. It happens on the one request the API is not part
1306
+ of, so the only symptom is a file that never appears.
1307
+
1308
+ - **The SDK signed a checksum of a body it had not seen.** Since v3.729 the AWS
1309
+ SDK computes a CRC32 for every upload by default, including one it is only
1310
+ presigning — so `x-amz-checksum-crc32` for an _empty_ body went into the URL
1311
+ and S3 rejected the browser's bytes for not hashing to it. `S3Storage` now
1312
+ sets `requestChecksumCalculation: 'WHEN_REQUIRED'`. MinIO ignores the
1313
+ mismatch, which is the worst version of this: the development stack works and
1314
+ the deployment does not.
1315
+
1316
+ ## files 1.3.0
1317
+
1318
+ ### Added
1319
+
1320
+ - **`@birtalanrobert/files/images`** — the image pipeline eight of the seventeen
1321
+ specifications call for, behind a subpath with `sharp` as an _optional_ peer
1322
+ dependency. A repair shop, a wedding microsite, a menu and a made-to-order
1323
+ workshop all receive photographs from people who are not thinking about the
1324
+ web, and none of them will ever be asked to resize anything.
1325
+
1326
+ `renderImage(bytes, { sizes, formats, placeholder })` returns a responsive set
1327
+ in modern formats, the displayed dimensions, a dominant colour and optionally
1328
+ a blurred `data:` URI. Sizes are named in the product's own vocabulary —
1329
+ `thumb`, `card`, `full` — because a stored key of `card` survives the day
1330
+ somebody decides cards are 720 wide and a key of `640` does not.
1331
+
1332
+ Four corrections are the reason this is one function rather than four calls at
1333
+ each consumer. **Orientation is applied and the returned dimensions are the
1334
+ turned ones**: a phone stores a portrait photograph as a landscape one plus an
1335
+ EXIF flag, and a page that reserves the stored shape produces exactly the
1336
+ layout shift the placeholder was added to prevent. **Metadata is dropped**,
1337
+ and the customer's front-door coordinates with it — sharp's default rather
1338
+ than a call, which is why there is a test asserting it. **Nothing is
1339
+ enlarged.** **A colour is measured** so the box is filled rather than white.
1340
+
1341
+ Input is identified by its bytes; anything that is not a raster photograph
1342
+ raises `UnsupportedImageError`. The refusal that matters is the SVG, which
1343
+ libvips will rasterise happily and which is a document format with a script
1344
+ engine in it. `maxPixels` guards decompression, because the failure mode of a
1345
+ bomb is a worker killed by the kernel rather than an error anybody sees.
1346
+
1347
+ Encoding is sequential on purpose: sharp threads each operation already, so
1348
+ six at once finishes no sooner while holding six decoded images in memory.
1349
+
1350
+ ## workflow 1.3.0
1351
+
1352
+ ### Added
1353
+
1354
+ - **`appendOnlySql(table, { redactable })`** — columns a retention sweep may set
1355
+ to `NULL`, and nothing else. Append-only and a published retention schedule
1356
+ pull against each other: the row is evidence of when somebody clocked in and
1357
+ must never be rewritten, while one column of it — a location trace, a
1358
+ photograph, an IP address — is personal data with an expiry date printed in a
1359
+ document a regulator can read.
1360
+
1361
+ Without this the only way to keep that promise is to drop the trigger, sweep,
1362
+ and put it back: an operation performed under time pressure, on production,
1363
+ and sometimes forgotten halfway. So the erasure is narrowed instead of the
1364
+ protection being removed. An update passes only when every named column ends
1365
+ up `NULL` and every other column is exactly what it was; nulling an
1366
+ already-null column is fine, because an hourly sweep re-reaches rows it has
1367
+ already cleared and an error there is a worker restarting rather than a
1368
+ promise being kept.
1369
+
1370
+ Tables that name no redactable column keep the function they had, character
1371
+ for character.
1372
+
1373
+ **The order of the two checks inside the trigger is the error message rather
1374
+ than the outcome.** Checked the other way round, an ordinary rewrite of an
1375
+ unrelated column is still refused — but refused for leaving the _location_
1376
+ untouched, which sends whoever reads the log looking at the wrong column
1377
+ entirely. The rest of the row is compared first.
1378
+
1379
+ ## context 1.2.0
1380
+
1381
+ ### Added
1382
+
1383
+ - **`device` as an actor type.** Hardware that acts on its own credential —
1384
+ a kitchen display acknowledging a ticket, a tablet by a door recording a
1385
+ clock-in — was previously recorded as `client`, alongside the guest whose
1386
+ phone is in the same room. That is the one distinction the audit trail cannot
1387
+ afford to lose: "which display acknowledged this order?" is the first question
1388
+ asked when a table waits forty minutes, and an answer of "a client" does not
1389
+ separate the screen at the pass from the guest's own handset.
1390
+
1391
+ Additive: `client` still means what it meant, and nothing has to move.
1392
+
1393
+ ## comms 1.9.1
1394
+
1395
+ ### Changed
1396
+
1397
+ - **`WebPushMessagePort` now takes a `DataSource` rather than a `keysFor`
1398
+ callback.** The callback had to be `CommsService.pushKeysFor`, and that cannot
1399
+ be built at the moment the module which _provides_ `CommsService` is being
1400
+ configured — every consumer would hit the same circle, and Nest's message for
1401
+ it names neither the port nor the reason.
1402
+
1403
+ `mortar_push_subscription` is this package's own table, so the port reading it
1404
+ is not a layer being crossed. It still learns nothing about _who_ is
1405
+ subscribed: an endpoint and the two keys for it is all a transport should
1406
+ know, and all it gets.
1407
+
1408
+ ## comms 1.9.0
1409
+
1410
+ ### Added
1411
+
1412
+ - **Web push**, as a channel and the subscriptions it needs. Three of the
1413
+ seventeen specifications send to a PWA (02, 04, 07), and every one of them
1414
+ would otherwise reimplement the same thing: registering an endpoint,
1415
+ de-duplicating it, and — the part that goes wrong — **deleting it on 410
1416
+ Gone**. A browser that has revoked permission answers that for ever, and a
1417
+ product still trying has delivery figures that are quietly meaningless.
1418
+
1419
+ `WebPushMessagePort` is a transport and knows nothing about who is subscribed:
1420
+ `CommsService` owns `mortar_push_subscription` and hands the port a resolver.
1421
+ `subject` names the relationship rather than one product's noun — an employee
1422
+ here, a customer there — because a foreign key to either is exactly what would
1423
+ stop the table being shared.
1424
+
1425
+ `PushSubscriptionGone` is raised for 404 and 410 and for an endpoint with no
1426
+ keys, so a caller has one behaviour to reason about rather than three. A 503
1427
+ is an ordinary failure and does **not** delete anything: a push service having
1428
+ a bad minute is not a person revoking permission.
1429
+
1430
+ `OutboundMessage.url` carries where a tap should land. In the encrypted
1431
+ payload rather than a header, because it is the service worker that decides
1432
+ what a notification does and it reads the payload.
1433
+
1434
+ ### Changed
1435
+
1436
+ - **`MessageLog.channel` and `Suppression.channel` now import `Channel`** rather
1437
+ than spelling the union out. It was written in three places that had to move
1438
+ together with no compiler check that they did — and adding a member is already
1439
+ a schema change in disguise without also being a search.
1440
+
1441
+ ## csv 1.1.1
1442
+
1443
+ ### Changed
1444
+
1445
+ - **`toXlsx` now writes with `write-excel-file` rather than ExcelJS**, and the
1446
+ reason is a licence rather than a feature. ExcelJS reaches an _unlicensed_
1447
+ transitive dependency through its **reading** path — `unzipper` → `binary` →
1448
+ `buffers`, and `buffers@0.1.1` declares no licence anywhere, which under
1449
+ copyright means no rights granted. A product that ships cannot rely on code
1450
+ nobody has granted rights to, and `pnpm licenses:check` in project 07 refused
1451
+ it on exactly those grounds.
1452
+
1453
+ Writing is all this subpath does. The replacement writes and does not read,
1454
+ and its whole dependency tree is one MIT package — so the reading half was
1455
+ buying a licence problem for a capability with no consumer.
1456
+
1457
+ The interface is unchanged: same `toXlsx(rows, options)`, same contract that
1458
+ strings stay strings and numbers stay numbers.
1459
+
1460
+ ## csv 1.1.0
1461
+
1462
+ ### Added
1463
+
1464
+ - **`@birtalanrobert/csv/xlsx`** — writing the `.xlsx` a bookkeeper opens. Four
1465
+ of the seventeen specifications ask for Excel _beside_ CSV (03, 04, 05, 07)
1466
+ and the reason is always the same: a CSV opened in Excel is reinterpreted on
1467
+ the way in. An employee reference of `0042` becomes the number 42, and `7,50`
1468
+ becomes either seven and a half or the text "7,50" depending on a setting
1469
+ nobody in the business can find. An `.xlsx` says what each cell is.
1470
+
1471
+ **Behind a subpath, deliberately.** The CSV side of this package is
1472
+ browser-safe and small; a spreadsheet writer is neither, and a console
1473
+ counting segments on every keystroke must not be shipping a zip library to do
1474
+ it. Only what needs Excel pays for Excel.
1475
+
1476
+ Strings stay strings and numbers stay numbers, which is the whole contract:
1477
+ the caller decides, because a reference is text and a column somebody wants to
1478
+ sum is not.
1479
+
1480
+ ### Changed
1481
+
1482
+ - The package description now says _tabular files_ rather than _CSV files_. The
1483
+ name stays `csv`, because renaming a published package costs every consumer a
1484
+ change for no gain.
1485
+
1486
+ ## auth 1.2.0
1487
+
1488
+ ### Added
1489
+
1490
+ - **`Pbkdf2Hasher`**, for a secret a **browser** must also verify. `ScryptHasher`
1491
+ remains the default and should stay it — scrypt is memory-hard and PBKDF2 is
1492
+ not — but the Web Crypto API implements PBKDF2 and does not implement scrypt.
1493
+ A surface that has to authenticate with no network therefore either verifies a
1494
+ PBKDF2 hash with the platform's own primitive, ships a JavaScript scrypt into
1495
+ every bundle, or holds a _second_ hash of the same secret in a second format.
1496
+ The third is the worst of the three: two representations of one secret is two
1497
+ things to keep in step, and the day they disagree is the day somebody cannot
1498
+ clock in.
1499
+
1500
+ Encoded as `pbkdf2$sha256$iterations$salt$hash`, so the parameters travel with
1501
+ the hash. Defaults to OWASP's current floor of 600,000 iterations, and refuses
1502
+ a truncated digest for the same reason scrypt's parser does — PBKDF2 is
1503
+ prefix-stable, so verifying at the stored length would let a one-byte digest
1504
+ match roughly one attempt in 256.
1505
+
1506
+ Use it only where offline verification is the requirement, and only for
1507
+ secrets whose real protection is something else: a rate limit, a locked
1508
+ cabinet, a short lifetime. For an account password, use scrypt.
1509
+
1510
+ ## comms 1.8.0
1511
+
1512
+ ### Added
1513
+
1514
+ - **`CommsService.serves(channel)`** — whether anything is configured that could
1515
+ carry a channel. For callers holding a genuine choice: somebody with both a
1516
+ mobile number and an email address, on a deployment that has SMTP and no text
1517
+ provider, should receive an email rather than a recorded failure, and the
1518
+ caller cannot know which without asking. The ports are the deployment's
1519
+ business rather than the product's, and until now they were private.
1520
+
1521
+ It is deliberately not a promise of delivery and not a way around `send`'s
1522
+ honesty: a caller with only one address still sends on it and still gets the
1523
+ failure recorded, because "we tried and there was no way to reach them" is a
1524
+ fact a business needs.
1525
+
1526
+ ## clock 1.0.0
1527
+
1528
+ ### Added
1529
+
1530
+ - **Wall-clock and interval arithmetic across time zones**, extracted from
1531
+ project 02's slot engine at its second consumer — project 07's rota. Two
1532
+ sentences account for most scheduling bugs in this catalogue and both are in
1533
+ here: **a date is not an instant** — "Tuesday the fourteenth" is a different
1534
+ span of real time in Bucharest and Budapest — and **a duration is not a
1535
+ difference of wall clocks**, because a 22:00–06:00 shift is seven hours on the
1536
+ spring-forward night and nine on the autumn one.
1537
+ - Extracted rather than copied because six of the seventeen specifications
1538
+ schedule against somebody's local wall clock, and a second copy of DST
1539
+ arithmetic is one that goes subtly wrong in a single place. What stays in each
1540
+ project is the engine built _on_ it — the slot engine, the scheduling rules
1541
+ engine, the costing engine — which share this substrate and no logic.
1542
+ - `offsetMinutesAt` reads `Intl` rather than a table, so the tz database is the
1543
+ runtime's. `toInstant` reports `gap` and `ambiguous` rather than hiding them.
1544
+ `MinuteOfDay` may exceed 1440, which is what makes an overnight span one
1545
+ interval. Project 02's 102 tests came with it.
1546
+
1547
+ ## comms 1.7.0
1548
+
1549
+ ### Added
1550
+
1551
+ - **WhatsApp**, as a transport beside SMS and SMTP — four of the seventeen
1552
+ specifications want it and none of them wants a second implementation.
1553
+ `WhatsAppMessagePort` sends through Twilio, which the products that want this
1554
+ already hold credentials for and which already models the two things that
1555
+ make WhatsApp _not_ a third kind of text message: outside twenty-four hours
1556
+ of the customer's own last message only a **template Meta approved in
1557
+ advance** may be sent, and it is billed per conversation rather than per
1558
+ segment.
1559
+ - `OutboundMessage.template` carries that approved template and its variables.
1560
+ `text` is still required and still logged: what a business needs to read back
1561
+ months later is the words that went out, not a pointer to a template that has
1562
+ since been edited.
1563
+ - `FallbackMessagePort` tries one channel and then another, on the **two**
1564
+ refusals that mean "this channel will never work for this person" — a number
1565
+ that is not on WhatsApp, and a closed window with no template. Everything else
1566
+ is raised, because silently sending every message by SMS when a token expired
1567
+ is a bill nobody expected and a fault nobody saw.
1568
+ - `AllowWhatsApp` widens the channel constraints on the message log **and the
1569
+ suppression list**. The type and the constraint move together: adding a member
1570
+ to a union lets the code compile and leaves the database refusing the row —
1571
+ the mistake that cost a release in `commerce` and is not repeated here.
1572
+ - `whatsAppEnvSchema` — the sender and the approved templates, read from the
1573
+ environment. Shared because more than one service reads them and they have to
1574
+ agree: the worker builds the port, and whatever surface a business configures
1575
+ its messages on has to know whether the channel exists before offering it.
1576
+ - `SendResult.channel` names what actually carried a message, and
1577
+ `MessagePort.fallbackChannel` declares where a port may divert to. The log
1578
+ records the first, so "sent on WhatsApp" is never the answer for something
1579
+ that went by SMS — a business reads that row when it asks why it was charged
1580
+ for texts.
1581
+
1582
+ ### Changed
1583
+
1584
+ - A suppression is honoured on the channel a port **may divert to** as well as
1585
+ the one addressed. Somebody who replied STOP to a text message has their
1586
+ refusal recorded against `sms`; addressing the same number on WhatsApp must
1587
+ not be a way round it, because nothing can promise which channel will carry
1588
+ it.
1589
+
1590
+ ## calendars 1.0.0
1591
+
1592
+ ### Added
1593
+
1594
+ - Two-way calendar sync for projects 02 and 08. **The rules come first**,
1595
+ because a one-way feed cannot corrupt the diary it exports and a sync can: the
1596
+ diary owns appointments and the external calendar holds a copy, the external
1597
+ calendar owns everything else and we never touch it, and disconnecting leaves
1598
+ every appointment exactly where it was. "Last write wins" is what this refuses
1599
+ to be.
1600
+ - `driftOf` recognises a copy somebody moved, renamed or deleted, and the next
1601
+ sync puts it back. `busyFrom` merges their events into the stretches of
1602
+ unavailable time a diary should hold — skipping our own copies, anything
1603
+ marked free, and cancelled events providers keep returning.
1604
+ - Adapters for Google Calendar and Microsoft Graph through the vendors' own
1605
+ clients, behind `/google` and `/microsoft` so a product using one does not
1606
+ install the other's SDK. Refresh tokens are sealed with AES-256-GCM.
1607
+ - Their events are never stored: read in a window, turned into busy periods,
1608
+ forgotten. And `pull` _returns_ those periods rather than writing into a
1609
+ product's own diary — a shared package with a foreign key into a product's
1610
+ tables is not a shared package.
1611
+
1612
+ ## database 1.1.0
1613
+
1614
+ ### Added
1615
+
1616
+ - `sealSecret` / `openSecret`: AES-256-GCM for a credential that has to live in
1617
+ a column — a third-party refresh token, a provider key. Authenticated, so a
1618
+ tampered value fails to open rather than decrypting to rubbish some code path
1619
+ then uses as a credential; versioned, so the algorithm can change without
1620
+ making old rows unreadable; and `sealingKey` refuses a key of the wrong length
1621
+ loudly, because a silently padded one works until the day it does not.
1622
+ - `secretsMatch`, for comparing a presented secret against a stored one in
1623
+ constant time.
1624
+
1625
+ ## vouchers 1.0.0
1626
+
1627
+ ### Added
1628
+
1629
+ - Stored value for projects 02 and 06: gift vouchers, prepaid packages and
1630
+ balances, as an **append-only ledger** rather than a counter. The balance is
1631
+ the sum of the entries; the `balance` column is a cache written in the same
1632
+ transaction with `CHECK (balance >= 0)` behind it, so overspending is
1633
+ impossible even when the application is wrong and the history still explains
1634
+ the number.
1635
+ - `money` and `units` are separate denominations on purpose — ten sessions and
1636
+ a thousand lei must never be added — and a package carries the `subject` it
1637
+ was sold for, so a course of physiotherapy cannot be spent on haircuts.
1638
+ - Expiry is **null by default**, because rules for stored value differ by
1639
+ jurisdiction and several treat an unused voucher as the customer's money for
1640
+ years. `expiryFrom` clamps a month's arithmetic to the end of a shorter month
1641
+ rather than rolling into the next one.
1642
+ - Codes in Crockford's base32, with its substitutions applied on the way in: a
1643
+ customer reading a code off a photograph is not told their voucher does not
1644
+ exist because they typed a letter O.
1645
+ - The root entry point is framework-free and browser-safe; entities, migration
1646
+ and `VouchersService` live behind `/nestjs`.
1647
+
1648
+ ## commerce 4.1.0
1649
+
1650
+ ### Added
1651
+
1652
+ - `voucher` as a payment kind: money taken for stored value, a gift card sold
1653
+ or a package bought. A _redemption_ is deliberately not a payment — selling a
1654
+ voucher brings money in and spending it later brings none, and counting both
1655
+ would tell a business it earned the same two hundred twice. Redemptions are
1656
+ entries in `@birtalanrobert/vouchers`' ledger instead.
1657
+
1658
+ ## commerce 1.0.0
1659
+
1660
+ ### Added
1661
+
1662
+ - Taking money on a business's behalf, for projects 01, 02 and 11: payout
1663
+ onboarding with a hard gate, card payments through Stripe Connect as
1664
+ destination charges, holds that are captured only by a human decision,
1665
+ manually recorded cash, terminal, voucher and transfer takings, partial
1666
+ refunds with a required reason, and webhook verification.
1667
+ - `depositFor` and `canTakeMoney` at the pure root entry point, because a
1668
+ console shows both while somebody drags a slider.
1669
+ - **We never hold anybody's funds** — the customer pays the business directly
1670
+ and our cut is an application fee. Everything in the package follows from it.
1671
+
1672
+ ## phone 1.0.0
1673
+
1674
+ ### Added
1675
+
1676
+ - Telephone numbers as the durable identity of a customer: `normalisePhone`,
1677
+ `formatPhone`, `dialable`, `isSearchablePhone`, for Romania and Hungary.
1678
+ Extracted from project 12 when project 02 needed the same matching, which is
1679
+ the second consumer the policy asks for.
1680
+ - **Three forms, and they are not interchangeable**: as typed, normalised (a
1681
+ search key) and dialable (E.164). Project 12 shipped a pumping check that
1682
+ refused a perfectly good normalised number for having no plus, which is what
1683
+ the distinction exists to prevent.
1684
+ - No dependencies and nothing framework-shaped, so a browser bundle can decide
1685
+ whether a lookup is worth making before the keystroke lands.
1686
+
1687
+ ## comms 1.3.0
1688
+
1689
+ ### Added
1690
+
1691
+ - `SmtpMessagePort`: email over SMTP, built on nodemailer. Every project's
1692
+ Compose file runs Mailpit and nothing could reach it, so an invitation, a
1693
+ receipt or a password reset could not be followed end to end on a developer's
1694
+ machine without a vendor account. It is a production transport too — a
1695
+ customer's own mail server, a relay offered instead of an API, a deployment
1696
+ where mail may not leave the building.
1697
+ - A recipient the server refuses after accepting the conversation is a failure
1698
+ rather than a success. `sendMail` resolves in that case, and recording it as
1699
+ sent writes "delivered" against a message the server explicitly refused.
1700
+ - Certificates are verified by default. `allowSelfSignedCertificate` is for a
1701
+ local catcher or a private relay, and named so nobody enables it casually.
1702
+ - Its tests run a real SMTP server in-process rather than mocking the client:
1703
+ neither the refused recipient nor the untrusted certificate can be asserted
1704
+ against a mock.
1705
+
1706
+ ## messaging 1.0.1
1707
+
1708
+ ### Fixed
1709
+
1710
+ - **The entity pointed at the wrong table.** `MessageCreditEntry` was mapped to
1711
+ `message_credits` while the migration creates `mortar_message_credits`, so
1712
+ every read through the service failed with `relation "public.message_credits"
1713
+ does not exist`. The migration and the raw SQL in the README were right; only
1714
+ the decorator was wrong, which is why the package's own build and typecheck
1715
+ had nothing to say about it.
1716
+
1717
+ ## messaging 1.0.0
1718
+
1719
+ Extracted from project 13 at its second consumer (project 12), which is the
1720
+ rule: written once, moved when a second product needs it.
1721
+
1722
+ ### Added
1723
+
1724
+ - **`countSegments`** — what a message actually costs, counted the way a
1725
+ provider counts rather than the way a person counts characters. A single
1726
+ character outside GSM 03.38 changes the encoding for the whole message and
1727
+ cuts capacity from 160 to 70, so `offenders` names the characters responsible:
1728
+ "your ș and ț are doubling the cost" is something a person can act on.
1729
+ - **Quiet hours** — `isQuiet`, `nextAllowed`, `localTime`, in the _business's_
1730
+ zone rather than the recipient's. A phone number says nothing about where
1731
+ somebody is sitting.
1732
+ - **`assessSmsRisk`** — pumping detection. Fraud that costs money rather than
1733
+ data, and visible only in the shape of recent traffic rather than in any one
1734
+ message.
1735
+ - **`MessageCreditsService`** and `mortar_message_credits` (`/nestjs`) — credit
1736
+ as a ledger, append-only, with the balance summed from the entries rather than
1737
+ kept in a column that can disagree with them. No foreign key to whatever the
1738
+ segments were spent on, which is what lets two products share it.
1739
+
1740
+ The root entry point is pure — no database, no framework, no Node built-ins —
1741
+ because a console counts segments on every keystroke and that has to run in a
1742
+ browser. Everything needing TypeORM is behind `/nestjs`.
1743
+
1744
+ ## csv 1.0.0
1745
+
1746
+ Extracted at the second consumer: project 12 reads a shop's shelf out of a
1747
+ spreadsheet, project 13 writes an access log a regulator will open. Both had
1748
+ hand-written code, and both had a bug the other did not.
1749
+
1750
+ ### Added
1751
+
1752
+ - **`parseCsv`** — delimiter detected from the file. Every locale that uses a
1753
+ comma as the decimal separator gets semicolon-separated files out of Excel,
1754
+ still called CSV, and the obvious shortcut of honouring both at once splits a
1755
+ field reading `screen cracked; battery dead` in two and shifts every column
1756
+ after it, silently. Blank lines dropped; the mark Excel writes stripped, since
1757
+ left in place it hides in the first header.
1758
+ - **`toCsv` and `toCsvFrom`** — quoting that is not optional, empty cells rather
1759
+ than the strings `null` and `undefined`, and a byte-order mark by default,
1760
+ because Excel guesses a file's encoding by looking at it and reads an unmarked
1761
+ UTF-8 file as the system code page — so `Ioană` opens as `Ioană` for exactly
1762
+ the people whose names have diacritics. `toCsvFrom` takes the column order
1763
+ rather than reading it off the first object's keys.
1764
+
1765
+ Pure: no database, no framework, no Node built-ins, so a console can preview an
1766
+ upload before it happens. Deliberately not part of `files`, which carries S3,
1767
+ virus scanning and PDF assembly.
1768
+
1769
+ ## jobs 1.1.1
1770
+
1771
+ ### Fixed
1772
+
1773
+ - **`JobsModule` now closes the Redis connection it opened.** It creates a
1774
+ dedicated connection and hands it to BullMQ, and BullMQ closes connections it
1775
+ created while leaving alone the ones it was given — correctly, since it does
1776
+ not own them. Nothing closed this one. An application that finished its work
1777
+ and called `app.close()` sat there with an open socket for ever: a seed script
1778
+ that never returned, and a deployment step that hung waiting for it. The
1779
+ connection is now provided under `MORTAR_QUEUE_CONNECTION` and closed in
1780
+ `onApplicationShutdown`, after the workers and queues that use it.
1781
+
1782
+ ## redis 1.0.1
1783
+
1784
+ ### Fixed
1785
+
1786
+ - **A queue connection no longer carries a command timeout.** `createQueueConnection`
1787
+ already cleared `maxRetriesPerRequest` for BullMQ, but left the five-second
1788
+ `commandTimeout` in place — and a queue consumer waits for work with blocking
1789
+ reads that are _designed_ to sit there for longer than any sensible deadline.
1790
+ The result was an idle worker logging `Command timed out` every few seconds,
1791
+ on every queue, for ever. Jobs still ran, which is what made it easy to read
1792
+ as a sick Redis rather than a misconfigured client. `commandTimeoutMs` now
1793
+ accepts `null` to mean "no deadline", and queue connections pass it.
1794
+
1795
+ ## comms 1.2.0
1796
+
1797
+ The vendors, behind the ports that were waiting for them (dossier D-10).
1798
+
1799
+ ### Added
1800
+
1801
+ - **`ResendMessagePort`** — email, on Resend's own SDK. A message may carry its
1802
+ own `from` and `replyTo`, which is how it is branded as a customer without
1803
+ their domain being one the provider can sign for: their name in the display
1804
+ part, their address to reply to, so a client who replies reaches their
1805
+ accountant rather than a mailbox nobody reads.
1806
+ - **`TwilioMessagePort`** — SMS, on Twilio's SDK, preferring a messaging service
1807
+ over a single number. The sender identity is a per-market question — an
1808
+ alphanumeric sender ID is permitted in some countries, requires registration
1809
+ in others, and cannot be replied to anywhere — and a messaging service is what
1810
+ lets it change without a deployment. It refuses to be constructed with no
1811
+ sender at all, because the alternative is finding out twelve days into a
1812
+ reminder cadence.
1813
+ - **The segment count comes back from the provider**, not from our estimate.
1814
+ `countSegments` decides whether a message is worth sending; the ledger is
1815
+ debited by what was actually charged, and the two differing is the case a
1816
+ ledger exists to catch — one accented character downgrades a message to UCS-2
1817
+ and doubles its cost without changing a word.
1818
+ - **`ResendInbound`** — verifying the provider's webhook and fetching the
1819
+ message it names. The webhook carries metadata and no body, so the original is
1820
+ fetched and returned as **raw MIME** for `parseMime` to read: the parser stays
1821
+ ours, and the day the provider changes nothing above it moves. Verification is
1822
+ the vendor's own (Standard Webhooks) and takes the **raw** request body — a
1823
+ parsed object re-serialised has different bytes and fails.
1824
+
1825
+ ### Notes
1826
+
1827
+ - **The vendors' SDKs rather than their REST APIs**, which is the arrangement
1828
+ `files` already has with `@aws-sdk/client-s3`. Both were first written against
1829
+ the published REST documentation, and the SDK types caught a field this got
1830
+ wrong — a received message's download URL. Fewer lines, and the shapes are
1831
+ right by construction.
1832
+ - Both ports **throw** on refusal rather than returning a failure, carrying the
1833
+ provider's own sentence. `CommsService` records it in the message log, which
1834
+ is what support reads — a port that swallowed the reason would leave "it did
1835
+ not send" and nothing else.
1836
+ - `ResendMessagePort` imposes its own **timeout**: the SDK sets none, and
1837
+ something is usually waiting on a message — a professional who has just
1838
+ pressed send should not hold a response open until a socket gives up.
1839
+ - Every port takes an optional `client`, so a deployment can share one and a
1840
+ test can fake the vendor at its own surface rather than stubbing `fetch`.
1841
+
1842
+ ## context 1.1.0
1843
+
1844
+ An actor can be an operator.
1845
+
1846
+ ### Added
1847
+
1848
+ - **`Actor.type` accepts `'operator'`** — one of _us_, working inside a
1849
+ customer's account with their consent. Separate from `user` because the audit
1850
+ trail has to be able to say which it was: support access recorded as the
1851
+ customer's own action is worse than no record, being a confident answer to
1852
+ "who opened this?" that names the wrong person. Thirteen of the seventeen
1853
+ specifications describe back-office impersonation, so the type belongs here
1854
+ rather than in each of them.
1855
+ - `impersonatedBy` is now documented as the _other_ shape — an operator acting
1856
+ as a named user — with a note that acting as oneself inside the customer's
1857
+ account is the safer one, because nothing is disguised.
1858
+
1859
+ ## comms 1.1.0
1860
+
1861
+ Attachments, so a completed set of documents can be delivered by email (dossier
1862
+ F-174).
1863
+
1864
+ ### Added
1865
+
1866
+ - **`OutboundMessage.attachments`**, and `MAX_ATTACHMENT_BYTES` at 10 MB.
1867
+ Providers differ — many refuse at 10, most at 25 — and base64 inflates an
1868
+ attachment by a third, so the useful limit sits well under the smallest of
1869
+ them.
1870
+ - **Refused before the provider sees it.** A receiving server bounces an
1871
+ oversized attachment silently and late, which becomes "they never got it and
1872
+ nobody knows why". The log records a failure with a sentence instead, and
1873
+ nothing is handed to the port.
1874
+ - The message log records **how many files and how many bytes**, never their
1875
+ names: the log is read by support, and a client's filenames are not theirs to
1876
+ read.
1877
+
1878
+ ### Fixed
1879
+
1880
+ - **`NoopMessagePort` ids are now unique across processes.** They counted from
1881
+ one, and the message log has a unique index on
1882
+ `(direction, provider_message_id)` — so the second test run against the same
1883
+ database collided, and `CommsService` reported it as a message the provider
1884
+ refused. The failure surfaced in whatever was being tested rather than in the
1885
+ double, and only on the second run.
1886
+
1887
+ ## files 1.2.0
1888
+
1889
+ ZIP archives and provider-enforced retention (dossier F-170, F-178): a completed
1890
+ request leaves as one file whose folders and names the receiving firm can file
1891
+ without opening it. A ZIP of `IMG_4471.jpg` is worthless; one of
1892
+ `Ion_Popescu/03_Bank_statement.pdf` is already filed.
1893
+
1894
+ ### Added
1895
+
1896
+ - **`createZip`.** Hand-written over `node:zlib` rather than taken from a
1897
+ dependency — the essential format is two hundred lines and has not changed
1898
+ since 1993, and every library that writes it brings a stream stack and a
1899
+ supply chain with it.
1900
+ - Deterministic when given a `modified` date, so a delivery retry produces the
1901
+ file the destination already has rather than a second copy.
1902
+ - Zip-slip paths (`/etc/passwd`, `../../secrets`) are stripped rather than
1903
+ trusted to the extractor; duplicate paths are refused rather than left for the
1904
+ extractor to resolve; names are flagged UTF-8 so a Romanian filename survives.
1905
+ - Entries are deflated, and stored instead when deflate would make them bigger —
1906
+ which is every photograph and most PDFs.
1907
+ - Verified against `unzip` in the tests, not only against its own reader: an
1908
+ archive only this package can read is not an archive.
1909
+ - **`S3Storage.applyLifecycle` / `describeLifecycle`.** Provider-enforced expiry
1910
+ as a backstop under the application's own retention. The failure it covers is
1911
+ the one the application cannot: a sweep broken for a month leaves documents in
1912
+ a bucket and nothing in the application says so. An empty rule list removes
1913
+ the configuration, because S3 refuses one with zero rules.
1914
+ - **`S3Storage` now has integration tests**, against MinIO rather than a mocked
1915
+ SDK — whether a presigned URL is actually accepted, what a missing object
1916
+ answers, and whether a lifecycle configuration is written in a shape a
1917
+ provider takes are all things a mock cannot speak to. Mortar's development
1918
+ stack gained a MinIO service on 3052/3053 for it.
1919
+ - **`MemoryStorage` gained `has`, `clear`, `failOn` and `stopFailing`.** A suite
1920
+ shares one instance across a file, so without `clear` every object from every
1921
+ earlier test is still there and an assertion about what a cleanup removed
1922
+ silently starts passing for the wrong reason. `failOn` exists because real
1923
+ buckets fail one object at a time, and what matters is what the caller does
1924
+ about it: a retention sweep must not abandon thirty-nine other firms because
1925
+ one object would not delete.
1926
+
1927
+ ## files 1.1.0
1928
+
1929
+ Single-PDF assembly (dossier F-090): several photographed pages become one
1930
+ document, which is what a professional actually wants — three separate JPEGs of
1931
+ a statement means three files to open in an order only knowable from filenames
1932
+ the client did not choose.
1933
+
1934
+ ### Added
1935
+
1936
+ - **`assemblePdf`.** JPEG and PNG are embedded natively, `DCTDecode` and
1937
+ `FlateDecode`, so a photograph reaches the professional as the bytes the
1938
+ camera produced rather than a generational copy. Pages are sized to their
1939
+ image rather than floated on a fixed A4, scaled down but never up.
1940
+ - HEIC is refused. A phone produces it, no PDF reader opens it, and converting
1941
+ it needs a decoder this package is not going to carry.
1942
+ - No producer or creation date is written: these are a client's bank statements,
1943
+ and the defaults name the software that touched them. It also makes the output
1944
+ deterministic, which a test asserts.
1945
+
1946
+ ### A dependency, and why this one
1947
+
1948
+ `pdf-lib` is a real dependency in a package that has argued against them —
1949
+ `@birtalanrobert/comms` writes its own MIME parser, and the ClamAV adapter
1950
+ speaks the protocol directly. The distinction is where a failure shows up. A
1951
+ MIME parser that gets something wrong loses an attachment, visibly, immediately.
1952
+ **A malformed PDF is invisible until a professional cannot open it**, days
1953
+ later, with a client who has already put the paper away — and PDF is a format
1954
+ with enough subtlety that hand-rolling a writer is a wager on being right about
1955
+ all of it.
1956
+
1957
+ ### A bug found while writing the tests
1958
+
1959
+ `pdf-lib` reads an image's **whole backing `ArrayBuffer` and ignores the view's
1960
+ `byteOffset`**. Node allocates every Buffer under 4 KB from a shared 8 KB pool,
1961
+ so a small page — a compressed scan, or anything fetched from storage — arrives
1962
+ at a non-zero offset, and the embedder parses whatever sits at the pool's start.
1963
+
1964
+ It is a nasty shape of bug: whether it fires depends on what else the process
1965
+ has allocated, so the first several runs passed by reading a stale copy of the
1966
+ same image left at position 0. An offset-aware view does not fix it, because it
1967
+ shares the ArrayBuffer. `assemblePdf` copies the bytes, and a test builds a
1968
+ pooled buffer deliberately.
1969
+
1970
+ ## http 2.0.0 — and a minor for everything that depends on it
1971
+
1972
+ `@birtalanrobert/http` root entry point is now framework-free.
1973
+
1974
+ ### Why a major
1975
+
1976
+ The root exported the exception filter, the context middleware, the validation
1977
+ pipe, the health controller, `HttpModule` and `@PublicRoute()` — so importing
1978
+ `NotFoundError` imported NestJS. Every package that raises a mortar error
1979
+ inherited that, which is a framework in an edge bundle for the sake of a type
1980
+ guard.
1981
+
1982
+ Those six now live at `@birtalanrobert/http/nestjs`. **The error classes,
1983
+ problem serialisation, header names, locale negotiation and the health registry
1984
+ have not moved**, so most files need no change; an application module and a
1985
+ bootstrap file need one line each.
1986
+
1987
+ ### Also changed
1988
+
1989
+ - **`toProblemDetails` recognises a Nest `HttpException` by shape rather than
1990
+ by `instanceof`.** That removes the last runtime import, and it is the more
1991
+ correct check: two copies of `@nestjs/common` in one install — routine in a
1992
+ monorepo — make `instanceof` false for the framework's own exceptions, so its
1993
+ validation errors would silently fall through to the generic 500 branch. The
1994
+ function is documented as total; recognising the contract is what makes that
1995
+ true.
1996
+ - **`REQUEST_ID_HEADER`, `CORRELATION_ID_HEADER` and `negotiateLocale` moved to
1997
+ their own module** so a Next.js middleware can read the same header names
1998
+ without the middleware class that uses them.
1999
+
2000
+ ### auth 1.1.0, idempotency 1.1.0, tenancy 1.1.0, workflow 1.1.0
2001
+
2002
+ No API change. Each depends on `http`, and each is republished so its dependency
2003
+ range moves to `^2.0.0` — otherwise an application installing `http@2` would end
2004
+ up with a second copy at `1.x` underneath these, and `isMortarError` is an
2005
+ `instanceof` check that two copies quietly break.
2006
+
2007
+ `workflow` also gains the `mortar.entries` field it was missing, so its
2008
+ `nestjs/` subpath stub is regenerated by the build instead of surviving only
2009
+ because nothing had deleted it.
2010
+
2011
+ ### Every package README now documents its wiring
2012
+
2013
+ What to import, whether it is `forRoot` or `forRootAsync`, where it goes in the
2014
+ imports array and what breaks if it goes elsewhere, which entities and
2015
+ migrations to register, and what needs no module at all — `context`, `money` and
2016
+ the root half of `http` are imported directly.
2017
+
2018
+ Two scripts check the result rather than trusting it: one resolves every
2019
+ documented import against the built `.d.ts` files, the other checks every
2020
+ `Module.forRoot…()` shown actually exists. Both found real errors — a
2021
+ `RedisService.remember` that does not exist (it is `redis.cache.getOrSet`), a
2022
+ `workers.handle` that is `workers.register`, an `envBool` that is `envBoolean`,
2023
+ and column helpers documented in the wrong package.
2024
+
2025
+ ## files 1.0.0, comms 1.0.0
2026
+
2027
+ The two Tier 2 packages dossier's Phase 2 needs: somewhere for an uploaded
2028
+ document to go, and a way for a client to forward one they already have.
2029
+
2030
+ Built now rather than up front because this is the phase that first needs them —
2031
+ and built partially, on purpose. `files` has no PDF assembly, thumbnailing or
2032
+ ZIP packaging; `comms` has no templates, quiet hours or credit ledger. Those
2033
+ belong to the phases that need them, and writing them now would be guessing at
2034
+ requirements three projects away.
2035
+
2036
+ ### `files`
2037
+
2038
+ - **Pre-signed direct upload.** The browser uploads to storage without touching
2039
+ the API. Proxying the bytes costs a request-sized chunk of memory per
2040
+ concurrent upload and puts the API's timeout between a client on a train and
2041
+ finishing. The cost is real rows in `pending`, which `sweepAbandoned` clears.
2042
+ - **The type is read from the bytes, never from the header.** A `Content-Type`
2043
+ and a filename extension are claims made by whoever uploaded the file.
2044
+ - **One bucket, tenant id as the first path segment**, so a bucket policy can
2045
+ name it. `assertTenantOwns` before every read, delete and signature: nothing
2046
+ governs a bucket except the key handed to it.
2047
+ - **Envelope encryption for erasure, not confidentiality.** The provider already
2048
+ encrypts at rest. Destroying one wrapped key is the difference between an
2049
+ erasure request honoured in seconds and one that cannot honestly be honoured,
2050
+ because backups exist. The object key is bound in as AAD, so a ciphertext
2051
+ moved under another tenant's prefix fails to open.
2052
+ - **`RefusingScanner` is the default.** A misconfiguration that silently
2053
+ disables virus scanning is indistinguishable from working software until it
2054
+ matters; one that refuses uploads is noticed in minutes.
2055
+ - **`MemoryStorage` is exported.** Every service consuming `StoragePort` lives
2056
+ in another repository and needs to test its upload flow without a bucket.
2057
+
2058
+ ### `comms`
2059
+
2060
+ - **Signed per-request inbound addresses.** The address is the credential, so it
2061
+ carries an HMAC tag; without one a predictable local part lets a stranger post
2062
+ documents into a firm's workflow. Its own secret, because an address lives for
2063
+ years in sent folders while a link expires in days.
2064
+ - **A MIME parser rather than a dependency.** Inbound mail is the most hostile
2065
+ input the system accepts. Eighty readable lines tested against what actually
2066
+ arrives is a smaller permanent surface than a parser that knows every corner
2067
+ of MIME in order to be asked about six.
2068
+ - **A partial unique index on the provider's message id.** Providers redeliver;
2069
+ without it a forwarded bank statement is attached three times. A constraint
2070
+ rather than a check, because two redeliveries can arrive at once.
2071
+ - **The message body is never logged.** A reminder is innocuous; inbound mail
2072
+ here is bank statements.
2073
+ - **Ports only for sending.** Providers are Phase 5; the seam exists now so the
2074
+ one thing that needs sending sooner has somewhere to go.
2075
+
2076
+ ### A defect in the scaffolding, found by the editor
2077
+
2078
+ `scripts/new-package.mjs` generated a single `tsconfig.json` that both emitted
2079
+ to `dist` and excluded `*.test.ts` — so a new package's tests belonged to no
2080
+ project and were type-checked by nothing. The build passed while the editor
2081
+ showed errors, which is how three genuine type errors in `envelope.test.ts`
2082
+ survived a green run.
2083
+
2084
+ `files`, `comms` and `workflow` now carry the standard pair the other twelve
2085
+ packages already had, and the scaffold writes both. Nothing published changes:
2086
+ `dist` never contained tests either way.
2087
+
2088
+ ### A bug this found
2089
+
2090
+ The inbound tag was base64url at first, and every address failed to verify
2091
+ itself. `parse` lowercases the address on the way in — correctly, because
2092
+ providers lowercase local parts — which destroys a case-sensitive tag. Hex
2093
+ costs a few characters in an address nobody types by hand.
2094
+
2095
+ ## observability 1.0.1
2096
+
2097
+ Never published. An interrupted publish left `1.0.0` partially staged and npm
2098
+ rejected a retry, so the version was stepped over — and then the staged upload
2099
+ finalised on npm's side after all. `1.0.0` is the real release; `1.0.1` does
2100
+ not exist.
2101
+
2102
+ ## observability 1.1.0, jobs 1.1.0
2103
+
2104
+ Everything a worker needs to be observable. Found by building `starter-worker`,
2105
+ whose specification asks for queue depth, job duration, failure rate and
2106
+ scanner lag — none of which anything recorded.
2107
+
2108
+ ### Added
2109
+
2110
+ - **`JobWorkers` records `job_duration_ms`, `jobs_total` (labelled by outcome)
2111
+ and `jobs_dead_lettered_total`.** In the runner rather than in each handler:
2112
+ how many ran, how many failed and how long they took are properties of the
2113
+ runner and identical in every service. One counter with a `status` label
2114
+ rather than two counters, because failure rate is a ratio and both halves
2115
+ must share their labels. Defaults to a no-op registry.
2116
+
2117
+ - **`WindowScanner` records `scanner_scan_duration_ms`, `scanner_items_total`
2118
+ and `scanner_last_success_timestamp_ms`.** The last is the one worth alerting
2119
+ on: a scanner that has stopped logs nothing and errors nothing, it simply
2120
+ stops finding work, and the first anyone hears is a customer asking why they
2121
+ were never reminded. A timestamp rather than an age, because a gauge written
2122
+ only on success cannot grow while the scanner is dead.
2123
+
2124
+ - **`JobQueues` rejects a job id containing `:`**, naming the job and the id.
2125
+ BullMQ uses the colon as a key separator and refuses such an id with an error
2126
+ that mentions neither — and `` `reminder:${id}` `` is the natural thing to
2127
+ write, so that error is reached often and explains nothing.
2128
+
2129
+ - **`JobsModule` passes the container's metrics registry** to the worker
2130
+ registry, so this costs a consumer nothing to switch on.
2131
+
2132
+ - **`InMemoryMetrics.snapshot()`**, returning every series held. A `/metrics`
2133
+ endpoint has to enumerate what exists, and `value()` could only answer about
2134
+ a name the caller already knew. Histograms report count, sum, min and max;
2135
+ bucketing is a presentation decision belonging to whatever scrapes it.
2136
+
2137
+ ### Fixed
2138
+
2139
+ - **Histogram labels are stored beside their observations** rather than
2140
+ recovered by parsing the storage key. A label value containing `=` or `,`
2141
+ would not have survived the round trip.
2142
+
2143
+ ## 1.0.0
2144
+
2145
+ The version numbers become meaningful.
2146
+
2147
+ Until now every package shared one version and all twelve were republished
2148
+ together. That does not survive contact with per-package releases while the
2149
+ major is `0`: under semver a `^0.2.0` range excludes `0.3.0`, so changing one
2150
+ package and releasing only it leaves every dependent pinned to the old copy —
2151
+ and npm resolves that by installing both. Two copies of `observability` means
2152
+ two distinct `MORTAR_LOGGER` symbols, and dependency injection stops working
2153
+ with an error that names neither.
2154
+
2155
+ At `1.x` a caret range accepts later minors, so a package can be released on
2156
+ its own and its dependents pick it up on their next install. From here:
2157
+
2158
+ - **patch** — a fix that changes no signature
2159
+ - **minor** — anything added
2160
+ - **major** — anything removed or changed in shape
2161
+
2162
+ ### Added
2163
+
2164
+ - **`DatabaseModule` can run migrations at boot** — `migrationsRun: true`.
2165
+
2166
+ Guarded by a Postgres advisory lock, so several replicas starting at once are
2167
+ safe: one applies while the others wait, then find nothing pending. TypeORM
2168
+ takes no lock of its own, and without one the second replica to reach a
2169
+ `CREATE TABLE` fails and that container crash-loops. Also exported directly
2170
+ as `runMigrationsWithLock` for release-step scripts.
2171
+
2172
+ - **`LoggerModule` provides `NestLoggerAdapter` and `LoggingInterceptor`.**
2173
+ Both were exported but never registered, so `app.get(NestLoggerAdapter)` and
2174
+ `{ provide: APP_INTERCEPTOR, useExisting: LoggingInterceptor }` — the two
2175
+ documented ways to use them — both failed. Constructing them by hand still
2176
+ works.
2177
+
2178
+ - **`PUBLIC_ROUTE_KEY` and `PublicRoute()` in `@birtalanrobert/http`**, and the
2179
+ health controller now carries them. `@birtalanrobert/auth` re-exports the key
2180
+ as `PUBLIC_KEY`, unchanged, so `PermissionsGuard` and `@Public()` behave
2181
+ exactly as before — but a globally registered guard no longer 401s the
2182
+ readiness probe, which previously left pods that never joined the load
2183
+ balancer.
2184
+
2185
+ - **`auditEntities` and `idempotencyEntities`**, so every package that ships
2186
+ entities exports them as an array the same way it exports its migrations.
2187
+
2188
+ ### Fixed
2189
+
2190
+ - **A circular import between `logger.module.ts` and the two classes it now
2191
+ provides** left `MORTAR_LOGGER` `undefined` at decorator evaluation time, so
2192
+ `@Inject(MORTAR_LOGGER)` silently degraded to reflected-type injection and
2193
+ Nest reported that it could not resolve `Function`. The tokens moved to a
2194
+ leaf module. Under CommonJS this class of bug fails at wiring time, never at
2195
+ build time.
2196
+
2197
+ ### Testing
2198
+
2199
+ `@nestjs/testing` and `unplugin-swc` are now dev dependencies, and the Nest
2200
+ modules are exercised by building a real container rather than by inspecting
2201
+ the `DynamicModule` object. Every defect above was invisible to a test that
2202
+ asserts on `module.providers` and obvious to one that calls `moduleRef.get()`.
2203
+
2204
+ ## 0.2.0
2205
+
2206
+ Composing the packages into a real application surfaced three problems that
2207
+ package-level tests could not.
2208
+
2209
+ ### Added
2210
+
2211
+ - **`forRootAsync` on every configurable module** — `LoggerModule`,
2212
+ `DatabaseModule`, `RedisModule`, `HttpModule`, `TenancyModule`, `AuthModule`,
2213
+ `IdempotencyModule` and `JobsModule`.
2214
+
2215
+ Previously each module took its options synchronously, which meant a consumer
2216
+ had to read `process.env` at import time — before anything had validated it —
2217
+ to configure a database URL or a Redis connection. That defeats having a
2218
+ configuration layer at all. Options can now come from any provider, including
2219
+ the validated config.
2220
+
2221
+ - **`ConfigModule.token()`**, so a wiring site can write
2222
+ `inject: [ConfigModule.token()]` rather than importing the raw symbol.
2223
+
2224
+ - **`AsyncModuleOptions<T>`** in `@birtalanrobert/context`: the shared shape for
2225
+ the above.
2226
+
2227
+ ### Fixed
2228
+
2229
+ - **`HttpModule` and `TenancyModule` no longer hold module options in static
2230
+ fields.** Both middlewares now receive their options through dependency
2231
+ injection. The previous arrangement meant a second `forRoot()` call silently
2232
+ overwrote the first — which is exactly what happens when a test suite builds
2233
+ more than one application in a process.
2234
+
2235
+ - **`@birtalanrobert/http` accepts `class-validator` 0.15**, which is current.
2236
+ The peer range previously stopped at 0.14 and produced an unmet-peer warning
2237
+ on every install.
2238
+
2239
+ - **Internal dependencies publish as `^x.y.z` rather than an exact pin.** Exact
2240
+ pins across a family released together make npm install several copies of the
2241
+ same package as soon as two versions coexist in one tree.
2242
+
2243
+ ### Note on compatibility
2244
+
2245
+ `HttpModule.contextOptions` and `TenancyModule.resolvers` are no longer present
2246
+ as static properties. They were declared `private` and were never part of the
2247
+ documented surface — TypeScript consumers could not reach them — but a
2248
+ JavaScript consumer reading them would break. Nothing else changed shape.
2249
+
2250
+ ## 0.1.0
2251
+
2252
+ First release.