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