@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 CHANGED
@@ -1,6 +1,370 @@
1
1
  # Changelog
2
2
 
3
- All packages share a version and are released together.
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
- AsyncLocalStorage request context
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
- /** Set when an operator is impersonating; the audit trail records both. */
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
@@ -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;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@birtalanrobert/context",
3
- "version": "0.2.0",
3
+ "version": "1.1.0",
4
4
  "description": "AsyncLocalStorage request context",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "commonjs",
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
- /** Set when an operator is impersonating; the audit trail records both. */
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
  }