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