1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
//! Cross-cutting value types, used across every domain module - and the
//! home for primitives (once implemented) reused by both
//! `access_control`'s `AccessToken` and `bootstrap`'s `BootstrapSecret`
//! (`generate_token_secret`, `secret_matches`, `generate_token_id`). See
//! docs/architecture.md §3.2 and `specs/skilj.allium`'s Value Types
//! section, which this module mirrors closely.
use JsonSchema;
use ;
/// Names a payload field to filter, tag, or mark sensitive: a bare
/// top-level name, or a two-segment dotted path reaching one leaf inside
/// the spec's one permitted level of nesting (e.g. `"address.country"`).
/// See the payload schema shape note above `entity CommandType` in
/// `specs/skilj.allium`.
pub type FieldPath = String;
/// One condition on a payload field, as `queryEvents` and projection queries
/// take it: `field` compared with `value` by `operator`.
/// How a [`Filter`] compares a payload field with its value.
/// See `value Metadata` in the spec. `version` and every other
/// sequence-adjacent integer in this codebase is `i64` - see
/// docs/architecture.md §2.2.1.
///
/// `correlation_id`/`causation_id` (issue #18) are `Option`
/// here regardless of the "every stored `Command`/`Event` ends up with a
/// `correlation_id`" guarantee the spec's `CorrelationIdIsAlwaysRecorded`
/// invariant makes - that guarantee is a property of the write path
/// (`event_store::process_command`/`create_external_event`/
/// `create_direct_event`/`create_system_event` all generate one when the
/// caller didn't supply one), not of this type. A row written before
/// this field existed genuinely has neither, and decoding it as
/// `Option::None` rather than inventing a value is the honest read.
/// Deliberately not the OTel trace id (`skilj-rest`/`skilj-graphql`'s own
/// `current_trace_id()`, docs/architecture.md §10b) - see the spec's own
/// note above `value Metadata` for why the two are unrelated: a trace id
/// is ephemeral and exporter-dependent, these are durable and queryable
/// from the store itself.
/// `value` is `None` (absent) rather than the tag being omitted entirely
/// when the mapped payload field was itself absent - see `Tag.value` in
/// the spec for why that's deliberate, not an oversight.
/// Derives a tag from a payload field. A `blind` mapping's tag value is a
/// keyed hash of the field rather than the field itself
/// (docs/architecture.md §205): it may name a sensitive field, and two
/// payloads carry the same tag exactly when their normalized values are
/// equal - a uniqueness check without storing the value. Its key must be
/// declared by a [`crate::plugin::BlindTag`] in the same bounded context;
/// build with [`TagMapping::blind`]. Stored with the type, so every
/// instance knows the tag must be hashed, including one that doesn't
/// declare it and so refuses rather than storing the value in the clear.
/// One item of a command type's consistency query (docs/architecture.md
/// §198, `value QueryItemMapping` in specs/skilj.allium), declared by
/// `CommandType::consistency_query`. Derived per command into a
/// [`QueryItem`], as a [`TagMapping`] is into a [`Tag`]. An event matches
/// the item when its type is one of `event_types` (any type when empty)
/// and it carries every tag `tag_mappings` derives. `latest`, when set,
/// asks for only that many matching events, those with the highest
/// sequences; `None` for every match.
///
/// ```
/// # use skilj_core::shared::{QueryItemMapping, TagMapping};
/// let company = TagMapping::new("company", "company_id");
/// // The company's latest lifecycle event, whatever kind it is.
/// let status = QueryItemMapping::types(&["CompanySignedUp", "CompanyExpired"])
/// .tagged(vec![company])
/// .latest();
/// assert_eq!(status.latest, Some(1));
/// // The requester's last three tickets.
/// let recent = QueryItemMapping::types(&["TicketCreated"]).last(3);
/// assert_eq!(recent.latest, Some(3));
/// // A command whose decide() needs no history: alone in its query.
/// let none = QueryItemMapping::nothing();
/// assert!(none.event_types.is_empty() && none.tag_mappings.is_empty());
/// ```
///
/// An item with `snapshot` set hands `decide_with()` that snapshot's
/// state for the tag value its tag mapping derives, brought up to date,
/// instead of events - see [`QueryItemMapping::snapshot`].
/// A [`QueryItemMapping`] derived from one command's payload - `value
/// QueryItem` in specs/skilj.allium, recorded on `Command.consistency_query`.
/// `subject_field` is never encrypted - it's the plaintext identifier the
/// `EncryptionKey` is looked up or derived by. Only `field` is what gets
/// swapped for ciphertext (at the leaf, for a dotted path - see
/// `protect_sensitive_fields` in `event_store`).
/// See `value PrivateField` in the spec - the private-field mechanism's
/// own three-way visibility rule, standing beside `SensitiveField` rather
/// than inside it: a plain read-time redaction, no `EncryptionKey`
/// anywhere behind it. `capability`/`addressee_field` are each set exactly
/// when `kind` calls for them, checked by `event_store::valid_private_fields`
/// at registration - `None` for whichever the other two kinds leave
/// unused.
/// See `enum PrivateFieldKind` in the spec for what each variant means in
/// full - `Own` (default reader: the record's own creator),
/// `Capability` (default reader: any grant holding `PrivateField.capability`),
/// `Addressed` (default reader: the party `PrivateField.addressee_field`
/// names in the payload). A fourth kind, `Draft` - visible only until an
/// external status changes - is a real, deliberately deferred future
/// kind: it needs a live `Projection` lookup at read time, a materially
/// different evaluability shape from these three, which are decidable
/// from the record and the reading caller's own identity alone. Nothing
/// here is shaped around it and nothing here forecloses it.
/// The whole contract a bounded context's `decide()` returns - see
/// `value CommandDecision` in the spec, and docs/architecture.md
/// §4.1/§5.4/§7.3 for why a rejection is typed data, never a
/// GraphQL/HTTP-level error.
/// One event a `decide()` call wants appended, on acceptance.
// `secret_matches` below was added propagating `bootstrap::create_superadmin`
// - unlike `generate_token_secret`/`generate_token_id` below, it needed no
// caller-supplied-output treatment (it takes strings, not producing an
// opaque one), so it was real from the start rather than deferred.
/// Generates the opaque `id` half of an `AccessToken`/`Role` - 16 random
/// bytes from the OS CSPRNG, hex-encoded (32 lowercase hex characters),
/// matching every other identifier this codebase already treats as an
/// opaque `String` rather than a typed newtype. Not a secret itself -
/// safe to log, appear in a URL path, etc. - only
/// `generate_token_secret` below carries the confidentiality requirement
/// `secret_matches` is built to compare without leaking.
/// Generates the `secret` half of an `AccessToken`/`BootstrapSecret` -
/// 32 random bytes from the OS CSPRNG (256 bits), hex-encoded (64
/// lowercase hex characters). `ring`'s `SystemRandom` reads from the OS
/// CSPRNG, so this is cryptographically unpredictable, not merely unique
/// like `generate_token_id` only needs to be.
/// `N` random bytes from the OS CSPRNG, lower-case hex - the shared body
/// of [`generate_token_id`]/[`generate_token_secret`] above. Uses `ring`
/// (already this crate's cryptographically-secure randomness source, via
/// `encryption`) rather than the `uuid` crate these two used to go
/// through: neither value grew any structure from being spelled as a
/// UUID, only the 32/64 hex characters did, so a plain byte-string is the
/// honest shape.
/// Hashes the `secret` half of an `AccessToken` for storage - see the
/// REST token presentation note above the surfaces in the spec: "the
/// secret half is then verified against that row's stored hash... Both
/// the hashing scheme and the comparison are black boxes here, the same
/// register as generate_token_secret and generate_token_id: the spec
/// owns that the secret is never stored or compared in plaintext, not
/// which primitive does it." `generate_token_secret` already gives 256
/// bits of CSPRNG randomness (see its own doc comment) - high-entropy,
/// unlike a user-chosen password - so a plain cryptographic hash
/// (SHA-256, via `ring`, already a dependency for `encryption`) is the
/// appropriate primitive: brute-forcing a 256-bit secret back out of its
/// hash is infeasible regardless of how fast the hash is, and a slow
/// password KDF (bcrypt/argon2/scrypt) would only add latency to every
/// request's auth check for no security benefit. Base64-encoded
/// (`crate::base64`), the same encoding `encryption::encrypt` already
/// uses for its own opaque byte output.
/// Deterministic (no per-secret salt) - safe here specifically because
/// the input is never a low-entropy human-chosen value a rainbow table
/// could target, unlike a password hash.
/// See the `secret_matches` note above `rule CreateSuperadmin`: compares
/// two secrets in time that depends only on their lengths, never on where
/// they first differ, so a failed presentation leaks nothing about how
/// much of the real secret was guessed correctly. Real rather than
/// deferred - simple enough that there's no trivial/reachable-case split
/// the way `protect_sensitive_fields`/`derive_tags` etc. have. A length
/// mismatch still short-circuits (the length itself isn't the secret -
/// only its content is), but every byte of an equal-length comparison is
/// folded in regardless of an early difference.