@cratis/pi 0.0.1
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/README.md +43 -0
- package/package.json +24 -0
- package/skills/cratis-application-react-specifications/LICENSE +2 -0
- package/skills/cratis-application-react-specifications/SKILL.md +192 -0
- package/skills/cratis-application-slice-specifications/LICENSE +2 -0
- package/skills/cratis-application-slice-specifications/SKILL.md +222 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/LICENSE +2 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +233 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +140 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +112 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +102 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +85 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +87 -0
- package/skills/cratis-arc-command/LICENSE +2 -0
- package/skills/cratis-arc-command/SKILL.md +286 -0
- package/skills/cratis-arc-command/references/command-result.md +111 -0
- package/skills/cratis-arc-command/references/handler-shapes.md +135 -0
- package/skills/cratis-arc-command/references/proxy-generation.md +87 -0
- package/skills/cratis-arc-command/references/read-model-injection.md +97 -0
- package/skills/cratis-arc-command-execution/LICENSE +2 -0
- package/skills/cratis-arc-command-execution/SKILL.md +148 -0
- package/skills/cratis-arc-command-validation/LICENSE +2 -0
- package/skills/cratis-arc-command-validation/SKILL.md +230 -0
- package/skills/cratis-arc-ef-core-migration/LICENSE +2 -0
- package/skills/cratis-arc-ef-core-migration/SKILL.md +231 -0
- package/skills/cratis-arc-observable-query-http/LICENSE +2 -0
- package/skills/cratis-arc-observable-query-http/SKILL.md +157 -0
- package/skills/cratis-arc-query-paging/LICENSE +2 -0
- package/skills/cratis-arc-query-paging/SKILL.md +173 -0
- package/skills/cratis-arc-react-page/LICENSE +2 -0
- package/skills/cratis-arc-react-page/SKILL.md +291 -0
- package/skills/cratis-arc-react-page/references/data-page.md +125 -0
- package/skills/cratis-arc-react-page/references/data-tables.md +136 -0
- package/skills/cratis-arc-react-page/references/dialogs.md +238 -0
- package/skills/cratis-arc-react-page/references/mvvm.md +150 -0
- package/skills/cratis-arc-react-page/references/queries-and-commands.md +200 -0
- package/skills/cratis-chronicle-cli-operations/LICENSE +2 -0
- package/skills/cratis-chronicle-cli-operations/SKILL.md +199 -0
- package/skills/cratis-chronicle-client-dotnet/LICENSE +2 -0
- package/skills/cratis-chronicle-client-dotnet/SKILL.md +433 -0
- package/skills/cratis-chronicle-client-elixir/LICENSE +2 -0
- package/skills/cratis-chronicle-client-elixir/SKILL.md +429 -0
- package/skills/cratis-chronicle-client-kotlin/LICENSE +2 -0
- package/skills/cratis-chronicle-client-kotlin/SKILL.md +498 -0
- package/skills/cratis-chronicle-client-typescript/LICENSE +2 -0
- package/skills/cratis-chronicle-client-typescript/SKILL.md +396 -0
- package/skills/cratis-chronicle-compliance/LICENSE +2 -0
- package/skills/cratis-chronicle-compliance/SKILL.md +246 -0
- package/skills/cratis-chronicle-event-constraints/LICENSE +2 -0
- package/skills/cratis-chronicle-event-constraints/SKILL.md +227 -0
- package/skills/cratis-chronicle-event-modeling/LICENSE +2 -0
- package/skills/cratis-chronicle-event-modeling/SKILL.md +157 -0
- package/skills/cratis-chronicle-event-specifications/LICENSE +2 -0
- package/skills/cratis-chronicle-event-specifications/SKILL.md +200 -0
- package/skills/cratis-chronicle-event-type-migration/LICENSE +2 -0
- package/skills/cratis-chronicle-event-type-migration/SKILL.md +235 -0
- package/skills/cratis-chronicle-mcp-inspection/LICENSE +2 -0
- package/skills/cratis-chronicle-mcp-inspection/SKILL.md +79 -0
- package/skills/cratis-chronicle-mcp-inspection/references/blocked-tools.md +12 -0
- package/skills/cratis-chronicle-mcp-inspection/references/observational-tools.md +8 -0
- package/skills/cratis-chronicle-multi-tenancy/LICENSE +2 -0
- package/skills/cratis-chronicle-multi-tenancy/SKILL.md +198 -0
- package/skills/cratis-chronicle-projection/LICENSE +2 -0
- package/skills/cratis-chronicle-projection/SKILL.md +162 -0
- package/skills/cratis-chronicle-projection/references/fluent-builder.md +126 -0
- package/skills/cratis-chronicle-projection/references/model-bound-attributes.md +118 -0
- package/skills/cratis-chronicle-reactor/LICENSE +2 -0
- package/skills/cratis-chronicle-reactor/SKILL.md +344 -0
- package/skills/cratis-chronicle-read-model/LICENSE +2 -0
- package/skills/cratis-chronicle-read-model/SKILL.md +193 -0
- package/skills/cratis-chronicle-read-model/references/queries.md +142 -0
- package/skills/cratis-chronicle-read-model-specifications/LICENSE +2 -0
- package/skills/cratis-chronicle-read-model-specifications/SKILL.md +176 -0
- package/skills/cratis-chronicle-reducer/LICENSE +2 -0
- package/skills/cratis-chronicle-reducer/SKILL.md +173 -0
- package/skills/cratis-chronicle-web-workbench/LICENSE +2 -0
- package/skills/cratis-chronicle-web-workbench/SKILL.md +177 -0
- package/skills/cratis-cli-terminal-workbench/LICENSE +2 -0
- package/skills/cratis-cli-terminal-workbench/SKILL.md +156 -0
- package/skills/cratis-code-review/LICENSE +2 -0
- package/skills/cratis-code-review/SKILL.md +177 -0
- package/skills/cratis-components-accessibility/LICENSE +2 -0
- package/skills/cratis-components-accessibility/SKILL.md +176 -0
- package/skills/cratis-components-schema-editor/LICENSE +2 -0
- package/skills/cratis-components-schema-editor/SKILL.md +238 -0
- package/skills/cratis-components-stepper-command-dialog/LICENSE +2 -0
- package/skills/cratis-components-stepper-command-dialog/SKILL.md +209 -0
- package/skills/cratis-components-styling/LICENSE +2 -0
- package/skills/cratis-components-styling/SKILL.md +221 -0
- package/skills/cratis-components-toolbar/LICENSE +2 -0
- package/skills/cratis-components-toolbar/SKILL.md +259 -0
- package/skills/cratis-documentation-writing/LICENSE +2 -0
- package/skills/cratis-documentation-writing/SKILL.md +121 -0
- package/skills/cratis-event-model-diagram/LICENSE +2 -0
- package/skills/cratis-event-model-diagram/SKILL.md +142 -0
- package/skills/cratis-fundamentals-concept/LICENSE +2 -0
- package/skills/cratis-fundamentals-concept/SKILL.md +198 -0
- package/skills/cratis-fundamentals-type-discovery/LICENSE +2 -0
- package/skills/cratis-fundamentals-type-discovery/SKILL.md +152 -0
- package/skills/cratis-governed-release-methodology/LICENSE +2 -0
- package/skills/cratis-governed-release-methodology/SKILL.md +297 -0
- package/skills/cratis-lens-browser-extension/LICENSE +2 -0
- package/skills/cratis-lens-browser-extension/SKILL.md +239 -0
- package/skills/cratis-performance-review/LICENSE +2 -0
- package/skills/cratis-performance-review/SKILL.md +139 -0
- package/skills/cratis-screenplay-model-authoring/LICENSE +2 -0
- package/skills/cratis-screenplay-model-authoring/SKILL.md +231 -0
- package/skills/cratis-security-review/LICENSE +2 -0
- package/skills/cratis-security-review/SKILL.md +141 -0
- package/skills/cratis-specification-by-example/LICENSE +2 -0
- package/skills/cratis-specification-by-example/SKILL.md +188 -0
- package/skills/cratis-specifications-csharp/LICENSE +2 -0
- package/skills/cratis-specifications-csharp/SKILL.md +230 -0
- package/skills/cratis-specifications-csharp/references/application-scenarios.md +191 -0
- package/skills/cratis-specifications-csharp/references/csharp-patterns.md +142 -0
- package/skills/cratis-specifications-csharp/references/integration-specs.md +126 -0
- package/skills/cratis-specifications-typescript/LICENSE +2 -0
- package/skills/cratis-specifications-typescript/SKILL.md +202 -0
- package/skills/cratis-specifications-typescript/references/typescript-patterns.md +168 -0
- package/skills/cratis-stage-rendering-and-sandbox/LICENSE +2 -0
- package/skills/cratis-stage-rendering-and-sandbox/SKILL.md +230 -0
- package/skills/cratis-studio-mcp-safety-guidance/LICENSE +2 -0
- package/skills/cratis-studio-mcp-safety-guidance/SKILL.md +69 -0
- package/skills/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +12 -0
- package/skills/cratis-studio-mcp-safety-guidance/references/observational-tools.md +8 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-chronicle-client-typescript
|
|
3
|
+
description: Talk to a Chronicle server from a Node.js or TypeScript application with @cratis/chronicle - reflect-metadata and decorator compiler settings, ChronicleClient and connection strings, @eventType classes, eventLog.append, reactors and reducers dispatched by camelCase method name, model-bound and declarative projections, glob-based artifact discovery, and the lazy connect and keepalive lifecycle. Use when a Node application appends to or observes a Chronicle event store. Do not use for the .NET, Kotlin, or Elixir clients, and do not use for Arc React frontends or generated Arc proxies.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# The Chronicle client for TypeScript
|
|
8
|
+
|
|
9
|
+
`@cratis/chronicle` is a **standalone Node.js client SDK** that speaks gRPC to a
|
|
10
|
+
Chronicle server. It is not a browser library and it is not the Arc TypeScript
|
|
11
|
+
proxy layer — an Arc frontend calls generated command and query proxies over
|
|
12
|
+
HTTP and never sees this package.
|
|
13
|
+
|
|
14
|
+
## Verified product sources
|
|
15
|
+
|
|
16
|
+
This skill is verified against `Cratis/Chronicle.TypeScript` at tag `v4.0.0`
|
|
17
|
+
(commit `bed0b86`), which is the version published on npm.
|
|
18
|
+
|
|
19
|
+
| Package | Version | Where |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| `@cratis/chronicle` | `4.0.0` | `Source/package.json:2` |
|
|
22
|
+
| `@cratis/chronicle.contracts` | `17.0.0` | `Source/package.json` dependency |
|
|
23
|
+
| `@cratis/fundamentals` | `^7.14.0` | **peer dependency** — you install it |
|
|
24
|
+
|
|
25
|
+
> **Do not read the version out of the repository.** `Source/package.json` says
|
|
26
|
+
> `1.0.0`; `publish-version.js` overwrites it at publish time. Take the version
|
|
27
|
+
> from npm.
|
|
28
|
+
|
|
29
|
+
**`v4.0.0` is a major bump and it changed real API.** Guidance written against
|
|
30
|
+
`3.x` is wrong in at least three places — event tags did not exist, reducers took
|
|
31
|
+
two arguments, and two event-store RPCs were renamed. Do not carry `3.x` examples
|
|
32
|
+
forward.
|
|
33
|
+
|
|
34
|
+
## Project setup
|
|
35
|
+
|
|
36
|
+
```shell
|
|
37
|
+
yarn add @cratis/chronicle @cratis/fundamentals reflect-metadata
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`@cratis/fundamentals` is a peer dependency, so it does not arrive on its own.
|
|
41
|
+
`reflect-metadata` must be imported **once, at the entry point**, before any
|
|
42
|
+
decorated class is loaded (`Documentation/getting-started.md:16-27`):
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import 'reflect-metadata';
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The package is **ESM-only** (`"type": "module"`, `Source/package.json:34`) and
|
|
49
|
+
requires decorator support:
|
|
50
|
+
|
|
51
|
+
```jsonc
|
|
52
|
+
// tsconfig.json
|
|
53
|
+
{
|
|
54
|
+
"compilerOptions": {
|
|
55
|
+
"experimentalDecorators": true,
|
|
56
|
+
"emitDecoratorMetadata": true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Both are set in the shipped sample (`Samples/Console/tsconfig.json:12-13`).
|
|
62
|
+
|
|
63
|
+
## Connecting
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import 'reflect-metadata';
|
|
67
|
+
import { ChronicleClient, ChronicleOptions } from '@cratis/chronicle';
|
|
68
|
+
|
|
69
|
+
const client = new ChronicleClient(ChronicleOptions.development());
|
|
70
|
+
try {
|
|
71
|
+
const store = await client.getEventStore('<EventStoreName>');
|
|
72
|
+
// ... use store ...
|
|
73
|
+
} finally {
|
|
74
|
+
client.dispose();
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`IChronicleClient` is three members — `Source/IChronicleClient.ts:16-39`:
|
|
79
|
+
|
|
80
|
+
| Member | Line |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `readonly options: ChronicleOptions` | `:18` |
|
|
83
|
+
| `getEventStore(name, namespace?): Promise<IEventStore>` | `:27` |
|
|
84
|
+
| `getEventStores(): Promise<EventStoreName[]>` | `:33` |
|
|
85
|
+
| `dispose(): void` | `:38` |
|
|
86
|
+
|
|
87
|
+
`ChronicleClient`'s constructor takes exactly one argument —
|
|
88
|
+
`constructor(readonly options: ChronicleOptions)`, `Source/ChronicleClient.ts:58`.
|
|
89
|
+
|
|
90
|
+
**Connecting is implicit and lazy.** There is no `connect()`. The constructor
|
|
91
|
+
starts a health-check watchdog and kicks off artifact discovery; the actual
|
|
92
|
+
connect happens on the first `getEventStore`/`getEventStores` through an internal
|
|
93
|
+
`ensureConnected()`. `getEventStore` also *creates* the store if it does not
|
|
94
|
+
exist — it calls `ensureEventStore` (`Source/ChronicleClient.ts:135`).
|
|
95
|
+
|
|
96
|
+
`dispose()` is **synchronous** (`Source/ChronicleClient.ts:193`), not
|
|
97
|
+
`Symbol.dispose` or `Symbol.asyncDispose`. After disposal every call throws.
|
|
98
|
+
|
|
99
|
+
### Options
|
|
100
|
+
|
|
101
|
+
`ChronicleOptions` has a **private constructor**
|
|
102
|
+
(`Source/ChronicleOptions.ts:67`); use a static factory:
|
|
103
|
+
|
|
104
|
+
- `ChronicleOptions.fromConnectionString(connectionString, options?)` — `:92`
|
|
105
|
+
- `ChronicleOptions.development(options?)` — `:116`
|
|
106
|
+
|
|
107
|
+
The optional bag accepts `clientArtifactsProvider`, `discoveryPatterns`, and
|
|
108
|
+
`defaultSinkTypeId`. `defaultSinkTypeId` defaults to `WellKnownSinks.MongoDB`
|
|
109
|
+
(`:84`).
|
|
110
|
+
|
|
111
|
+
> `programIdentifier`, `softwareVersion`, and `softwareCommit` exist on the type
|
|
112
|
+
> (`:69-71`) but **neither public factory forwards them**, so they cannot be set.
|
|
113
|
+
> Do not write guidance that configures them.
|
|
114
|
+
|
|
115
|
+
### The connection string
|
|
116
|
+
|
|
117
|
+
Schemes `chronicle://` and `chronicle+srv://`, default port `35000`
|
|
118
|
+
(`Source/connection/ChronicleConnectionString.ts`). Query parameters: `apiKey`,
|
|
119
|
+
`disableTls`, `skipTlsValidation`, `certificatePath`, `certificatePassword`,
|
|
120
|
+
`loadBalancer`, `srvNameServer`.
|
|
121
|
+
|
|
122
|
+
Two facts to carry into production guidance:
|
|
123
|
+
|
|
124
|
+
- **`skipTlsValidation` defaults to `true`.** TLS is on but the certificate chain
|
|
125
|
+
is not validated, because a development kernel serves a self-signed
|
|
126
|
+
certificate. Set `?skipTlsValidation=false` against a real server.
|
|
127
|
+
- **A connection string with no credentials silently falls back to the
|
|
128
|
+
development client credentials** (`Source/connection/ChronicleConnection.ts`).
|
|
129
|
+
An anonymous-looking connection string is not anonymous; it is
|
|
130
|
+
`chronicle-dev-client`.
|
|
131
|
+
|
|
132
|
+
> **There is no `./connection` subpath export.** `ChronicleConnectionString`,
|
|
133
|
+
> `ChronicleConnectionStringBuilder`, `AuthenticationMode`, and `LoadBalancerMode`
|
|
134
|
+
> are unreachable from the public entry points — `Source/index.ts` never
|
|
135
|
+
> re-exports them and `Source/package.json`'s `exports` map has no
|
|
136
|
+
> `"./connection"` key. **Application code passes a connection *string*.** Do not
|
|
137
|
+
> write an example that imports a connection-string type.
|
|
138
|
+
|
|
139
|
+
The subpath exports that do exist: `.`, `./events`, `./eventSequences`,
|
|
140
|
+
`./eventStoreSubscriptions`, `./compliance`, `./reactors`, `./reducers`,
|
|
141
|
+
`./seeding`, `./readModels`, `./projections`, `./jobs`, `./webhooks`,
|
|
142
|
+
`./externalServices`, `./identities`, `./observation`, `./sinks`, `./schemas`,
|
|
143
|
+
`./types`, `./artifacts`, `./identity`, `./auditing`, `./correlation`,
|
|
144
|
+
`./transactions`.
|
|
145
|
+
|
|
146
|
+
## Event types
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { eventType } from '@cratis/chronicle';
|
|
150
|
+
|
|
151
|
+
@eventType()
|
|
152
|
+
export class <EventName> {
|
|
153
|
+
constructor(readonly <property>: <Type> = <default>) {}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Seven overloads — `Source/events/eventTypeDecorator.ts:54-60`:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
export function eventType(): ClassDecorator;
|
|
161
|
+
export function eventType(id: string): ClassDecorator;
|
|
162
|
+
export function eventType(id: string, generation: number): ClassDecorator;
|
|
163
|
+
export function eventType(id: string, generation: number, tombstone: boolean): ClassDecorator;
|
|
164
|
+
export function eventType(generation: number): ClassDecorator;
|
|
165
|
+
export function eventType(generation: number, tombstone: boolean): ClassDecorator;
|
|
166
|
+
export function eventType(tombstone: boolean): ClassDecorator;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**The event type id defaults to the class name** — `new EventTypeId(id || constructor.name)`
|
|
170
|
+
at `:91`. Two classes with the same name in different modules collide on the wire.
|
|
171
|
+
|
|
172
|
+
**Give constructor parameters default values.** Member discovery walks explicit
|
|
173
|
+
`@field` declarations from `@cratis/fundamentals` first, then tracked properties,
|
|
174
|
+
then the properties present on a *default-constructed* instance, then
|
|
175
|
+
`design:paramtypes` — `Source/types/TypeIntrospector.ts:52-60`, with the reason
|
|
176
|
+
spelled out at `:40-48`: **esbuild and tsx do not emit `design:type` or
|
|
177
|
+
`design:paramtypes`**. Under `tsx`, a class whose constructor parameters have no
|
|
178
|
+
defaults and no `@field` decorators produces no members and an empty schema. Every
|
|
179
|
+
sample event carries defaults for exactly this reason.
|
|
180
|
+
|
|
181
|
+
Tag an event type at declaration with `@tag(...)` / `@tags(...)` —
|
|
182
|
+
`Source/events/tagDecorator.ts:30`, `:42`.
|
|
183
|
+
|
|
184
|
+
## Appending
|
|
185
|
+
|
|
186
|
+
`IEventLog` is an empty extension of `IEventSequence`
|
|
187
|
+
(`Source/eventSequences/IEventLog.ts:10`); the surface is on `IEventSequence`:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
append(eventSourceId: string, event: object, options?: AppendOptions): Promise<AppendResult>; // :44
|
|
191
|
+
appendMany(eventSourceId: string, events: object[], options?: AppendOptions): Promise<AppendResult[]>; // :53
|
|
192
|
+
appendMany(events: EventForEventSourceId[], options?: AppendOptions): Promise<AppendResult[]>; // :61
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const result = await store.eventLog.append('<event-source-id>', new <EventName>('<value>'));
|
|
197
|
+
if (!result.isSuccess) {
|
|
198
|
+
// result.constraintViolations, result.concurrencyViolation, result.errors
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- **The event source id is a plain `string`.** There is no `EventSourceId` value
|
|
203
|
+
type at this boundary.
|
|
204
|
+
- `AppendResult` — `Source/eventSequences/AppendResult.ts:15-38`: `sequenceNumber`,
|
|
205
|
+
`constraintViolations`, `concurrencyViolation?`, `errors`, `isSuccess`, and
|
|
206
|
+
`waitForCompletion(timeoutMs?)` which defaults to 5000 ms.
|
|
207
|
+
- **`sequenceNumber` wraps a `bigint`.** Read it as `result.sequenceNumber.value`.
|
|
208
|
+
`EventContext.sequenceNumber` is a `bigint` too.
|
|
209
|
+
- `AppendOptions` — `Source/eventSequences/AppendOptions.ts`: `correlationId?`,
|
|
210
|
+
`eventSourceId?`, `concurrencyScope?`, `concurrencyScopes?`, and **`tags?`**
|
|
211
|
+
(`:22-29`, new in `4.0.0`).
|
|
212
|
+
|
|
213
|
+
> **Single `append()` cannot target a stream or a subject.** It hardcodes
|
|
214
|
+
> `EventSourceType: 'Default'`, `EventStreamType: 'Default'`,
|
|
215
|
+
> `EventStreamId: eventSourceId`, and `Subject: eventSourceId`
|
|
216
|
+
> (`Source/eventSequences/EventSequence.ts:100-119`). Only `tags` became a real
|
|
217
|
+
> option in `4.0.0` (`:117`). To target a different stream or subject, use the
|
|
218
|
+
> `EventForEventSourceId[]` overload, whose entries carry `eventStreamType`,
|
|
219
|
+
> `eventStreamId`, `eventSourceType`, `subject`, and `tags`.
|
|
220
|
+
|
|
221
|
+
Correlation id, identity, and the causation chain are picked up from the ambient
|
|
222
|
+
module-level managers (`identityProvider`, `causationManager`,
|
|
223
|
+
`correlationIdManager`) — you do not pass them.
|
|
224
|
+
|
|
225
|
+
Several appends as one unit:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const unitOfWork = store.unitOfWorkManager.begin();
|
|
229
|
+
await store.eventLog.transactional.append('<id>', new <EventName>('<value>'));
|
|
230
|
+
await unitOfWork.commit();
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`ITransactionalEventSequence` returns `Promise<void>`, not an `AppendResult`;
|
|
234
|
+
results are on the unit of work.
|
|
235
|
+
|
|
236
|
+
## Observing
|
|
237
|
+
|
|
238
|
+
### Handlers are found by camelCase method name
|
|
239
|
+
|
|
240
|
+
This is the defining convention of this client, and it is **not** the parameter
|
|
241
|
+
type. `Source/reactors/Reactors.ts:378-381` computes the method name from the
|
|
242
|
+
event class name and looks it up on the prototype:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
const className = (eventTypeClass as Function).name;
|
|
246
|
+
const methodName = className.charAt(0).toLowerCase() + className.slice(1);
|
|
247
|
+
|
|
248
|
+
if (typeof proto[methodName] === 'function') { /* it is a handler */ }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Reducers do the same (`Source/reducers/Reducers.ts:454-460`). A handler whose
|
|
252
|
+
method name does not match is simply never called — **there is no error**.
|
|
253
|
+
|
|
254
|
+
> The docstring at `Source/reactors/reactor.ts:28-29` claims dispatch is by the
|
|
255
|
+
> first parameter's type. **It is wrong**, and it is still wrong at `4.0.0`. The
|
|
256
|
+
> code is the authority; so are the client snippets under `Documentation/`, which
|
|
257
|
+
> state the name rule explicitly.
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { reactor } from '@cratis/chronicle/reactors';
|
|
261
|
+
import { EventContext, EventForEventSourceId } from '@cratis/chronicle';
|
|
262
|
+
|
|
263
|
+
@reactor()
|
|
264
|
+
export class <ReactorName> {
|
|
265
|
+
async <eventName>(event: <EventName>, context: EventContext): Promise<EventForEventSourceId> {
|
|
266
|
+
return {
|
|
267
|
+
eventSourceId: '<other-event-source-id>',
|
|
268
|
+
event: new <SideEffectEvent>(context.eventSourceId)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
A returned event, array, or `EventForEventSourceId` is appended as a side effect;
|
|
275
|
+
a failed side-effect append fails the partition.
|
|
276
|
+
|
|
277
|
+
> **The handler receives `JSON.parse(event.Content)` — a plain object, not an
|
|
278
|
+
> instance of your event class** (`Source/reactors/Reactors.ts:297`, `:320`;
|
|
279
|
+
> `Source/reducers/Reducers.ts:377`). `instanceof` checks and methods on the event
|
|
280
|
+
> class do not work. Read properties only.
|
|
281
|
+
|
|
282
|
+
`@reducer(id?, eventSequenceId?, readModel?, isActive?)` —
|
|
283
|
+
`Source/reducers/reducer.ts:58`. Passing `readModel` also registers that type as a
|
|
284
|
+
read model. **The reducer handler takes three arguments at `4.0.0`** —
|
|
285
|
+
`reducerInstance[methodName](content, currentState, context)`
|
|
286
|
+
(`Source/reducers/Reducers.ts:400`). The third is optional by arity, and a handler
|
|
287
|
+
may be sync or async.
|
|
288
|
+
|
|
289
|
+
Restrict an observer to tagged events with `@filterEventsByTag(value)` —
|
|
290
|
+
`Source/events/filterEventsByTagDecorator.ts:36`.
|
|
291
|
+
|
|
292
|
+
### Read models and projections
|
|
293
|
+
|
|
294
|
+
`store.readModels` — `Source/readModels/IReadModels.ts`: `getInstanceById(type, key, sessionId?)`
|
|
295
|
+
(`:38`), `getInstances(type, eventCount?)` (`:46`), `getSnapshotsById` (`:54`),
|
|
296
|
+
`watch(type): AsyncIterable<ReadModelChangeset<T>>` (`:61`), plus `materialized`
|
|
297
|
+
for paged access.
|
|
298
|
+
|
|
299
|
+
Two projection styles:
|
|
300
|
+
|
|
301
|
+
- **Model-bound** — decorators on the read model, from
|
|
302
|
+
`@cratis/chronicle/projections`: `fromEvent`, `fromEvery`, `fromAll`, `setFrom`,
|
|
303
|
+
`setFromContext`, `setValue`, `join`, `addFrom`, `subtractFrom`, `increment`,
|
|
304
|
+
`decrement`, `count`, `childrenFrom`, `nested`, `clearWith`, `removedWith`,
|
|
305
|
+
`removedWithJoin`, `noAutoMap`, `notRewindable`, `passive`.
|
|
306
|
+
- **Declarative** — `@projection(id?, readModelType?, eventSequenceId?)` on a class
|
|
307
|
+
implementing `IProjectionFor<TReadModel>` with
|
|
308
|
+
`define(builder: IProjectionBuilderFor<TReadModel>): void`.
|
|
309
|
+
|
|
310
|
+
Constraints are `@constraint()` on a class implementing `IConstraint` with a
|
|
311
|
+
`define(builder)`; the builder gives `unique(...)`, `uniqueFor(...)`,
|
|
312
|
+
`perEventSourceType`, `perEventStreamType`, `perEventStreamId`. **There are no
|
|
313
|
+
model-bound constraint decorators in this client** — the class-plus-builder form
|
|
314
|
+
is the only one.
|
|
315
|
+
|
|
316
|
+
## Discovery is a runtime file glob — this is the biggest difference
|
|
317
|
+
|
|
318
|
+
`ChronicleOptions.discoveryPatterns` defaults to
|
|
319
|
+
`['**/*.ts', '!**/*.d.ts', '!**/node_modules', '!**/dist', '!**/build', '!**/.git', '!**/.vscode', '!**/*.spec.ts', '!**/*.test.ts']`
|
|
320
|
+
(`Source/ChronicleOptions.ts:73-83`), and the client `import()`s every matching
|
|
321
|
+
file at construction time so that the decorators run. Registration is into a
|
|
322
|
+
process-wide static map.
|
|
323
|
+
|
|
324
|
+
Two consequences that decide whether an application works at all:
|
|
325
|
+
|
|
326
|
+
1. **The default pattern matches `.ts`, not `.js`.** It works under a
|
|
327
|
+
TypeScript-capable runtime — the sample runs `tsx index.ts` — and a compiled
|
|
328
|
+
`dist/*.js` deployment discovers **nothing** unless `discoveryPatterns` is
|
|
329
|
+
overridden.
|
|
330
|
+
2. **Decorators only register when their module is imported.** Side-effect imports
|
|
331
|
+
of event, reactor, reducer, and projection modules are load-bearing; the sample
|
|
332
|
+
does exactly this with a comment saying why.
|
|
333
|
+
|
|
334
|
+
`EventStore.registerArtifacts()` registers event types first, then everything else
|
|
335
|
+
in parallel, and re-runs for every cached event store on reconnect.
|
|
336
|
+
|
|
337
|
+
## Lifecycle
|
|
338
|
+
|
|
339
|
+
- A health-check watchdog runs `getVersionInfo` every 5 seconds and reconnects on
|
|
340
|
+
failure; the interval handle is `unref()`'d so it does not hold the process open.
|
|
341
|
+
- Reconnect uses exponential backoff with jitter, capped at 30 seconds, rebuilding
|
|
342
|
+
the gRPC channel on every attempt.
|
|
343
|
+
- **Keepalive registration is what keeps observers alive.** Losing it triggers a
|
|
344
|
+
full reconnect, because the kernel tears down the client's observers otherwise.
|
|
345
|
+
- Failed operations are retried once after reconnecting, keyed on gRPC status
|
|
346
|
+
codes 4/13/14 plus a substring match over the error text.
|
|
347
|
+
|
|
348
|
+
> **The client version reported to the kernel is a hardcoded `'1.0.0'`** with a
|
|
349
|
+
> TODO — `Source/ChronicleClient.ts:429`, alongside `ClientType: 'TypeScript'`
|
|
350
|
+
> (`:434`). The Chronicle 17 server-side compatibility check therefore sees a
|
|
351
|
+
> placeholder from this client, and grepping `Source/` for `CheckCompatibility`,
|
|
352
|
+
> `ProtocolVersion`, or descriptor-set handling finds nothing. **Do not tell
|
|
353
|
+
> developers this client negotiates protocol compatibility** — it does not.
|
|
354
|
+
|
|
355
|
+
## No dependency injection
|
|
356
|
+
|
|
357
|
+
There is none, and none is planned in the source: no container integration, no
|
|
358
|
+
`addChronicle`-style registration. Reactors, reducers, projections, constraints,
|
|
359
|
+
and seeders are instantiated by the client itself, so **constructor injection does
|
|
360
|
+
not work**. Use module-scope collaborators, as the shipped sample does.
|
|
361
|
+
|
|
362
|
+
## Common pitfalls
|
|
363
|
+
|
|
364
|
+
| Pitfall | Why it bites |
|
|
365
|
+
| --- | --- |
|
|
366
|
+
| Naming a reactor method after the handler's purpose | Dispatch is by the camelCase event class name; a mismatch is silently never called |
|
|
367
|
+
| `instanceof` on the event inside a handler | It is a `JSON.parse`d plain object, not your class |
|
|
368
|
+
| Event class with no default constructor values under `tsx` | No decorator metadata is emitted, so the schema comes out empty |
|
|
369
|
+
| Two `@eventType()` classes sharing a class name | The id defaults to the class name and they collide |
|
|
370
|
+
| Deploying compiled `dist/*.js` with default discovery | The default glob matches `**/*.ts`; nothing is discovered |
|
|
371
|
+
| Omitting a side-effect import of an artifact module | Its decorator never runs, so it is never registered |
|
|
372
|
+
| Forgetting `import 'reflect-metadata'` at the entry point | Decorators do not work at runtime |
|
|
373
|
+
| Forgetting to install `@cratis/fundamentals` | It is a peer dependency and is not installed for you |
|
|
374
|
+
| Importing `ChronicleConnectionString` | There is no `./connection` export; pass a string |
|
|
375
|
+
| Expecting single `append()` to set a stream or subject | Those fields are hardcoded; use the `EventForEventSourceId[]` overload |
|
|
376
|
+
| A connection string with no credentials | Silently falls back to the development client credentials |
|
|
377
|
+
| Shipping the default TLS behavior | `skipTlsValidation` defaults to `true` |
|
|
378
|
+
| Constructor-injecting a dependency into a reactor | There is no DI; the client constructs it |
|
|
379
|
+
| Carrying a `3.x` example forward | Tags, the reducer context parameter, and two RPC names changed in `4.0.0` |
|
|
380
|
+
| Reading `result.sequenceNumber` as a number | It is a `bigint` behind `.value` |
|
|
381
|
+
|
|
382
|
+
## Verify
|
|
383
|
+
|
|
384
|
+
- The installed version is the one you intended, taken from npm, and
|
|
385
|
+
`@cratis/fundamentals` is installed alongside it.
|
|
386
|
+
- `reflect-metadata` is imported exactly once, at the entry point.
|
|
387
|
+
- `experimentalDecorators` and `emitDecoratorMetadata` are both on.
|
|
388
|
+
- Every event class either has `@field` declarations or default constructor
|
|
389
|
+
values, and its schema is non-empty at runtime.
|
|
390
|
+
- `discoveryPatterns` matches the files that actually ship in the deployed
|
|
391
|
+
artifact.
|
|
392
|
+
- Every reactor and reducer handler is named as the exact camelCase of its event
|
|
393
|
+
class, and an appended event demonstrably reaches it.
|
|
394
|
+
- A production connection string carries credentials and `skipTlsValidation=false`.
|
|
395
|
+
- `client.dispose()` runs on shutdown.
|
|
396
|
+
- Lint, `tsc`, and the test suite are clean against the verified package version.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-chronicle-compliance
|
|
3
|
+
description: Model personal data in Chronicle with [PII] and [Subject], and erase it through crypto-shredding with IPIIManager. Use when an event or read model carries data about a natural person, when a subject must be identified for erasure, when a right-to-erasure request must be executed, or when redacting a stored event. Do not use for authorization, authentication, secret handling, or generic data-protection policy.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Chronicle compliance and personal data
|
|
8
|
+
|
|
9
|
+
Chronicle keeps events forever, so personal data cannot be deleted from the log.
|
|
10
|
+
Chronicle protects it by encrypting each marked value under a key owned by the
|
|
11
|
+
subject, and erases it by destroying that key. Decide the subject before you
|
|
12
|
+
decide the event shape — the subject is what erasure operates on.
|
|
13
|
+
|
|
14
|
+
## Verified product sources
|
|
15
|
+
|
|
16
|
+
This skill is verified against this exact public release:
|
|
17
|
+
|
|
18
|
+
| Package | Version | Purpose |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `Cratis.Chronicle` | `16.45.3` | `Cratis.Chronicle.Compliance.GDPR`, `Cratis.Chronicle.SubjectAttribute`, `IEventSequence.Redact`, `IReadModels.Release` |
|
|
21
|
+
| `Cratis.Chronicle.CodeAnalysis` | `16.45.3` | `CHR0026`, `CHR0034`, `CHR0035`, `CHR0043`, `CHR0046` |
|
|
22
|
+
|
|
23
|
+
Reverify product sources before claiming support for another version.
|
|
24
|
+
|
|
25
|
+
## Mark personal data with `[PII]`
|
|
26
|
+
|
|
27
|
+
`Cratis.Chronicle.Compliance.GDPR.PIIAttribute` marks a value as personal data.
|
|
28
|
+
It targets a class, a property, or a constructor parameter, and it is not
|
|
29
|
+
repeatable.
|
|
30
|
+
|
|
31
|
+
```csharp
|
|
32
|
+
using Cratis.Chronicle.Compliance;
|
|
33
|
+
using Cratis.Chronicle.Compliance.GDPR;
|
|
34
|
+
|
|
35
|
+
[EventType]
|
|
36
|
+
public record <PersonEventName>(
|
|
37
|
+
[property: PII] string <PersonalPropertyName>,
|
|
38
|
+
<NonPersonalType> <BusinessPropertyName>);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
On a positional record the attribute must be written as `[property: PII]` or
|
|
42
|
+
placed on the constructor parameter; Chronicle looks for it on the property, the
|
|
43
|
+
property's declaring type, the property's own type, and the matching primary
|
|
44
|
+
constructor parameter.
|
|
45
|
+
|
|
46
|
+
Marking the concept type itself makes the value personal everywhere it appears,
|
|
47
|
+
which is usually what you want for an email, a phone number, or a personal name:
|
|
48
|
+
|
|
49
|
+
```csharp
|
|
50
|
+
[PII]
|
|
51
|
+
[ComplianceDetails("<why this value is collected and under what basis>")]
|
|
52
|
+
public record <ConceptName>(string Value) : ConceptAs<string>(Value);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`PIIAttribute` accepts an optional `details` string, **but Chronicle never reads
|
|
56
|
+
it.** The rationale that actually reaches the event schema comes from
|
|
57
|
+
`Cratis.Chronicle.Compliance.ComplianceDetailsAttribute`. Write `[PII]` for the
|
|
58
|
+
marker and `[ComplianceDetails("...")]` for the reason; do not write
|
|
59
|
+
`[PII("reason")]` and expect the reason to be recorded.
|
|
60
|
+
|
|
61
|
+
`PII` is the only compliance category Chronicle defines — there is exactly one
|
|
62
|
+
well-known `ComplianceMetadataType`. Chronicle has no `[Redact]`, `[Erase]`,
|
|
63
|
+
`[Anonymize]`, `[Encrypt]`, or `[NotAudited]` attribute; do not write one into
|
|
64
|
+
an example. Redaction is an API call, not an annotation.
|
|
65
|
+
|
|
66
|
+
Mark only genuinely personal values. Business metadata, identifiers of
|
|
67
|
+
non-persons, and amounts stay unmarked — every marked value costs an encryption
|
|
68
|
+
round trip and becomes unreadable the moment the subject is erased.
|
|
69
|
+
|
|
70
|
+
## Decide the subject
|
|
71
|
+
|
|
72
|
+
The subject is the natural person a value belongs to, and it is the unit of
|
|
73
|
+
erasure. `Cratis.Chronicle.SubjectAttribute` marks the value that supplies it.
|
|
74
|
+
The attribute is a pure marker with no arguments and targets a property or a
|
|
75
|
+
constructor parameter.
|
|
76
|
+
|
|
77
|
+
For an appended event, Chronicle uses the explicit subject when one is supplied
|
|
78
|
+
and otherwise falls back to the event source id. That default is the shape you
|
|
79
|
+
want: a person-per-stream model needs no `[Subject]` at all.
|
|
80
|
+
|
|
81
|
+
```csharp
|
|
82
|
+
[EventType]
|
|
83
|
+
public record <PersonEventName>(
|
|
84
|
+
[property: Subject] <SubjectValueType> <SubjectPropertyName>,
|
|
85
|
+
[property: PII] string <PersonalPropertyName>);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Add `[Subject]` only when the person is **not** the event source — for example
|
|
89
|
+
an event on an order stream that carries the customer's personal data. A
|
|
90
|
+
reactor can supply the subject for its side-effect events by implementing
|
|
91
|
+
`Cratis.Chronicle.Events.ICanProvideSubject`, whose single member is
|
|
92
|
+
`Subject GetSubject()`.
|
|
93
|
+
|
|
94
|
+
`Cratis.Chronicle.Subject` is `record Subject(string Value) : ConceptAs<string>`
|
|
95
|
+
with a `NotSet` value, an `IsSet` test, and implicit conversion from `string`,
|
|
96
|
+
`Guid`, and `EventSourceId`.
|
|
97
|
+
|
|
98
|
+
For a read model instance, Chronicle resolves the subject from the `[Subject]`
|
|
99
|
+
property that has a value, then the `[Subject]` constructor parameter, then a
|
|
100
|
+
property named `Id`. A stored document carries one default subject; keep one
|
|
101
|
+
person's personal data in one document rather than mixing several people into
|
|
102
|
+
one row.
|
|
103
|
+
|
|
104
|
+
### Do not put `[Subject]` or `[PII]` on the stream identity
|
|
105
|
+
|
|
106
|
+
An `EventSourceId<T>`-derived value already is both the key and the compliance
|
|
107
|
+
subject.
|
|
108
|
+
|
|
109
|
+
- `[Key]` or `[Subject]` on it is redundant — analyzer **CHR0026** (warning).
|
|
110
|
+
- `[PII]` on it is rejected — analyzer **CHR0034** (error), and at runtime
|
|
111
|
+
`Cratis.Chronicle.Compliance.GDPR.PIINotSupportedOnEventSourceId`.
|
|
112
|
+
|
|
113
|
+
Chronicle cannot encrypt an event source id. When the natural identifier is
|
|
114
|
+
itself sensitive, use a random surrogate stream id and carry the sensitive value
|
|
115
|
+
as a `[PII]` property.
|
|
116
|
+
|
|
117
|
+
### Reserved read-model property names
|
|
118
|
+
|
|
119
|
+
Chronicle stamps `__subject` and `__subjects` onto stored read-model documents.
|
|
120
|
+
Analyzer **CHR0035** (error) rejects a `[ReadModel]` that declares `_subject`,
|
|
121
|
+
`__subject`, or `__subjects`. Rename the property.
|
|
122
|
+
|
|
123
|
+
## Erase a subject
|
|
124
|
+
|
|
125
|
+
`IEventStore.PII` exposes `Cratis.Chronicle.Compliance.GDPR.IPIIManager`:
|
|
126
|
+
|
|
127
|
+
```csharp
|
|
128
|
+
Task DeleteEncryptionKeyFor(EncryptionKeyIdentifier identifier);
|
|
129
|
+
Task AllowNewEncryptionKeyFor(EncryptionKeyIdentifier identifier);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`EncryptionKeyIdentifier` is a `ConceptAs<string>` with implicit conversion from
|
|
133
|
+
`string`, so the subject value can be passed directly:
|
|
134
|
+
|
|
135
|
+
```csharp
|
|
136
|
+
var eventStore = await chronicleClient.GetEventStore("<EventStoreName>");
|
|
137
|
+
await eventStore.PII.DeleteEncryptionKeyFor("<subject-value>");
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
What this does and does not do:
|
|
141
|
+
|
|
142
|
+
- It destroys the subject's key and records an **erasure fence** so a later
|
|
143
|
+
write cannot resurrect a key for that subject. The fence is monotonic and is
|
|
144
|
+
recorded before any key is deleted.
|
|
145
|
+
- The events stay in the log. Their `[PII]` values remain as unreadable
|
|
146
|
+
ciphertext — this is crypto-shredding, not deletion.
|
|
147
|
+
- Reading an erased value yields an **empty string** rather than an exception,
|
|
148
|
+
so queries and read models over an erased subject keep working. An erased
|
|
149
|
+
container value comes back as an empty object or array.
|
|
150
|
+
- Appending a new `[PII]` value for an erased subject **fails** with
|
|
151
|
+
`EncryptionKeyErased`. It does not quietly mint a new key. Call
|
|
152
|
+
`AllowNewEncryptionKeyFor` deliberately if the subject must be re-enrolled.
|
|
153
|
+
- A partially completed erasure throws `EncryptionKeyErasureIncomplete`, which
|
|
154
|
+
carries the individual failures. Treat it as unfinished and retry — the fence
|
|
155
|
+
is already in place, so retrying is safe.
|
|
156
|
+
|
|
157
|
+
**Erasure is scoped to one namespace.** There is no cross-namespace erasure. In
|
|
158
|
+
a multi-tenant deployment, an erasure request that spans tenants is one call per
|
|
159
|
+
tenant namespace, and completing only some of them is an incomplete erasure.
|
|
160
|
+
|
|
161
|
+
## Redact a stored event
|
|
162
|
+
|
|
163
|
+
Erasure removes a subject's readability. Redaction removes a specific event's
|
|
164
|
+
content. `Cratis.Chronicle.EventSequences.IEventSequence` exposes both forms:
|
|
165
|
+
|
|
166
|
+
```csharp
|
|
167
|
+
Task Redact(EventSequenceNumber sequenceNumber, RedactionReason reason);
|
|
168
|
+
Task Redact(EventSourceId eventSourceId, RedactionReason reason, params Type[] clrEventTypes);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`RedactionReason` is a `ConceptAs<string>` with an `Unknown` value and implicit
|
|
172
|
+
conversion from `string`. Chronicle records the redaction itself as an
|
|
173
|
+
`EventRedacted` event, so the fact that a redaction happened stays auditable.
|
|
174
|
+
|
|
175
|
+
Use redaction for content that should never have been appended. Use key erasure
|
|
176
|
+
for a subject exercising a right to erasure. They are different mechanisms and
|
|
177
|
+
one does not imply the other.
|
|
178
|
+
|
|
179
|
+
## Read personal data back
|
|
180
|
+
|
|
181
|
+
The kernel decrypts on the paths it owns, so projections and observers see plain
|
|
182
|
+
values. When a read-model instance is fetched outside those paths, decrypt it
|
|
183
|
+
explicitly through `Cratis.Chronicle.ReadModels.IReadModels`:
|
|
184
|
+
|
|
185
|
+
```csharp
|
|
186
|
+
Task<TReadModel> Release<TReadModel>(TReadModel instance);
|
|
187
|
+
Task<IEnumerable<TReadModel>> Release<TReadModel>(IEnumerable<TReadModel> instances);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`Release` degrades quietly by design: it returns the instance unchanged when no
|
|
191
|
+
subject can be derived, when the type carries no compliance metadata, or when
|
|
192
|
+
decryption fails. It logs, it does not throw. Do not treat a value that came
|
|
193
|
+
back looking encrypted as proof the data is intact — check the subject
|
|
194
|
+
resolution first.
|
|
195
|
+
|
|
196
|
+
The asymmetry is deliberate and worth knowing: **applying** compliance on append
|
|
197
|
+
fails loudly, **releasing** it on read fails quietly.
|
|
198
|
+
|
|
199
|
+
## Setup
|
|
200
|
+
|
|
201
|
+
An ASP.NET Core host that calls `builder.AddCratisChronicle()` needs no
|
|
202
|
+
compliance setup — `AddCompliance()` is already called for you, and the raw
|
|
203
|
+
client path discovers the metadata providers by convention. The developer-facing
|
|
204
|
+
requirement is only to annotate the types.
|
|
205
|
+
|
|
206
|
+
Key storage is a hosting decision, not an application one. Chronicle ships
|
|
207
|
+
MongoDB (the default), SQL, in-memory, HashiCorp Vault, and Azure Key Vault key
|
|
208
|
+
storage, configured under `Cratis:Chronicle:Compliance:Encryption`.
|
|
209
|
+
|
|
210
|
+
> **Operational trap.** Switching to a dedicated compliance key store without
|
|
211
|
+
> setting `MigrateFromDefaultStorage` leaves every existing key unreachable.
|
|
212
|
+
> Every `[PII]` value then reads back as an empty string, with no exception and
|
|
213
|
+
> no log — byte-for-byte indistinguishable from a completed erasure. Verify the
|
|
214
|
+
> migration flag before switching stores, and confirm a known subject still
|
|
215
|
+
> reads after the switch.
|
|
216
|
+
|
|
217
|
+
## Modeling rules that follow from erasure
|
|
218
|
+
|
|
219
|
+
- Prefer **one subject per event stream** for person-level personal data. The
|
|
220
|
+
default subject is then correct everywhere and erasure is one call.
|
|
221
|
+
- A key redirected away from the event's own stream can carry a `[PII]` value
|
|
222
|
+
across a compliance subject boundary — analyzers **CHR0043** and **CHR0046**
|
|
223
|
+
(warnings) flag that shape. Treat the warning as a modeling question, not
|
|
224
|
+
noise: which person owns the resulting document?
|
|
225
|
+
- Bearer tokens, magic links, and signed URLs are secrets, not durable facts.
|
|
226
|
+
Store a keyed hash or an opaque reference. Chronicle has no attribute that
|
|
227
|
+
withholds a secret from the log.
|
|
228
|
+
- If a subject boundary cannot be made person-level without changing product
|
|
229
|
+
behavior, stop and surface the trade-off before implementing.
|
|
230
|
+
|
|
231
|
+
## Verify
|
|
232
|
+
|
|
233
|
+
- Every value that identifies or describes a natural person carries `[PII]`;
|
|
234
|
+
business metadata does not.
|
|
235
|
+
- Rationale is written with `[ComplianceDetails]`, not with a `[PII]` argument.
|
|
236
|
+
- The subject is explicit wherever the person is not the event source, and
|
|
237
|
+
absent where the event source already is the person.
|
|
238
|
+
- No `[Key]`, `[Subject]`, or `[PII]` sits on an `EventSourceId<T>` value.
|
|
239
|
+
- No read model declares `_subject`, `__subject`, or `__subjects`.
|
|
240
|
+
- One stored read-model document holds one person's personal data.
|
|
241
|
+
- Erasure is executed per namespace, and an incomplete erasure is retried rather
|
|
242
|
+
than reported as done.
|
|
243
|
+
- Reads that must show personal data outside the kernel's own paths call
|
|
244
|
+
`Release`, and an unchanged instance is investigated rather than assumed.
|
|
245
|
+
- The build is clean with zero `CHR00xx` diagnostics outstanding, and the
|
|
246
|
+
relevant specifications pass against the verified package version.
|