@ccmsg/protocol 2.2.0 → 2.4.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/src/errors.ts CHANGED
@@ -79,6 +79,16 @@ export const ERROR_CODES = [
79
79
  * spend it, is not one this mesh knows or could reach just now. The client
80
80
  * asks for a fresh one, which the instance it is talking to can issue. */
81
81
  "auth_unknown_issuer",
82
+ /** What the caller asked to remove is what they are using to ask. Removing
83
+ * the ownership of the instance this connection is on, or the credential this
84
+ * session was authenticated with, would be cutting the branch underneath — so
85
+ * it is refused rather than half-applied.
86
+ *
87
+ * Apart from `forbidden` because nothing about the caller's standing is
88
+ * wrong: it is theirs to remove, and doing it from somewhere else is all it
89
+ * takes. Stated as a code of its own so a client can say that rather than
90
+ * showing a refusal it cannot explain. */
91
+ "auth_in_use",
82
92
  // --- translate ---
83
93
  /** The helper process is present but failed on this call. (Its absence is
84
94
  * `capability_unavailable` instead.) */
@@ -12,8 +12,14 @@ import type {
12
12
  AuthRegisterResponse,
13
13
  AuthResolveRequest,
14
14
  AuthResolveResponse,
15
- AuthRotateRequest,
16
- AuthRotateResponse,
15
+ AuthAccountReadRequest,
16
+ AuthAccountReadResponse,
17
+ AuthCredentialRemoveRequest,
18
+ AuthCredentialRemoveResponse,
19
+ AuthOwnershipRemoveRequest,
20
+ AuthOwnershipRemoveResponse,
21
+ AuthEnrollRequest,
22
+ AuthEnrollResponse,
17
23
  } from "../common/auth.ts";
18
24
  import type {
19
25
  HelloInstanceRequest,
@@ -36,7 +42,18 @@ import type {
36
42
  } from "../common/topics.ts";
37
43
  import { FIXTURE_IDS, FIXTURE_NOW } from "./ids.ts";
38
44
 
39
- const { sid, instance, other_instance, endpoint, other_endpoint, webui, request_id } = FIXTURE_IDS;
45
+ const {
46
+ sid,
47
+ instance,
48
+ other_instance,
49
+ endpoint,
50
+ other_endpoint,
51
+ origin,
52
+ same_site_origin,
53
+ hosting_endpoint,
54
+ user: USER,
55
+ request_id,
56
+ } = FIXTURE_IDS;
40
57
 
41
58
  export const HELLO_SESSION_REQUEST: Static<typeof HelloSessionRequest> = {
42
59
  request_id,
@@ -176,11 +193,8 @@ const CHALLENGE = {
176
193
  };
177
194
 
178
195
  const ACCESS = { value: "YWNjZXNzLXRva2Vu", expires_at: FIXTURE_NOW + 10_000_000 };
179
- const REFRESH = { value: "cmVmcmVzaA", expires_at: FIXTURE_NOW + 600_000_000 };
180
- const REGISTER_TOKEN = "eyJhbGciOiJIUzI1NiJ9.e30.c2ln";
181
- const REGISTER_CODE = "048213";
182
- const SUBJECT = "personal-1";
183
-
196
+ const ENROLL_TOKEN = "eyJhbGciOiJIUzI1NiJ9.e30.c2ln";
197
+ const ENROLL_CODE = "048213";
184
198
  export const AUTH_CHALLENGE_REQUEST: Static<typeof AuthChallengeRequest> = {
185
199
  request_id,
186
200
  op: "auth.challenge",
@@ -195,8 +209,8 @@ export const AUTH_CHALLENGE_RESPONSE: Static<typeof AuthChallengeResponse> = {
195
209
  export const AUTH_REGISTER_REQUEST: Static<typeof AuthRegisterRequest> = {
196
210
  request_id,
197
211
  op: "auth.register",
198
- token: REGISTER_TOKEN,
199
- code: REGISTER_CODE,
212
+ token: ENROLL_TOKEN,
213
+ code: ENROLL_CODE,
200
214
  device_label: "work laptop",
201
215
  challenge: CHALLENGE,
202
216
  credential: {
@@ -210,7 +224,7 @@ export const AUTH_REGISTER_REQUEST: Static<typeof AuthRegisterRequest> = {
210
224
  export const AUTH_REGISTER_RESPONSE: Static<typeof AuthRegisterResponse> = {
211
225
  ok: true,
212
226
  request_id,
213
- sub: SUBJECT,
227
+ user: USER,
214
228
  access: ACCESS,
215
229
  };
216
230
 
@@ -223,14 +237,38 @@ export const AUTH_ASSERT_REQUEST: Static<typeof AuthAssertRequest> = {
223
237
  client_data_json: "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0In0",
224
238
  authenticator_data: "YXV0aC1kYXRh",
225
239
  signature: "c2lnbmF0dXJl",
226
- user_handle: "dXNlci1oYW5kbGU",
240
+ user_handle: USER,
227
241
  },
228
242
  };
229
243
 
230
244
  export const AUTH_ASSERT_RESPONSE: Static<typeof AuthAssertResponse> = {
231
245
  ok: true,
232
246
  request_id,
233
- sub: SUBJECT,
247
+ user: USER,
248
+ access: ACCESS,
249
+ };
250
+
251
+ /** Taking a second instance as one's own: the same token and digits a
252
+ * registration carries, answered with the passkey that already exists. */
253
+ export const AUTH_ENROLL_REQUEST: Static<typeof AuthEnrollRequest> = {
254
+ request_id,
255
+ op: "auth.enroll",
256
+ token: ENROLL_TOKEN,
257
+ code: ENROLL_CODE,
258
+ challenge: CHALLENGE,
259
+ credential: {
260
+ raw_id: "Y3JlZC1pZA",
261
+ client_data_json: "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0In0",
262
+ authenticator_data: "YXV0aC1kYXRh",
263
+ signature: "c2lnbmF0dXJl",
264
+ user_handle: USER,
265
+ },
266
+ };
267
+
268
+ export const AUTH_ENROLL_RESPONSE: Static<typeof AuthEnrollResponse> = {
269
+ ok: true,
270
+ request_id,
271
+ user: USER,
234
272
  access: ACCESS,
235
273
  };
236
274
 
@@ -243,7 +281,7 @@ export const AUTH_TOKEN_REFRESH_REQUEST: Static<typeof AuthTokenRefreshRequest>
243
281
  export const AUTH_TOKEN_REFRESH_RESPONSE: Static<typeof AuthTokenRefreshResponse> = {
244
282
  ok: true,
245
283
  request_id,
246
- sub: SUBJECT,
284
+ user: USER,
247
285
  access: ACCESS,
248
286
  };
249
287
 
@@ -259,32 +297,137 @@ export const AUTH_EXTEND_RESPONSE: Static<typeof AuthExtendResponse> = {
259
297
  auth_expires_at: FIXTURE_NOW + 20_000_000,
260
298
  };
261
299
 
300
+ /** The three things a person has, read back at once: who they are, what answers
301
+ * for them at each origin, and the instances they own. Two instances here, one
302
+ * of them granted by nobody (they made it) and one added by an owner. */
303
+ export const AUTH_ACCOUNT_READ_REQUEST: Static<typeof AuthAccountReadRequest> = {
304
+ request_id,
305
+ op: "auth.account.read",
306
+ };
307
+
308
+ export const AUTH_ACCOUNT_READ_RESPONSE: Static<typeof AuthAccountReadResponse> = {
309
+ ok: true,
310
+ request_id,
311
+ user: {
312
+ kind: "user",
313
+ user: USER,
314
+ display_name: "kawaz",
315
+ created_at: FIXTURE_NOW - 900_000,
316
+ },
317
+ credentials: [
318
+ {
319
+ kind: "credential",
320
+ user: USER,
321
+ credential_id: "Y3JlZC1pZA",
322
+ origin,
323
+ sign_count: 0,
324
+ issued_label: "for kawaz",
325
+ device_label: "work laptop",
326
+ registered_at: FIXTURE_NOW - 600_000,
327
+ registered_ip: "203.0.113.7",
328
+ registered_user_agent: "Mozilla/5.0",
329
+ last_used_at: FIXTURE_NOW,
330
+ last_used_ip: "203.0.113.7",
331
+ last_used_user_agent: "Mozilla/5.0",
332
+ },
333
+ {
334
+ kind: "credential",
335
+ user: USER,
336
+ credential_id: "Y3JlZC1pZC0y",
337
+ origin: same_site_origin,
338
+ device_label: "phone",
339
+ registered_at: FIXTURE_NOW - 300_000,
340
+ },
341
+ ],
342
+ instances: [
343
+ {
344
+ instance,
345
+ endpoint,
346
+ granted_at: FIXTURE_NOW - 600_000,
347
+ granted_by: { kind: "instance", instance },
348
+ },
349
+ {
350
+ instance: other_instance,
351
+ endpoint: other_endpoint,
352
+ granted_at: FIXTURE_NOW - 60_000,
353
+ granted_by: { kind: "user", user: USER },
354
+ },
355
+ ],
356
+ };
357
+
358
+ /** Giving up an instance, named from another one the person owns — the
359
+ * connection's own instance is the one call that is refused. */
360
+ export const AUTH_OWNERSHIP_REMOVE_REQUEST: Static<typeof AuthOwnershipRemoveRequest> = {
361
+ request_id,
362
+ op: "auth.ownership.remove",
363
+ instance: other_instance,
364
+ };
365
+
366
+ export const AUTH_OWNERSHIP_REMOVE_RESPONSE: Static<typeof AuthOwnershipRemoveResponse> = {
367
+ ok: true,
368
+ request_id,
369
+ };
370
+
371
+ /** Removing a passkey that is not the one this session authenticated with. */
372
+ export const AUTH_CREDENTIAL_REMOVE_REQUEST: Static<typeof AuthCredentialRemoveRequest> = {
373
+ request_id,
374
+ op: "auth.credential.remove",
375
+ credential_id: "Y3JlZC1pZC0y",
376
+ };
377
+
378
+ export const AUTH_CREDENTIAL_REMOVE_RESPONSE: Static<typeof AuthCredentialRemoveResponse> = {
379
+ ok: true,
380
+ request_id,
381
+ };
382
+
262
383
  export const AUTH_RESOLVE_REQUEST = {
263
384
  request_id,
264
385
  op: "auth.resolve",
265
386
  to_instance: instance,
266
- kind: "register",
267
- token: REGISTER_TOKEN,
268
- code: REGISTER_CODE,
387
+ kind: "claims",
388
+ token: ENROLL_TOKEN,
389
+ code: ENROLL_CODE,
269
390
  } satisfies Static<typeof AuthResolveRequest>;
270
391
 
392
+ /** What the issuer answers about a URL that makes a person. It names the user
393
+ * the credential will be created against, and posts to the address in front of
394
+ * the instances rather than to the issuer's own — whichever of them the answer
395
+ * lands on completes it. */
271
396
  export const AUTH_RESOLVE_RESPONSE = {
272
397
  ok: true,
273
398
  request_id,
274
- kind: "register",
399
+ kind: "claims",
275
400
  claims: {
276
401
  iss: instance,
277
- sub: SUBJECT,
278
- unit: "personal",
279
- endpoint,
280
- webui,
402
+ purpose: "create_user",
403
+ instance,
404
+ origin,
405
+ endpoint: hosting_endpoint,
281
406
  expires_at: FIXTURE_NOW + 600_000,
282
407
  jti: "01J9Z3W2Q",
283
- user_id: "dXNlci1oYW5kbGU",
408
+ user: USER,
284
409
  issued_label: "for kawaz",
285
410
  },
286
411
  } satisfies Static<typeof AuthResolveResponse>;
287
412
 
413
+ /** The other purpose: a URL that adds an instance to whoever asserts. It names
414
+ * no user — who arrives is what the assertion says. */
415
+ export const AUTH_RESOLVE_ADD_OWNER_RESPONSE = {
416
+ ok: true,
417
+ request_id,
418
+ kind: "claims",
419
+ claims: {
420
+ iss: other_instance,
421
+ purpose: "add_owner",
422
+ instance: other_instance,
423
+ origin,
424
+ endpoint: hosting_endpoint,
425
+ expires_at: FIXTURE_NOW + 600_000,
426
+ jti: "01J9Z3W2R",
427
+ issued_label: "nuc, for kawaz",
428
+ },
429
+ } satisfies Static<typeof AuthResolveResponse>;
430
+
288
431
  /** The other half of the resolve union: spending a challenge, which answers
289
432
  * nothing beyond having spent it. */
290
433
  export const AUTH_RESOLVE_CHALLENGE_REQUEST = {
@@ -300,21 +443,3 @@ export const AUTH_RESOLVE_CHALLENGE_RESPONSE = {
300
443
  request_id,
301
444
  kind: "challenge",
302
445
  } satisfies Static<typeof AuthResolveResponse>;
303
-
304
- export const AUTH_ROTATE_REQUEST: Static<typeof AuthRotateRequest> = {
305
- request_id,
306
- op: "auth.rotate",
307
- to_instance: instance,
308
- refresh_token: REFRESH.value,
309
- reason: "reconnect",
310
- ip: "203.0.113.7",
311
- user_agent: "Mozilla/5.0",
312
- };
313
-
314
- export const AUTH_ROTATE_RESPONSE: Static<typeof AuthRotateResponse> = {
315
- ok: true,
316
- request_id,
317
- sub: SUBJECT,
318
- access: ACCESS,
319
- refresh: REFRESH,
320
- };
@@ -9,17 +9,29 @@ export const FIXTURE_IDS = {
9
9
  other_instance: "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10
10
  endpoint: "https://mba.example.ts.net/ccmsg/personal/",
11
11
  other_endpoint: "https://nuc.example.ts.net/ccmsg/personal/",
12
- /** Where the web UI is published, which is nobody's endpoint: a credential is
13
- * made here and used against the endpoints above. A base URL with a path of
14
- * its own, as an endpoint is, and its registrable domain is not the
15
- * endpoints' — so the two are cross-site and a refresh cookie between them is
16
- * a partitioned one. */
17
- webui: "https://ui.example.test/ccmsg/",
18
- /** A second web UI, sharing the endpoints' registrable domain: same-site, and
12
+ /** The person every auth fixture is about: sixteen bytes spelled base64url,
13
+ * which is at once their id in these records and the user handle their
14
+ * authenticator holds. Twenty-two characters, as sixteen bytes always are. */
15
+ user: "d2hvLWlzLXRoaXMtdXNlcg",
16
+ /** A second person, so that an instance with two owners can be written down.
17
+ * Nothing but the id distinguishes them here. */
18
+ other_user: "c29tZWJvZHktZWxzZS1oaQ",
19
+ /** Where the page a person opens is served from, which is nobody's endpoint:
20
+ * a credential is made here and used at whichever instance the person owns.
21
+ * Its registrable domain is not the endpoints' — so the two are cross-site
22
+ * and a refresh cookie between them is a partitioned one. */
23
+ origin: "https://ui.example.test",
24
+ /** A second origin, sharing the endpoints' registrable domain: same-site, and
19
25
  * still an origin of its own. A credential made here is a separate
20
26
  * credential, and the cookie between it and an endpoint is not partitioned —
21
27
  * one contract, two shapes, which is why both are written down. */
22
- same_site_webui: "https://ui.example.ts.net/",
28
+ same_site_origin: "https://ui.example.ts.net",
29
+ /** The address a person reaches the instances at when a load balancer is in
30
+ * front of them: an endpoint like any other, and the one an enrolment URL
31
+ * names as where to post. It is nobody's own address — which of the instances
32
+ * behind it answers is not a thing the caller chooses — so it is never
33
+ * compared with anything. */
34
+ hosting_endpoint: "https://ccmsg2.example.ts.net/",
23
35
  mid: "3f9c1a7b5e2d48069c1a7b5e2d480691/1841",
24
36
  request_id: "1",
25
37
  } as const;
@@ -61,13 +61,25 @@ export const OP_FIXTURES = {
61
61
  response: common.AUTH_REGISTER_RESPONSE,
62
62
  },
63
63
  "auth.assert": { request: common.AUTH_ASSERT_REQUEST, response: common.AUTH_ASSERT_RESPONSE },
64
+ "auth.enroll": { request: common.AUTH_ENROLL_REQUEST, response: common.AUTH_ENROLL_RESPONSE },
64
65
  "auth.token.refresh": {
65
66
  request: common.AUTH_TOKEN_REFRESH_REQUEST,
66
67
  response: common.AUTH_TOKEN_REFRESH_RESPONSE,
67
68
  },
68
69
  "auth.extend": { request: common.AUTH_EXTEND_REQUEST, response: common.AUTH_EXTEND_RESPONSE },
70
+ "auth.account.read": {
71
+ request: common.AUTH_ACCOUNT_READ_REQUEST,
72
+ response: common.AUTH_ACCOUNT_READ_RESPONSE,
73
+ },
74
+ "auth.ownership.remove": {
75
+ request: common.AUTH_OWNERSHIP_REMOVE_REQUEST,
76
+ response: common.AUTH_OWNERSHIP_REMOVE_RESPONSE,
77
+ },
78
+ "auth.credential.remove": {
79
+ request: common.AUTH_CREDENTIAL_REMOVE_REQUEST,
80
+ response: common.AUTH_CREDENTIAL_REMOVE_RESPONSE,
81
+ },
69
82
  "auth.resolve": { request: common.AUTH_RESOLVE_REQUEST, response: common.AUTH_RESOLVE_RESPONSE },
70
- "auth.rotate": { request: common.AUTH_ROTATE_REQUEST, response: common.AUTH_ROTATE_RESPONSE },
71
83
 
72
84
  "message.send": {
73
85
  request: messaging.MESSAGE_SEND_REQUEST,
@@ -21,8 +21,10 @@ const {
21
21
  other_instance,
22
22
  endpoint,
23
23
  other_endpoint,
24
- webui,
25
- same_site_webui,
24
+ origin,
25
+ same_site_origin,
26
+ user: USER,
27
+ other_user: OTHER_USER,
26
28
  mid,
27
29
  } = FIXTURE_IDS;
28
30
 
@@ -504,6 +506,15 @@ export const KV_FRAME: Static<typeof KvFrame> = {
504
506
  },
505
507
  };
506
508
 
509
+ /** The grantings the fixtures name. A granting's id is random and never reused,
510
+ * which is what lets the same instance be given up and taken again. */
511
+ const GRANT = "Z3JhbnQtb25l";
512
+ const OTHER_GRANT = "Z3JhbnQtdHdv";
513
+ const THIRD_GRANT = "Z3JhbnQtdGhyZWU";
514
+ const FOURTH_GRANT = "Z3JhbnQtZm91cg";
515
+
516
+ /** The person, the passkeys that answer for them, and the instances they own —
517
+ * the records an instance needs to admit somebody, all replicated. */
507
518
  export const AUTH_RECORDS_FRAME = {
508
519
  ev: "topic",
509
520
  topic: "auth.records",
@@ -512,16 +523,24 @@ export const AUTH_RECORDS_FRAME = {
512
523
  data: {
513
524
  records: [
514
525
  {
515
- key: "credential/personal-1/Y3JlZC1pZA",
526
+ key: `user/${USER}`,
527
+ updated_at: FIXTURE_NOW - 900_000,
528
+ body: {
529
+ kind: "user",
530
+ user: USER,
531
+ display_name: "kawaz",
532
+ created_at: FIXTURE_NOW - 900_000,
533
+ },
534
+ },
535
+ {
536
+ key: "credential/Y3JlZC1pZA",
516
537
  updated_at: FIXTURE_NOW,
517
538
  body: {
518
539
  kind: "credential",
519
- sub: "personal-1",
540
+ user: USER,
520
541
  credential_id: "Y3JlZC1pZA",
521
542
  public_key: "pQECAyYgASFYIA",
522
- user_handle: "dXNlci1oYW5kbGU",
523
- endpoint,
524
- webui,
543
+ origin,
525
544
  sign_count: 0,
526
545
  issued_label: "for kawaz",
527
546
  device_label: "work laptop",
@@ -534,30 +553,77 @@ export const AUTH_RECORDS_FRAME = {
534
553
  },
535
554
  },
536
555
  {
537
- // The same person at a second site, which shares the endpoint's
538
- // registrable domain where the first does not. One credential per site,
539
- // and the difference between the two is what decides whether the
540
- // refresh cookie for a session made here is a partitioned one.
541
- key: "credential/personal-1/Y3JlZC1pZC0y",
556
+ // The same person at a second origin, which shares the endpoints'
557
+ // registrable domain where the first does not. One credential per
558
+ // origin, and the difference between the two is what decides whether
559
+ // the refresh cookie for a session made here is a partitioned one.
560
+ key: "credential/Y3JlZC1pZC0y",
542
561
  updated_at: FIXTURE_NOW,
543
562
  body: {
544
563
  kind: "credential",
545
- sub: "personal-1",
564
+ user: USER,
546
565
  credential_id: "Y3JlZC1pZC0y",
547
566
  public_key: "pQECAyYgASFYIB",
548
- user_handle: "dXNlci1oYW5kbGU",
549
- endpoint,
550
- webui: same_site_webui,
567
+ origin: same_site_origin,
551
568
  device_label: "phone",
552
569
  registered_at: FIXTURE_NOW - 300_000,
553
570
  },
554
571
  },
572
+ {
573
+ // The instance this person made themselves, from its command line:
574
+ // there was no user yet to do it, which is why a granting's author can
575
+ // be an instance at all.
576
+ key: `ownership/${instance}/${USER}/${GRANT}`,
577
+ updated_at: FIXTURE_NOW - 600_000,
578
+ body: {
579
+ kind: "ownership",
580
+ user: USER,
581
+ instance,
582
+ grant: GRANT,
583
+ granted_at: FIXTURE_NOW - 600_000,
584
+ granted_by: { kind: "instance", instance },
585
+ },
586
+ },
587
+ {
588
+ // A second instance, taken as their own afterwards. Neither credential
589
+ // is named here: which instances a person may enter is this record's
590
+ // answer, and which page may speak is the credential's.
591
+ //
592
+ // The frame is this instance's, and what it carries is an ownership of
593
+ // its peer: a granting is written wherever the operator is standing and
594
+ // replicated from there, peers trusting one another equally. Nothing
595
+ // requires the instance that wrote a granting to be the one granted.
596
+ key: `ownership/${other_instance}/${USER}/${OTHER_GRANT}`,
597
+ updated_at: FIXTURE_NOW - 60_000,
598
+ body: {
599
+ kind: "ownership",
600
+ user: USER,
601
+ instance: other_instance,
602
+ grant: OTHER_GRANT,
603
+ granted_at: FIXTURE_NOW - 60_000,
604
+ granted_by: { kind: "user", user: USER },
605
+ },
606
+ },
607
+ {
608
+ // A second person owning that same instance. One instance may have
609
+ // several owners, and they are told apart by the key alone.
610
+ key: `ownership/${other_instance}/${OTHER_USER}/${THIRD_GRANT}`,
611
+ updated_at: FIXTURE_NOW - 30_000,
612
+ body: {
613
+ kind: "ownership",
614
+ user: OTHER_USER,
615
+ instance: other_instance,
616
+ grant: THIRD_GRANT,
617
+ granted_at: FIXTURE_NOW - 30_000,
618
+ granted_by: { kind: "user", user: USER },
619
+ },
620
+ },
555
621
  ],
556
622
  },
557
623
  } satisfies Static<typeof AuthRecordsFrame>;
558
624
 
559
- /** The token half of the same topic: one family, written by the instance that
560
- * issued it. */
625
+ /** The token half of the same topic: one family, minted by one instance and
626
+ * writable at every instance its owner owns. */
561
627
  export const AUTH_RECORDS_FAMILY_FRAME = {
562
628
  ev: "topic",
563
629
  topic: "auth.records",
@@ -569,9 +635,9 @@ export const AUTH_RECORDS_FAMILY_FRAME = {
569
635
  updated_at: FIXTURE_NOW + 100_000,
570
636
  body: {
571
637
  kind: "token_family",
572
- sub: "personal-1",
638
+ user: USER,
573
639
  iss: instance,
574
- webui,
640
+ origin,
575
641
  access: { value: "YWNjZXNz", expires_at: FIXTURE_NOW + 10_000_000 },
576
642
  refresh: { value: "cmVmcmVzaA", expires_at: FIXTURE_NOW + 600_000_000 },
577
643
  last_refresh: {
@@ -588,8 +654,10 @@ export const AUTH_RECORDS_FAMILY_FRAME = {
588
654
  },
589
655
  } satisfies Static<typeof AuthRecordsFrame>;
590
656
 
591
- /** A removal travels as a record of its own. A credential's never expires; a
592
- * family's is kept only as long as a refresh token could still arrive. */
657
+ /** A removal travels as a record of its own, and says nothing but when: what
658
+ * was removed is the key it arrives under. A credential's and an ownership's
659
+ * never expire; a family's is kept only as long as a refresh token could still
660
+ * arrive. */
593
661
  export const AUTH_RECORDS_TOMBSTONE_FRAME = {
594
662
  ev: "topic",
595
663
  topic: "auth.records",
@@ -597,16 +665,37 @@ export const AUTH_RECORDS_TOMBSTONE_FRAME = {
597
665
  data: {
598
666
  records: [
599
667
  {
600
- key: "credential/personal-1/Y3JlZC1pZA",
668
+ key: "credential/Y3JlZC1pZA",
669
+ updated_at: FIXTURE_NOW + 100_000,
670
+ body: { kind: "tombstone", deleted_at: FIXTURE_NOW + 100_000 },
671
+ },
672
+ {
673
+ // The second person gives that instance up.
674
+ key: `ownership/${other_instance}/${OTHER_USER}/${THIRD_GRANT}`,
601
675
  updated_at: FIXTURE_NOW + 100_000,
602
- body: { kind: "tombstone", sub: "personal-1", deleted_at: FIXTURE_NOW + 100_000 },
676
+ body: { kind: "tombstone", deleted_at: FIXTURE_NOW + 100_000 },
677
+ },
678
+ {
679
+ // And is made an owner of it again. A granting has an id of its own, so
680
+ // this is a key no tombstone stands on — where a key made of the
681
+ // instance and the person alone would have made the removal above
682
+ // final, with nothing able to undo it.
683
+ key: `ownership/${other_instance}/${OTHER_USER}/${FOURTH_GRANT}`,
684
+ updated_at: FIXTURE_NOW + 200_000,
685
+ body: {
686
+ kind: "ownership",
687
+ user: OTHER_USER,
688
+ instance: other_instance,
689
+ grant: FOURTH_GRANT,
690
+ granted_at: FIXTURE_NOW + 200_000,
691
+ granted_by: { kind: "user", user: USER },
692
+ },
603
693
  },
604
694
  {
605
695
  key: "family/01J9Z3W2Q",
606
696
  updated_at: FIXTURE_NOW + 100_000,
607
697
  body: {
608
698
  kind: "tombstone",
609
- sub: "personal-1",
610
699
  deleted_at: FIXTURE_NOW + 100_000,
611
700
  expires_at: FIXTURE_NOW + 604_900_000,
612
701
  },