@birtalanrobert/context 0.2.0 → 1.1.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 +365 -1
- package/README.md +67 -1
- package/dist/types.d.ts +16 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/types.ts +16 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,370 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
## context 1.1.0
|
|
8
|
+
|
|
9
|
+
An actor can be an operator.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`Actor.type` accepts `'operator'`** — one of _us_, working inside a
|
|
14
|
+
customer's account with their consent. Separate from `user` because the audit
|
|
15
|
+
trail has to be able to say which it was: support access recorded as the
|
|
16
|
+
customer's own action is worse than no record, being a confident answer to
|
|
17
|
+
"who opened this?" that names the wrong person. Thirteen of the seventeen
|
|
18
|
+
specifications describe back-office impersonation, so the type belongs here
|
|
19
|
+
rather than in each of them.
|
|
20
|
+
- `impersonatedBy` is now documented as the _other_ shape — an operator acting
|
|
21
|
+
as a named user — with a note that acting as oneself inside the customer's
|
|
22
|
+
account is the safer one, because nothing is disguised.
|
|
23
|
+
|
|
24
|
+
## comms 1.1.0
|
|
25
|
+
|
|
26
|
+
Attachments, so a completed set of documents can be delivered by email (dossier
|
|
27
|
+
F-174).
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **`OutboundMessage.attachments`**, and `MAX_ATTACHMENT_BYTES` at 10 MB.
|
|
32
|
+
Providers differ — many refuse at 10, most at 25 — and base64 inflates an
|
|
33
|
+
attachment by a third, so the useful limit sits well under the smallest of
|
|
34
|
+
them.
|
|
35
|
+
- **Refused before the provider sees it.** A receiving server bounces an
|
|
36
|
+
oversized attachment silently and late, which becomes "they never got it and
|
|
37
|
+
nobody knows why". The log records a failure with a sentence instead, and
|
|
38
|
+
nothing is handed to the port.
|
|
39
|
+
- The message log records **how many files and how many bytes**, never their
|
|
40
|
+
names: the log is read by support, and a client's filenames are not theirs to
|
|
41
|
+
read.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **`NoopMessagePort` ids are now unique across processes.** They counted from
|
|
46
|
+
one, and the message log has a unique index on
|
|
47
|
+
`(direction, provider_message_id)` — so the second test run against the same
|
|
48
|
+
database collided, and `CommsService` reported it as a message the provider
|
|
49
|
+
refused. The failure surfaced in whatever was being tested rather than in the
|
|
50
|
+
double, and only on the second run.
|
|
51
|
+
|
|
52
|
+
## files 1.2.0
|
|
53
|
+
|
|
54
|
+
ZIP archives and provider-enforced retention (dossier F-170, F-178): a completed
|
|
55
|
+
request leaves as one file whose folders and names the receiving firm can file
|
|
56
|
+
without opening it. A ZIP of `IMG_4471.jpg` is worthless; one of
|
|
57
|
+
`Ion_Popescu/03_Bank_statement.pdf` is already filed.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **`createZip`.** Hand-written over `node:zlib` rather than taken from a
|
|
62
|
+
dependency — the essential format is two hundred lines and has not changed
|
|
63
|
+
since 1993, and every library that writes it brings a stream stack and a
|
|
64
|
+
supply chain with it.
|
|
65
|
+
- Deterministic when given a `modified` date, so a delivery retry produces the
|
|
66
|
+
file the destination already has rather than a second copy.
|
|
67
|
+
- Zip-slip paths (`/etc/passwd`, `../../secrets`) are stripped rather than
|
|
68
|
+
trusted to the extractor; duplicate paths are refused rather than left for the
|
|
69
|
+
extractor to resolve; names are flagged UTF-8 so a Romanian filename survives.
|
|
70
|
+
- Entries are deflated, and stored instead when deflate would make them bigger —
|
|
71
|
+
which is every photograph and most PDFs.
|
|
72
|
+
- Verified against `unzip` in the tests, not only against its own reader: an
|
|
73
|
+
archive only this package can read is not an archive.
|
|
74
|
+
- **`S3Storage.applyLifecycle` / `describeLifecycle`.** Provider-enforced expiry
|
|
75
|
+
as a backstop under the application's own retention. The failure it covers is
|
|
76
|
+
the one the application cannot: a sweep broken for a month leaves documents in
|
|
77
|
+
a bucket and nothing in the application says so. An empty rule list removes
|
|
78
|
+
the configuration, because S3 refuses one with zero rules.
|
|
79
|
+
- **`S3Storage` now has integration tests**, against MinIO rather than a mocked
|
|
80
|
+
SDK — whether a presigned URL is actually accepted, what a missing object
|
|
81
|
+
answers, and whether a lifecycle configuration is written in a shape a
|
|
82
|
+
provider takes are all things a mock cannot speak to. Mortar's development
|
|
83
|
+
stack gained a MinIO service on 3052/3053 for it.
|
|
84
|
+
- **`MemoryStorage` gained `has`, `clear`, `failOn` and `stopFailing`.** A suite
|
|
85
|
+
shares one instance across a file, so without `clear` every object from every
|
|
86
|
+
earlier test is still there and an assertion about what a cleanup removed
|
|
87
|
+
silently starts passing for the wrong reason. `failOn` exists because real
|
|
88
|
+
buckets fail one object at a time, and what matters is what the caller does
|
|
89
|
+
about it: a retention sweep must not abandon thirty-nine other firms because
|
|
90
|
+
one object would not delete.
|
|
91
|
+
|
|
92
|
+
## files 1.1.0
|
|
93
|
+
|
|
94
|
+
Single-PDF assembly (dossier F-090): several photographed pages become one
|
|
95
|
+
document, which is what a professional actually wants — three separate JPEGs of
|
|
96
|
+
a statement means three files to open in an order only knowable from filenames
|
|
97
|
+
the client did not choose.
|
|
98
|
+
|
|
99
|
+
### Added
|
|
100
|
+
|
|
101
|
+
- **`assemblePdf`.** JPEG and PNG are embedded natively, `DCTDecode` and
|
|
102
|
+
`FlateDecode`, so a photograph reaches the professional as the bytes the
|
|
103
|
+
camera produced rather than a generational copy. Pages are sized to their
|
|
104
|
+
image rather than floated on a fixed A4, scaled down but never up.
|
|
105
|
+
- HEIC is refused. A phone produces it, no PDF reader opens it, and converting
|
|
106
|
+
it needs a decoder this package is not going to carry.
|
|
107
|
+
- No producer or creation date is written: these are a client's bank statements,
|
|
108
|
+
and the defaults name the software that touched them. It also makes the output
|
|
109
|
+
deterministic, which a test asserts.
|
|
110
|
+
|
|
111
|
+
### A dependency, and why this one
|
|
112
|
+
|
|
113
|
+
`pdf-lib` is a real dependency in a package that has argued against them —
|
|
114
|
+
`@birtalanrobert/comms` writes its own MIME parser, and the ClamAV adapter
|
|
115
|
+
speaks the protocol directly. The distinction is where a failure shows up. A
|
|
116
|
+
MIME parser that gets something wrong loses an attachment, visibly, immediately.
|
|
117
|
+
**A malformed PDF is invisible until a professional cannot open it**, days
|
|
118
|
+
later, with a client who has already put the paper away — and PDF is a format
|
|
119
|
+
with enough subtlety that hand-rolling a writer is a wager on being right about
|
|
120
|
+
all of it.
|
|
121
|
+
|
|
122
|
+
### A bug found while writing the tests
|
|
123
|
+
|
|
124
|
+
`pdf-lib` reads an image's **whole backing `ArrayBuffer` and ignores the view's
|
|
125
|
+
`byteOffset`**. Node allocates every Buffer under 4 KB from a shared 8 KB pool,
|
|
126
|
+
so a small page — a compressed scan, or anything fetched from storage — arrives
|
|
127
|
+
at a non-zero offset, and the embedder parses whatever sits at the pool's start.
|
|
128
|
+
|
|
129
|
+
It is a nasty shape of bug: whether it fires depends on what else the process
|
|
130
|
+
has allocated, so the first several runs passed by reading a stale copy of the
|
|
131
|
+
same image left at position 0. An offset-aware view does not fix it, because it
|
|
132
|
+
shares the ArrayBuffer. `assemblePdf` copies the bytes, and a test builds a
|
|
133
|
+
pooled buffer deliberately.
|
|
134
|
+
|
|
135
|
+
## http 2.0.0 — and a minor for everything that depends on it
|
|
136
|
+
|
|
137
|
+
`@birtalanrobert/http` root entry point is now framework-free.
|
|
138
|
+
|
|
139
|
+
### Why a major
|
|
140
|
+
|
|
141
|
+
The root exported the exception filter, the context middleware, the validation
|
|
142
|
+
pipe, the health controller, `HttpModule` and `@PublicRoute()` — so importing
|
|
143
|
+
`NotFoundError` imported NestJS. Every package that raises a mortar error
|
|
144
|
+
inherited that, which is a framework in an edge bundle for the sake of a type
|
|
145
|
+
guard.
|
|
146
|
+
|
|
147
|
+
Those six now live at `@birtalanrobert/http/nestjs`. **The error classes,
|
|
148
|
+
problem serialisation, header names, locale negotiation and the health registry
|
|
149
|
+
have not moved**, so most files need no change; an application module and a
|
|
150
|
+
bootstrap file need one line each.
|
|
151
|
+
|
|
152
|
+
### Also changed
|
|
153
|
+
|
|
154
|
+
- **`toProblemDetails` recognises a Nest `HttpException` by shape rather than
|
|
155
|
+
by `instanceof`.** That removes the last runtime import, and it is the more
|
|
156
|
+
correct check: two copies of `@nestjs/common` in one install — routine in a
|
|
157
|
+
monorepo — make `instanceof` false for the framework's own exceptions, so its
|
|
158
|
+
validation errors would silently fall through to the generic 500 branch. The
|
|
159
|
+
function is documented as total; recognising the contract is what makes that
|
|
160
|
+
true.
|
|
161
|
+
- **`REQUEST_ID_HEADER`, `CORRELATION_ID_HEADER` and `negotiateLocale` moved to
|
|
162
|
+
their own module** so a Next.js middleware can read the same header names
|
|
163
|
+
without the middleware class that uses them.
|
|
164
|
+
|
|
165
|
+
### auth 1.1.0, idempotency 1.1.0, tenancy 1.1.0, workflow 1.1.0
|
|
166
|
+
|
|
167
|
+
No API change. Each depends on `http`, and each is republished so its dependency
|
|
168
|
+
range moves to `^2.0.0` — otherwise an application installing `http@2` would end
|
|
169
|
+
up with a second copy at `1.x` underneath these, and `isMortarError` is an
|
|
170
|
+
`instanceof` check that two copies quietly break.
|
|
171
|
+
|
|
172
|
+
`workflow` also gains the `mortar.entries` field it was missing, so its
|
|
173
|
+
`nestjs/` subpath stub is regenerated by the build instead of surviving only
|
|
174
|
+
because nothing had deleted it.
|
|
175
|
+
|
|
176
|
+
### Every package README now documents its wiring
|
|
177
|
+
|
|
178
|
+
What to import, whether it is `forRoot` or `forRootAsync`, where it goes in the
|
|
179
|
+
imports array and what breaks if it goes elsewhere, which entities and
|
|
180
|
+
migrations to register, and what needs no module at all — `context`, `money` and
|
|
181
|
+
the root half of `http` are imported directly.
|
|
182
|
+
|
|
183
|
+
Two scripts check the result rather than trusting it: one resolves every
|
|
184
|
+
documented import against the built `.d.ts` files, the other checks every
|
|
185
|
+
`Module.forRoot…()` shown actually exists. Both found real errors — a
|
|
186
|
+
`RedisService.remember` that does not exist (it is `redis.cache.getOrSet`), a
|
|
187
|
+
`workers.handle` that is `workers.register`, an `envBool` that is `envBoolean`,
|
|
188
|
+
and column helpers documented in the wrong package.
|
|
189
|
+
|
|
190
|
+
## files 1.0.0, comms 1.0.0
|
|
191
|
+
|
|
192
|
+
The two Tier 2 packages dossier's Phase 2 needs: somewhere for an uploaded
|
|
193
|
+
document to go, and a way for a client to forward one they already have.
|
|
194
|
+
|
|
195
|
+
Built now rather than up front because this is the phase that first needs them —
|
|
196
|
+
and built partially, on purpose. `files` has no PDF assembly, thumbnailing or
|
|
197
|
+
ZIP packaging; `comms` has no templates, quiet hours or credit ledger. Those
|
|
198
|
+
belong to the phases that need them, and writing them now would be guessing at
|
|
199
|
+
requirements three projects away.
|
|
200
|
+
|
|
201
|
+
### `files`
|
|
202
|
+
|
|
203
|
+
- **Pre-signed direct upload.** The browser uploads to storage without touching
|
|
204
|
+
the API. Proxying the bytes costs a request-sized chunk of memory per
|
|
205
|
+
concurrent upload and puts the API's timeout between a client on a train and
|
|
206
|
+
finishing. The cost is real rows in `pending`, which `sweepAbandoned` clears.
|
|
207
|
+
- **The type is read from the bytes, never from the header.** A `Content-Type`
|
|
208
|
+
and a filename extension are claims made by whoever uploaded the file.
|
|
209
|
+
- **One bucket, tenant id as the first path segment**, so a bucket policy can
|
|
210
|
+
name it. `assertTenantOwns` before every read, delete and signature: nothing
|
|
211
|
+
governs a bucket except the key handed to it.
|
|
212
|
+
- **Envelope encryption for erasure, not confidentiality.** The provider already
|
|
213
|
+
encrypts at rest. Destroying one wrapped key is the difference between an
|
|
214
|
+
erasure request honoured in seconds and one that cannot honestly be honoured,
|
|
215
|
+
because backups exist. The object key is bound in as AAD, so a ciphertext
|
|
216
|
+
moved under another tenant's prefix fails to open.
|
|
217
|
+
- **`RefusingScanner` is the default.** A misconfiguration that silently
|
|
218
|
+
disables virus scanning is indistinguishable from working software until it
|
|
219
|
+
matters; one that refuses uploads is noticed in minutes.
|
|
220
|
+
- **`MemoryStorage` is exported.** Every service consuming `StoragePort` lives
|
|
221
|
+
in another repository and needs to test its upload flow without a bucket.
|
|
222
|
+
|
|
223
|
+
### `comms`
|
|
224
|
+
|
|
225
|
+
- **Signed per-request inbound addresses.** The address is the credential, so it
|
|
226
|
+
carries an HMAC tag; without one a predictable local part lets a stranger post
|
|
227
|
+
documents into a firm's workflow. Its own secret, because an address lives for
|
|
228
|
+
years in sent folders while a link expires in days.
|
|
229
|
+
- **A MIME parser rather than a dependency.** Inbound mail is the most hostile
|
|
230
|
+
input the system accepts. Eighty readable lines tested against what actually
|
|
231
|
+
arrives is a smaller permanent surface than a parser that knows every corner
|
|
232
|
+
of MIME in order to be asked about six.
|
|
233
|
+
- **A partial unique index on the provider's message id.** Providers redeliver;
|
|
234
|
+
without it a forwarded bank statement is attached three times. A constraint
|
|
235
|
+
rather than a check, because two redeliveries can arrive at once.
|
|
236
|
+
- **The message body is never logged.** A reminder is innocuous; inbound mail
|
|
237
|
+
here is bank statements.
|
|
238
|
+
- **Ports only for sending.** Providers are Phase 5; the seam exists now so the
|
|
239
|
+
one thing that needs sending sooner has somewhere to go.
|
|
240
|
+
|
|
241
|
+
### A defect in the scaffolding, found by the editor
|
|
242
|
+
|
|
243
|
+
`scripts/new-package.mjs` generated a single `tsconfig.json` that both emitted
|
|
244
|
+
to `dist` and excluded `*.test.ts` — so a new package's tests belonged to no
|
|
245
|
+
project and were type-checked by nothing. The build passed while the editor
|
|
246
|
+
showed errors, which is how three genuine type errors in `envelope.test.ts`
|
|
247
|
+
survived a green run.
|
|
248
|
+
|
|
249
|
+
`files`, `comms` and `workflow` now carry the standard pair the other twelve
|
|
250
|
+
packages already had, and the scaffold writes both. Nothing published changes:
|
|
251
|
+
`dist` never contained tests either way.
|
|
252
|
+
|
|
253
|
+
### A bug this found
|
|
254
|
+
|
|
255
|
+
The inbound tag was base64url at first, and every address failed to verify
|
|
256
|
+
itself. `parse` lowercases the address on the way in — correctly, because
|
|
257
|
+
providers lowercase local parts — which destroys a case-sensitive tag. Hex
|
|
258
|
+
costs a few characters in an address nobody types by hand.
|
|
259
|
+
|
|
260
|
+
## observability 1.0.1
|
|
261
|
+
|
|
262
|
+
Never published. An interrupted publish left `1.0.0` partially staged and npm
|
|
263
|
+
rejected a retry, so the version was stepped over — and then the staged upload
|
|
264
|
+
finalised on npm's side after all. `1.0.0` is the real release; `1.0.1` does
|
|
265
|
+
not exist.
|
|
266
|
+
|
|
267
|
+
## observability 1.1.0, jobs 1.1.0
|
|
268
|
+
|
|
269
|
+
Everything a worker needs to be observable. Found by building `starter-worker`,
|
|
270
|
+
whose specification asks for queue depth, job duration, failure rate and
|
|
271
|
+
scanner lag — none of which anything recorded.
|
|
272
|
+
|
|
273
|
+
### Added
|
|
274
|
+
|
|
275
|
+
- **`JobWorkers` records `job_duration_ms`, `jobs_total` (labelled by outcome)
|
|
276
|
+
and `jobs_dead_lettered_total`.** In the runner rather than in each handler:
|
|
277
|
+
how many ran, how many failed and how long they took are properties of the
|
|
278
|
+
runner and identical in every service. One counter with a `status` label
|
|
279
|
+
rather than two counters, because failure rate is a ratio and both halves
|
|
280
|
+
must share their labels. Defaults to a no-op registry.
|
|
281
|
+
|
|
282
|
+
- **`WindowScanner` records `scanner_scan_duration_ms`, `scanner_items_total`
|
|
283
|
+
and `scanner_last_success_timestamp_ms`.** The last is the one worth alerting
|
|
284
|
+
on: a scanner that has stopped logs nothing and errors nothing, it simply
|
|
285
|
+
stops finding work, and the first anyone hears is a customer asking why they
|
|
286
|
+
were never reminded. A timestamp rather than an age, because a gauge written
|
|
287
|
+
only on success cannot grow while the scanner is dead.
|
|
288
|
+
|
|
289
|
+
- **`JobQueues` rejects a job id containing `:`**, naming the job and the id.
|
|
290
|
+
BullMQ uses the colon as a key separator and refuses such an id with an error
|
|
291
|
+
that mentions neither — and `` `reminder:${id}` `` is the natural thing to
|
|
292
|
+
write, so that error is reached often and explains nothing.
|
|
293
|
+
|
|
294
|
+
- **`JobsModule` passes the container's metrics registry** to the worker
|
|
295
|
+
registry, so this costs a consumer nothing to switch on.
|
|
296
|
+
|
|
297
|
+
- **`InMemoryMetrics.snapshot()`**, returning every series held. A `/metrics`
|
|
298
|
+
endpoint has to enumerate what exists, and `value()` could only answer about
|
|
299
|
+
a name the caller already knew. Histograms report count, sum, min and max;
|
|
300
|
+
bucketing is a presentation decision belonging to whatever scrapes it.
|
|
301
|
+
|
|
302
|
+
### Fixed
|
|
303
|
+
|
|
304
|
+
- **Histogram labels are stored beside their observations** rather than
|
|
305
|
+
recovered by parsing the storage key. A label value containing `=` or `,`
|
|
306
|
+
would not have survived the round trip.
|
|
307
|
+
|
|
308
|
+
## 1.0.0
|
|
309
|
+
|
|
310
|
+
The version numbers become meaningful.
|
|
311
|
+
|
|
312
|
+
Until now every package shared one version and all twelve were republished
|
|
313
|
+
together. That does not survive contact with per-package releases while the
|
|
314
|
+
major is `0`: under semver a `^0.2.0` range excludes `0.3.0`, so changing one
|
|
315
|
+
package and releasing only it leaves every dependent pinned to the old copy —
|
|
316
|
+
and npm resolves that by installing both. Two copies of `observability` means
|
|
317
|
+
two distinct `MORTAR_LOGGER` symbols, and dependency injection stops working
|
|
318
|
+
with an error that names neither.
|
|
319
|
+
|
|
320
|
+
At `1.x` a caret range accepts later minors, so a package can be released on
|
|
321
|
+
its own and its dependents pick it up on their next install. From here:
|
|
322
|
+
|
|
323
|
+
- **patch** — a fix that changes no signature
|
|
324
|
+
- **minor** — anything added
|
|
325
|
+
- **major** — anything removed or changed in shape
|
|
326
|
+
|
|
327
|
+
### Added
|
|
328
|
+
|
|
329
|
+
- **`DatabaseModule` can run migrations at boot** — `migrationsRun: true`.
|
|
330
|
+
|
|
331
|
+
Guarded by a Postgres advisory lock, so several replicas starting at once are
|
|
332
|
+
safe: one applies while the others wait, then find nothing pending. TypeORM
|
|
333
|
+
takes no lock of its own, and without one the second replica to reach a
|
|
334
|
+
`CREATE TABLE` fails and that container crash-loops. Also exported directly
|
|
335
|
+
as `runMigrationsWithLock` for release-step scripts.
|
|
336
|
+
|
|
337
|
+
- **`LoggerModule` provides `NestLoggerAdapter` and `LoggingInterceptor`.**
|
|
338
|
+
Both were exported but never registered, so `app.get(NestLoggerAdapter)` and
|
|
339
|
+
`{ provide: APP_INTERCEPTOR, useExisting: LoggingInterceptor }` — the two
|
|
340
|
+
documented ways to use them — both failed. Constructing them by hand still
|
|
341
|
+
works.
|
|
342
|
+
|
|
343
|
+
- **`PUBLIC_ROUTE_KEY` and `PublicRoute()` in `@birtalanrobert/http`**, and the
|
|
344
|
+
health controller now carries them. `@birtalanrobert/auth` re-exports the key
|
|
345
|
+
as `PUBLIC_KEY`, unchanged, so `PermissionsGuard` and `@Public()` behave
|
|
346
|
+
exactly as before — but a globally registered guard no longer 401s the
|
|
347
|
+
readiness probe, which previously left pods that never joined the load
|
|
348
|
+
balancer.
|
|
349
|
+
|
|
350
|
+
- **`auditEntities` and `idempotencyEntities`**, so every package that ships
|
|
351
|
+
entities exports them as an array the same way it exports its migrations.
|
|
352
|
+
|
|
353
|
+
### Fixed
|
|
354
|
+
|
|
355
|
+
- **A circular import between `logger.module.ts` and the two classes it now
|
|
356
|
+
provides** left `MORTAR_LOGGER` `undefined` at decorator evaluation time, so
|
|
357
|
+
`@Inject(MORTAR_LOGGER)` silently degraded to reflected-type injection and
|
|
358
|
+
Nest reported that it could not resolve `Function`. The tokens moved to a
|
|
359
|
+
leaf module. Under CommonJS this class of bug fails at wiring time, never at
|
|
360
|
+
build time.
|
|
361
|
+
|
|
362
|
+
### Testing
|
|
363
|
+
|
|
364
|
+
`@nestjs/testing` and `unplugin-swc` are now dev dependencies, and the Nest
|
|
365
|
+
modules are exercised by building a real container rather than by inspecting
|
|
366
|
+
the `DynamicModule` object. Every defect above was invisible to a test that
|
|
367
|
+
asserts on `module.providers` and obvious to one that calls `moduleRef.get()`.
|
|
4
368
|
|
|
5
369
|
## 0.2.0
|
|
6
370
|
|
package/README.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
1
|
# @birtalanrobert/context
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The ambient request context: who is asking, which tenant, which request.
|
|
4
|
+
|
|
5
|
+
## What it is
|
|
6
|
+
|
|
7
|
+
An `AsyncLocalStorage` store carried automatically through every `await` in a
|
|
8
|
+
request, so that a service five layers down can know the actor and the tenant
|
|
9
|
+
without either being threaded through five signatures that do not otherwise
|
|
10
|
+
care about them.
|
|
11
|
+
|
|
12
|
+
That threading is not merely tedious — it is the thing that gets skipped, and a
|
|
13
|
+
tenant id skipped once is a query that reads another customer's rows.
|
|
14
|
+
|
|
15
|
+
## Using it in a NestJS application
|
|
16
|
+
|
|
17
|
+
**There is no module to import.** The store is opened by
|
|
18
|
+
`@birtalanrobert/http`'s `ContextMiddleware`, which `HttpModule` applies for
|
|
19
|
+
you, so an API gets this by importing `HttpModule` and nothing else.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { HttpModule } from '@birtalanrobert/http/nestjs';
|
|
23
|
+
|
|
24
|
+
@Module({ imports: [HttpModule.forRoot({})] })
|
|
25
|
+
export class AppModule {}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then, anywhere below it:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { getActor, getTenantId, requireTenantId, getRequestId } from '@birtalanrobert/context';
|
|
32
|
+
|
|
33
|
+
const tenantId = requireTenantId(); // throws if unset, which is the point
|
|
34
|
+
const actor = getActor(); // undefined on an unauthenticated route
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`requireTenantId` throws rather than returning `undefined`, because a query
|
|
38
|
+
built with an absent tenant is a query that quietly returns nothing — or, with
|
|
39
|
+
row-level security switched off, everything.
|
|
40
|
+
|
|
41
|
+
## Outside a request
|
|
42
|
+
|
|
43
|
+
A worker, a scheduled task or a script has no middleware to open a store, so it
|
|
44
|
+
opens one itself:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { runWithContext } from '@birtalanrobert/context';
|
|
48
|
+
|
|
49
|
+
await runWithContext({ tenantId, actor: { id: 'worker', type: 'system' } }, async () => {
|
|
50
|
+
// everything in here sees the same ambient context an HTTP request would
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This is how a job that acts on behalf of a tenant gets the same row-level
|
|
55
|
+
security behaviour as the request that enqueued it.
|
|
56
|
+
|
|
57
|
+
## What it does not do
|
|
58
|
+
|
|
59
|
+
It does not bind the tenant to a database connection. That is
|
|
60
|
+
`runInTenantTransaction` in `@birtalanrobert/tenancy`, and the distinction
|
|
61
|
+
matters: resolving a tenant into ambient context tells _the application_ who is
|
|
62
|
+
asking, while binding tells _Postgres_ — and an unbound read on a table with
|
|
63
|
+
row-level security returns nothing at all.
|
|
64
|
+
|
|
65
|
+
## No dependencies
|
|
66
|
+
|
|
67
|
+
Nothing here imports a framework or a driver. It is used by the API, the worker
|
|
68
|
+
and the shared packages alike, and anything framework-shaped would break at
|
|
69
|
+
least one of them.
|
package/dist/types.d.ts
CHANGED
|
@@ -37,12 +37,25 @@ export interface Actor {
|
|
|
37
37
|
/**
|
|
38
38
|
* `user` is a human with an account; `client` is a link-authenticated party
|
|
39
39
|
* with no account (a guest, a candidate, a tenant of a landlord); `system`
|
|
40
|
-
* is scheduled or internal work; `service` is a machine credential
|
|
40
|
+
* is scheduled or internal work; `service` is a machine credential;
|
|
41
|
+
* `operator` is one of *us* working inside a customer's account.
|
|
42
|
+
*
|
|
43
|
+
* `operator` is separate from `user` because the audit trail has to be able
|
|
44
|
+
* to say which it was. Support access recorded as the customer's own action
|
|
45
|
+
* is worse than no record at all — it is a confident answer to "who opened
|
|
46
|
+
* this?" that names the wrong person, and the customer has no way to tell.
|
|
41
47
|
*/
|
|
42
|
-
readonly type: 'user' | 'client' | 'system' | 'service';
|
|
48
|
+
readonly type: 'user' | 'client' | 'system' | 'service' | 'operator';
|
|
43
49
|
readonly displayName?: string;
|
|
44
50
|
readonly roles?: readonly string[];
|
|
45
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Who is behind the action, when it is not the actor.
|
|
53
|
+
*
|
|
54
|
+
* For the shape where an operator acts *as* a named user, so the trail can
|
|
55
|
+
* say both. Where the operator acts as themselves inside a customer's
|
|
56
|
+
* account — which is the safer shape, because nothing is disguised — the
|
|
57
|
+
* actor's own `type` is `operator` and this stays unset.
|
|
58
|
+
*/
|
|
46
59
|
readonly impersonatedBy?: string;
|
|
47
60
|
}
|
|
48
61
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,UAAU,CAAC;AAEzE,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,UAAU,CAAC;AAEzE,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAC;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;;;;OAOG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC"}
|
package/package.json
CHANGED
package/src/types.ts
CHANGED
|
@@ -39,11 +39,24 @@ export interface Actor {
|
|
|
39
39
|
/**
|
|
40
40
|
* `user` is a human with an account; `client` is a link-authenticated party
|
|
41
41
|
* with no account (a guest, a candidate, a tenant of a landlord); `system`
|
|
42
|
-
* is scheduled or internal work; `service` is a machine credential
|
|
42
|
+
* is scheduled or internal work; `service` is a machine credential;
|
|
43
|
+
* `operator` is one of *us* working inside a customer's account.
|
|
44
|
+
*
|
|
45
|
+
* `operator` is separate from `user` because the audit trail has to be able
|
|
46
|
+
* to say which it was. Support access recorded as the customer's own action
|
|
47
|
+
* is worse than no record at all — it is a confident answer to "who opened
|
|
48
|
+
* this?" that names the wrong person, and the customer has no way to tell.
|
|
43
49
|
*/
|
|
44
|
-
readonly type: 'user' | 'client' | 'system' | 'service';
|
|
50
|
+
readonly type: 'user' | 'client' | 'system' | 'service' | 'operator';
|
|
45
51
|
readonly displayName?: string;
|
|
46
52
|
readonly roles?: readonly string[];
|
|
47
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Who is behind the action, when it is not the actor.
|
|
55
|
+
*
|
|
56
|
+
* For the shape where an operator acts *as* a named user, so the trail can
|
|
57
|
+
* say both. Where the operator acts as themselves inside a customer's
|
|
58
|
+
* account — which is the safer shape, because nothing is disguised — the
|
|
59
|
+
* actor's own `type` is `operator` and this stays unset.
|
|
60
|
+
*/
|
|
48
61
|
readonly impersonatedBy?: string;
|
|
49
62
|
}
|