@ccmsg/protocol 2.3.0 → 2.5.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.
@@ -1,34 +1,6 @@
1
1
  import { type Static, Type } from "@sinclair/typebox";
2
2
  import { request, response, topicFrame } from "../envelope.ts";
3
- import { Endpoint, InstanceId, type Origin, Timestamp, WebUi } from "../identifiers.ts";
4
-
5
- /** The origin of a web UI's URL: what a browser puts in an `Origin` header and
6
- * in a credential's `clientDataJSON`, which is the URL's scheme and authority
7
- * and no more.
8
- *
9
- * Derived rather than stored, and derived here rather than once per
10
- * implementation, because every use of it is an exact comparison against a
11
- * value a browser serialized. The normalization that makes those comparisons
12
- * hold — a lowercase scheme and host, a port only where it is not the scheme's
13
- * own, an address literal in its brackets — is the URL parser's, and this is
14
- * the one place the contract says so. */
15
- export function originOf(webui: string): Origin {
16
- return new URL(webui).origin;
17
- }
18
-
19
- /** The WebAuthn relying party a credential made at a web UI is created under:
20
- * the host of its URL, port and scheme left off, as a relying party is a domain
21
- * and not an origin.
22
- *
23
- * Derived for the same reason as the origin, and held to the host exactly. A
24
- * client will accept a relying party that is the page's effective domain or a
25
- * registrable suffix of it, so anything shorter than the host would be one
26
- * credential several sites could answer with — which is the single thing
27
- * binding a credential to one web UI rules out. An assertion's `rpIdHash` is
28
- * the SHA-256 of what this returns. */
29
- export function rpIdOf(webui: string): string {
30
- return new URL(webui).hostname;
31
- }
3
+ import { Endpoint, InstanceId, Origin, Timestamp } from "../identifiers.ts";
32
4
 
33
5
  /** A value that is nothing but bytes to everyone who handles it: a token, a
34
6
  * challenge, a credential id, a signature. Spelled base64url without padding so
@@ -41,6 +13,33 @@ export const Base64Url = Type.String({
41
13
  });
42
14
  export type Base64Url = Static<typeof Base64Url>;
43
15
 
16
+ /** Who a person is: sixteen random bytes the instance that registered them
17
+ * settled on once, and never anything else after.
18
+ *
19
+ * This is the WebAuthn user handle itself rather than a name derived beside it.
20
+ * The same value keys the person's records, is stored in the authenticator, and
21
+ * comes back as an assertion's `user_handle` — one spelling, because every use
22
+ * of it is a string comparison and a second copy of one fact is only a thing
23
+ * that can disagree. Two values for one person would be two accounts in their
24
+ * authenticator, which no instance could reach in to merge.
25
+ *
26
+ * It names a person and nothing about where they connected. An instance, an
27
+ * endpoint and a mesh are all things a person may have or reach, and none of
28
+ * them is who they are.
29
+ *
30
+ * Sixteen bytes is stated by the pattern rather than left to the issuer, this
31
+ * being the one canonical form of an identity: twenty-two base64url characters,
32
+ * whose last one carries the four bits that have nowhere to go and so is one of
33
+ * `A`, `Q`, `g`, `w`. Anything else is either a different length or a second
34
+ * spelling of the same bytes, and a record keyed by one of those would be a
35
+ * person no assertion could ever find — the authenticator answers with the
36
+ * bytes it was given, and every comparison here is of the string. */
37
+ export const UserId = Type.String({
38
+ $id: "UserId",
39
+ pattern: "^[A-Za-z0-9_-]{21}[AQgw]$",
40
+ });
41
+ export type UserId = Static<typeof UserId>;
42
+
44
43
  /** How long a challenge is good for. Short because a challenge is consumed
45
44
  * within one interaction at a keyboard; the window is only what covers the
46
45
  * person reaching for their authenticator. */
@@ -55,14 +54,13 @@ export const REGISTER_TTL_MS = 10 * 60 * 1000;
55
54
  *
56
55
  * A credential's tombstone has no counterpart here on purpose: it is kept
57
56
  * without end, because a peer returning from a partition longer than any
58
- * retention would otherwise carry the removed credential back as news. */
57
+ * retention would otherwise carry the removed credential back as news. The same
58
+ * holds of an ownership's — a granting brought back would be an instance
59
+ * someone was let into again. Neither is a door closed for good: a credential id
60
+ * and a granting's id are both new every time, so registering again and being
61
+ * made an owner again write keys no tombstone stands on. */
59
62
  export const FAMILY_TOMBSTONE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
60
63
 
61
- /** Who a person is to this mesh. Issued when the registration URL is made
62
- * (`<unit>-<counter>` by default) and carried by every record they own. */
63
- export const Subject = Type.String({ $id: "Subject", minLength: 1, maxLength: 128 });
64
- export type Subject = Static<typeof Subject>;
65
-
66
64
  // --- challenge -------------------------------------------------------------
67
65
 
68
66
  export const AuthChallengeArgs = Type.Object({});
@@ -94,60 +92,141 @@ export type AuthChallengeResult = Static<typeof AuthChallengeResult>;
94
92
  export const AuthChallengeRequest = request("auth.challenge", AuthChallengeArgs);
95
93
  export const AuthChallengeResponse = response("auth.challenge", AuthChallengeResult);
96
94
 
97
- // --- registration ----------------------------------------------------------
95
+ // --- enrolment URLs --------------------------------------------------------
98
96
 
99
- /** What the registration URL carries, as the instance that issued it reads it
100
- * back. On the wire between a browser and an instance the whole of it is one
101
- * opaque string; this shape is what `auth.resolve` answers with, so the two
102
- * instances involved agree on what was authorized.
97
+ /** What the URL a person is sent authorizes: making the user, or adding an
98
+ * instance to one that exists.
103
99
  *
104
- * Its integrity rests on a secret made for this one registration and held only
105
- * in the issuing instance's memory. Nothing outlives the window: a restart
106
- * loses the secret, and the remedy is to issue another URL rather than to keep
107
- * a key that could sign anything later. */
108
- export const RegisterClaims = Type.Object(
109
- {
110
- /** The instance that issued the URL and holds the secret. */
111
- iss: InstanceId,
112
- sub: Subject,
113
- /** The instance's name as a person operates it, for display. */
114
- unit: Type.String({ minLength: 1 }),
115
- /** The endpoint the credential is being registered for. The base URL: the
116
- * registration is posted to `<endpoint>auth/register`, and the cookie set
117
- * for it hangs under the same prefix. */
118
- endpoint: Endpoint,
119
- /** Where the URL sends the person: the web UI they will open it at, and so
120
- * the page the credential will be made by. It is not read off the endpoint,
121
- * the UI being publishable anywhere, and a registration URL that did not
122
- * name it would not be a URL anyone could open.
123
- *
124
- * Its origin (`originOf`) is what the ceremony is then held to, and it is
125
- * also what lets a first registration be answered across sites at all: an
126
- * instance answers CORS for the origins its credentials name, and the first
127
- * registration at a new UI has no credential yet the URL it issued and
128
- * still holds stands in for one until it does. That is the issuer's own
129
- * knowledge and travels nowhere, which is why a registration is only
130
- * completed where it was issued. */
131
- webui: WebUi,
132
- expires_at: Timestamp,
133
- /** Names this registration, so it can be spent once. */
134
- jti: Type.String({ minLength: 1 }),
135
- /** The WebAuthn user handle for this subject: sixteen random bytes the
136
- * issuing instance settles on once per `sub`. The page creates the
137
- * credential against it, the record keeps it, and an assertion that names a
138
- * handle is held to it. It is here rather than left to the page because the
139
- * authenticator stores it beyond this instance's reach a second value for
140
- * one person would be a second account on their device. */
141
- user_id: Base64Url,
142
- /** What the administrator who issued the URL wrote down about who it was
143
- * for. Their words, not the holder's the label the person gives their
144
- * own device is `device_label` on the record, and the two are worth telling
145
- * apart when a list is read back later. */
146
- issued_label: Type.Optional(Type.String({ maxLength: 128 })),
147
- },
148
- { $id: "RegisterClaims" },
100
+ * The two are told apart here rather than by which fields happen to be set,
101
+ * because they are answered by different ceremonies a creation makes a
102
+ * credential, an addition asserts with one that exists and an op whose effect
103
+ * is read off the shape of its arguments is authorization written outside the
104
+ * table that decides it. */
105
+ export const EnrollPurpose = Type.Union([Type.Literal("create_user"), Type.Literal("add_owner")], {
106
+ $id: "EnrollPurpose",
107
+ });
108
+ export type EnrollPurpose = Static<typeof EnrollPurpose>;
109
+
110
+ /** What every enrolment URL carries, whichever of the two it is. */
111
+ const ENROLL_CLAIMS_FIELDS = {
112
+ /** The instance that issued the URL and holds the secret. */
113
+ iss: InstanceId,
114
+ /** The instance the person will own once this is spent. The issuer's own:
115
+ * an instance hands out the right to enter itself, and nothing here lets
116
+ * one instance open a door into another. */
117
+ instance: InstanceId,
118
+ /** Where the person is being sent, and so the only place the ceremony may
119
+ * be held: the `clientDataJSON.origin` is compared with this, the `Origin`
120
+ * header with this, and the relying party is this origin's host.
121
+ *
122
+ * An origin rather than a URL because that is the size of everything
123
+ * compared against it, and because there is nothing else to say: **the person
124
+ * is sent to the root of this origin**. Nothing under it is named, since
125
+ * nothing under it can be told apart a browser writes no path into an
126
+ * `Origin` header or a `clientDataJSON`, so two paths here would be one place
127
+ * to every check made. */
128
+ origin: Origin,
129
+ /** Where the page posts what it made: the base URL the `auth` routes hang
130
+ * under.
131
+ *
132
+ * **A destination and not a binding.** The page has to send its answer
133
+ * somewhere, and a URL a person carries from a terminal has no other way to
134
+ * say where. Nothing on the receiving side compares this with anything
135
+ * not with its own endpoint, not with the issuer's. It may be the address
136
+ * of a load balancer with several instances behind it, and whichever of
137
+ * them the answer lands on completes the enrolment: it checks the ceremony
138
+ * itself and asks the issuer only for what the issuer alone holds. Having
139
+ * nothing to compare here is the point rather than an omission. */
140
+ endpoint: Endpoint,
141
+ expires_at: Timestamp,
142
+ /** Names this enrolment, so it can be spent once. */
143
+ jti: Type.String({ minLength: 1 }),
144
+ /** What the administrator who issued the URL wrote down about who it was
145
+ * for. Their words, not the holder's — the label the person gives their
146
+ * own device is `device_label` on the record, and the two are worth telling
147
+ * apart when a list is read back later. */
148
+ issued_label: Type.Optional(Type.String({ maxLength: 128 })),
149
+ /** What to call the account in the authenticator, as the administrator who
150
+ * made the URL wrote it.
151
+ *
152
+ * It has to travel, because the page has to name the account before anything
153
+ * is created and has no other way to learn it: a passkey manager keeps the
154
+ * name the ceremony was given and shows it wherever the key is listed, so a
155
+ * URL that carried none would put the user handle — sixteen random bytes — in
156
+ * front of the person at every sign-in.
157
+ *
158
+ * Apart from `issued_label`, which is a note about who the URL was handed to
159
+ * and stays on the credential as exactly that. One value answering both would
160
+ * be an administrator's private memo shown to the person as their own name,
161
+ * with no way to correct either without the other.
162
+ *
163
+ * A starting point and not the answer: the registration form shows it and the
164
+ * person may say otherwise, and `auth.register.display_name` is what they
165
+ * settled. For a URL adding a passkey to somebody who exists, it is the name
166
+ * they already read themselves by, so a second key joins the same account. */
167
+ display_name: Type.Optional(Type.String({ maxLength: 128 })),
168
+ /** Every instance this enrolment hands the person, written by whichever one
169
+ * the ceremony lands on once it succeeds.
170
+ *
171
+ * The set is decided at the terminal that made the URL, because that is where
172
+ * "the instances this one knows of" is a question anybody can see the answer
173
+ * to. It has to travel, for the same reason the rest of the claims do: behind
174
+ * a load balancer the ceremony lands wherever it lands, and an instance that
175
+ * wrote the grantings it happened to know of would answer a different
176
+ * question than the one that was asked.
177
+ *
178
+ * Written when the enrolment succeeds and not before. A granting for somebody
179
+ * who never registered would sit in the replicated set naming a person no
180
+ * user record answers for — inert, since nothing could authenticate as them,
181
+ * but there to be read and impossible to tell from one that means something.
182
+ *
183
+ * Absent is the issuer's own instance alone, which is what `instance` already
184
+ * says. Naming it here as well is how a URL hands over more than one. */
185
+ instances: Type.Optional(Type.Array(InstanceId)),
186
+ } as const;
187
+
188
+ /** What an enrolment URL carries, as the instance that issued it reads it back.
189
+ * On the wire between a browser and an instance the whole of it is one opaque
190
+ * string; this shape is what `auth.resolve` answers with, so the two instances
191
+ * involved agree on what was authorized.
192
+ *
193
+ * Its integrity rests on a secret made for this one enrolment and held only in
194
+ * the issuing instance's memory. Nothing outlives the window: a restart loses
195
+ * the secret, and the remedy is to issue another URL rather than to keep a key
196
+ * that could sign anything later.
197
+ *
198
+ * A union on `purpose` rather than one shape with an optional `user`, because
199
+ * the two purposes do not carry the same claims and a schema that accepted
200
+ * either field with either purpose would leave the issuing and the receiving
201
+ * instance free to read one value two ways. The correlation is the claim. */
202
+ export const EnrollClaims = Type.Union(
203
+ [
204
+ Type.Object({
205
+ ...ENROLL_CLAIMS_FIELDS,
206
+ purpose: Type.Literal("create_user"),
207
+ /** The user handle the credential will be created against.
208
+ *
209
+ * The issuer settles it rather than the page because the authenticator
210
+ * keeps it beyond any instance's reach — a second value for one person
211
+ * would be a second account on their device that nothing here could
212
+ * undo. */
213
+ user: UserId,
214
+ }),
215
+ Type.Object({
216
+ ...ENROLL_CLAIMS_FIELDS,
217
+ purpose: Type.Literal("add_owner"),
218
+ /** Named here only to be refused. Who arrives is what the assertion
219
+ * says, so a handle stated up front would be a name the ceremony was
220
+ * never held to — and a claim that is merely unread is one an issuer and
221
+ * a receiver can still disagree about. */
222
+ user: Type.Optional(Type.Never()),
223
+ }),
224
+ ],
225
+ { $id: "EnrollClaims" },
149
226
  );
150
- export type RegisterClaims = Static<typeof RegisterClaims>;
227
+ export type EnrollClaims = Static<typeof EnrollClaims>;
228
+
229
+ // --- making a user ---------------------------------------------------------
151
230
 
152
231
  /** What `navigator.credentials.create()` produced, in this contract's spelling.
153
232
  * The browser's own field names are camelCase; they are written snake_case here
@@ -167,8 +246,8 @@ export const RegistrationCredential = Type.Object(
167
246
  export type RegistrationCredential = Static<typeof RegistrationCredential>;
168
247
 
169
248
  export const AuthRegisterArgs = Type.Object({
170
- /** The registration URL's token, opaque to the caller and to any instance
171
- * but its issuer. */
249
+ /** The enrolment URL's token, opaque to the caller and to any instance but
250
+ * its issuer. */
172
251
  token: Type.String({ minLength: 1 }),
173
252
  /** The six digits the command line showed when the URL was made, typed in by
174
253
  * the person registering. It is not in the URL and never travels with it, so
@@ -178,11 +257,24 @@ export const AuthRegisterArgs = Type.Object({
178
257
  /** What the person calls the device they are registering, for their own use
179
258
  * when they later read back a list of several. Nothing is decided by it. */
180
259
  device_label: Type.Optional(Type.String({ maxLength: 128 })),
260
+ /** What the person settled on being called, from the form the registration
261
+ * page showed them with the URL's `display_name` already in it.
262
+ *
263
+ * Here rather than left to the claims because the person is the one it is
264
+ * about: an administrator issuing the URL guesses at a name, and the person
265
+ * in front of the browser is who can say. It is what the authenticator was
266
+ * given as the account name, so what they see in their passkey manager and
267
+ * what they read themselves by here are the same string.
268
+ *
269
+ * Unstated leaves the claims' value standing. It authenticates nothing, like
270
+ * every other name here. Adding a passkey to somebody who already exists does
271
+ * not rename them — the account it joins is one they have already named. */
272
+ display_name: Type.Optional(Type.String({ maxLength: 128 })),
181
273
  /** The challenge this registration answers, with the instance that can spend
182
274
  * it — the same pairing an assertion carries, and for the same reason: the
183
275
  * value also sits inside `client_data_json`, but who may consume it does not,
184
276
  * and behind a load balancer the instance that issued it, the one that made
185
- * the registration URL and the one receiving this may all be different.
277
+ * the enrolment URL and the one receiving this may all be different.
186
278
  *
187
279
  * Omitting it leaves the receiver with a value and no issuer, so it can only
188
280
  * be honoured where the receiver itself holds the challenge; anywhere else
@@ -202,13 +294,14 @@ export type AuthRegisterArgs = Static<typeof AuthRegisterArgs>;
202
294
  * does not own only as a partitioned one, which keeps a session taken at one
203
295
  * site from being carried to another. That partition is by site, where a
204
296
  * credential is by origin, so it is the `Origin` held against this family's
205
- * `webui` that keeps a session to the one place it was made — the cookie's
206
- * partition answers for sites and nothing finer. Sending it at all across sites takes a browser that partitions
207
- * cookies, which is a premise of this contract rather than a case it
208
- * accommodates: one that does not is not an environment this is spoken over. */
297
+ * `origin` that keeps a session to the one place it was made — the cookie's
298
+ * partition answers for sites and nothing finer. Sending it at all across sites
299
+ * takes a browser that partitions cookies, which is a premise of this contract
300
+ * rather than a case it accommodates: one that does not is not an environment
301
+ * this is spoken over. */
209
302
  export const AuthSession = Type.Object(
210
303
  {
211
- sub: Subject,
304
+ user: UserId,
212
305
  /** The access token and when it stops being accepted. It is presented on
213
306
  * the WebSocket handshake, and a connection lives until this instant unless
214
307
  * it is renewed on the connection itself. */
@@ -227,15 +320,15 @@ export const AuthRegisterResponse = response("auth.register", AuthRegisterResult
227
320
  // --- assertion -------------------------------------------------------------
228
321
 
229
322
  /** What `navigator.credentials.get()` produced. `user_handle` is what a
230
- * resident credential answers with when the person named no account, so a
231
- * signed-in subject can be found without the browser having been told one. */
323
+ * resident credential answers with when the person named no account, so the
324
+ * person can be found without the browser having been told who they are. */
232
325
  export const AssertionCredential = Type.Object(
233
326
  {
234
327
  raw_id: Base64Url,
235
328
  client_data_json: Base64Url,
236
329
  authenticator_data: Base64Url,
237
330
  signature: Base64Url,
238
- user_handle: Type.Optional(Base64Url),
331
+ user_handle: Type.Optional(UserId),
239
332
  },
240
333
  { $id: "AssertionCredential" },
241
334
  );
@@ -256,6 +349,40 @@ export type AuthAssertResult = Static<typeof AuthAssertResult>;
256
349
  export const AuthAssertRequest = request("auth.assert", AuthAssertArgs);
257
350
  export const AuthAssertResponse = response("auth.assert", AuthAssertResult);
258
351
 
352
+ // --- adding an instance to a user ------------------------------------------
353
+
354
+ /** Adds one instance to the user the assertion names.
355
+ *
356
+ * Apart from `auth.assert` because it answers a different question. An
357
+ * assertion says who is here; this says that the person in front of the
358
+ * authenticator decided, now, to take an instance as theirs — and only the six
359
+ * digits shown at that instance's terminal can say the second thing. Folding it
360
+ * into the assertion as optional arguments would be an op whose effect changes
361
+ * with which fields are present, which is authorization decided outside the
362
+ * table.
363
+ *
364
+ * No credential is created: the person already has one, and an instance is not
365
+ * something a passkey is made for. */
366
+ export const AuthEnrollArgs = Type.Object({
367
+ /** The enrolment URL's token, as `auth.register` carries one. */
368
+ token: Type.String({ minLength: 1 }),
369
+ /** The six digits, required here exactly as they are for a registration. What
370
+ * the assertion proves is that this is the person; what the digits prove is
371
+ * that they are the one asking for this instance. Without them a synced
372
+ * passkey left unattended is enough for someone else to hand themselves an
373
+ * instance in the person's name. */
374
+ code: Type.String({ pattern: "^[0-9]{6}$" }),
375
+ challenge: AuthChallenge,
376
+ credential: AssertionCredential,
377
+ });
378
+ export type AuthEnrollArgs = Static<typeof AuthEnrollArgs>;
379
+
380
+ export const AuthEnrollResult = AuthSession;
381
+ export type AuthEnrollResult = Static<typeof AuthEnrollResult>;
382
+
383
+ export const AuthEnrollRequest = request("auth.enroll", AuthEnrollArgs);
384
+ export const AuthEnrollResponse = response("auth.enroll", AuthEnrollResult);
385
+
259
386
  // --- refreshing a token pair ----------------------------------------------
260
387
 
261
388
  /** Why a client asked for a fresh pair. Stated by the caller and never checked,
@@ -269,7 +396,15 @@ export type AuthRefreshReason = Static<typeof AuthRefreshReason>;
269
396
 
270
397
  /** The refresh token is not among the arguments: it is a cookie the carrier
271
398
  * already holds, and a caller that could state it is a caller that could read
272
- * it. What is left is why the caller is asking, which nothing is decided by. */
399
+ * it. What is left is why the caller is asking, which nothing is decided by.
400
+ *
401
+ * Answered wherever it lands. The family is replicated and every instance its
402
+ * owner owns may write it, so a rotation is not carried anywhere. Two instances
403
+ * rotating one family at once is a collision the losing value does not survive:
404
+ * it is a value the family retired, which is indistinguishable from a replay,
405
+ * and the contract has nothing that would tell the two apart. The person signs
406
+ * in again, which costs one verification and keeps replay detection as sharp as
407
+ * it was. */
273
408
  export const AuthTokenRefreshArgs = Type.Object({
274
409
  /** What prompted this refresh, as the client knows it: the page was loaded
275
410
  * again, the access token was about to expire, or a dropped connection is
@@ -310,14 +445,19 @@ export const AuthExtendResponse = response("auth.extend", AuthExtendResult);
310
445
 
311
446
  /** Asks the instance that issued something to check it and spend it.
312
447
  *
313
- * Two things are only knowable at their issuer: a registration URL, whose
314
- * secret never left it, and a challenge, which is good once and so has to be
315
- * spent somewhere single. Everything else about the exchange — the WebAuthn
316
- * verification, the record lookup the receiving instance does itself. */
448
+ * Two things are only knowable at their issuer: an enrolment URL, whose secret
449
+ * never left it, and a challenge, which is good once and so has to be spent
450
+ * somewhere single. Everything else about the exchange — the WebAuthn
451
+ * verification, the record lookup, writing what was authorized the receiving
452
+ * instance does itself.
453
+ *
454
+ * One kind covers both enrolments rather than one each. What is checked is the
455
+ * same in both — the token, the digits, the count of attempts against them —
456
+ * and what tells them apart is in the claims that come back. */
317
457
  export const AuthResolveArgs = Type.Union(
318
458
  [
319
459
  Type.Object({
320
- kind: Type.Literal("register"),
460
+ kind: Type.Literal("claims"),
321
461
  token: Type.String({ minLength: 1 }),
322
462
  /** The digits the person typed, forwarded unchecked. The issuer holds
323
463
  * both the code and the count of attempts against it, so it is the only
@@ -332,11 +472,11 @@ export const AuthResolveArgs = Type.Union(
332
472
  );
333
473
  export type AuthResolveArgs = Static<typeof AuthResolveArgs>;
334
474
 
335
- /** What was authorized, for a registration; nothing beyond the acknowledgement
475
+ /** What was authorized, for an enrolment; nothing beyond the acknowledgement
336
476
  * for a challenge, whose whole answer is that it was unspent and now is not. */
337
477
  export const AuthResolveResult = Type.Union(
338
478
  [
339
- Type.Object({ kind: Type.Literal("register"), claims: RegisterClaims }),
479
+ Type.Object({ kind: Type.Literal("claims"), claims: EnrollClaims }),
340
480
  Type.Object({ kind: Type.Literal("challenge") }),
341
481
  ],
342
482
  { $id: "AuthResolveResult" },
@@ -346,147 +486,186 @@ export type AuthResolveResult = Static<typeof AuthResolveResult>;
346
486
  export const AuthResolveRequest = request("auth.resolve", AuthResolveArgs);
347
487
  export const AuthResolveResponse = response("auth.resolve", AuthResolveResult);
348
488
 
349
- /** Rotates a token family at the one instance allowed to write it.
489
+ // --- the replicated records ------------------------------------------------
490
+
491
+ /** A person, as every instance in the mesh holds them.
350
492
  *
351
- * A family is written by its `iss` alone. Two instances rotating one family in
352
- * parallel would merge by last write and lose a generation, which reads exactly
353
- * like a stolen token being replayed so the rotation is forwarded rather than
354
- * done where the request landed. */
355
- export const AuthRotateArgs = Type.Object({
356
- refresh_token: Base64Url,
357
- /** What the receiving instance observed of the caller, carried to the issuer
358
- * for `last_refresh`. The person is at the other end of the receiver's
359
- * connection, not the issuer's, so these are only knowable there; forwarded
360
- * without them, a rotation would be remembered as a time and nothing else.
493
+ * The root of everything else here: credentials answer for this user, families
494
+ * belong to it, and ownerships say which instances it may enter. It is keyed by
495
+ * a value the authenticator also holds, so the person on a device and the
496
+ * person in these records are the same one by construction. */
497
+ export const UserRecord = Type.Object(
498
+ {
499
+ kind: Type.Literal("user"),
500
+ user: UserId,
501
+ /** What the person calls themselves, for their own sake when they read
502
+ * their account back or when several people share an instance.
503
+ *
504
+ * A hint like the labels on a credential: nothing is admitted, refused or
505
+ * matched by it, and the contract says nothing about what it may contain.
506
+ * The identity is the id beside it, which no display name ever stands in
507
+ * for. */
508
+ display_name: Type.Optional(Type.String({ maxLength: 128 })),
509
+ created_at: Timestamp,
510
+ },
511
+ { $id: "UserRecord" },
512
+ );
513
+ export type UserRecord = Static<typeof UserRecord>;
514
+
515
+ /** The fields of a credential that are a person's to read: everything but the
516
+ * public key, which is how an assertion is checked and nothing a list needs. */
517
+ const CREDENTIAL_PUBLIC_FIELDS = {
518
+ kind: Type.Literal("credential"),
519
+ user: UserId,
520
+ /** The credential's id as the authenticator names it, which is also what an
521
+ * assertion is looked up by. */
522
+ credential_id: Base64Url,
523
+ /** The one place a ceremony with this credential may be held.
361
524
  *
362
- * Stated by the receiver and never checked by the issuer the same standing
363
- * as the values on a rotation that was not forwarded, which the client and
364
- * its connection are equally the only source of. Nothing may be decided by
365
- * them. */
366
- reason: Type.Optional(AuthRefreshReason),
367
- ip: Type.Optional(Type.String({ minLength: 1, maxLength: 45 })),
368
- user_agent: Type.Optional(Type.String({ maxLength: 512 })),
369
- });
370
- export type AuthRotateArgs = Static<typeof AuthRotateArgs>;
371
-
372
- /** Both halves, unlike the person-facing ops: the instance that asked for the
373
- * rotation is the one that has to put the new refresh token in a cookie. */
374
- export const AuthRotateResult = Type.Object({
375
- sub: Subject,
376
- access: Type.Object({ value: Base64Url, expires_at: Timestamp }),
377
- refresh: Type.Object({ value: Base64Url, expires_at: Timestamp }),
525
+ * Registration and assertion alike are held to it: the `clientDataJSON.origin`
526
+ * has to equal this, the `Origin` header has to equal this, and the relying
527
+ * party is this origin's host so that the authenticator's own binding says
528
+ * the same thing rather than something wider. That last part is this
529
+ * contract's rule and not WebAuthn's — a passkey is bound to a relying party,
530
+ * which may be a suffix of the host, so the authenticator alone would answer
531
+ * for every origin under that suffix.
532
+ *
533
+ * It says nothing about which instance the holder may enter. That is the
534
+ * ownership record's answer, and keeping the two apart is what lets one
535
+ * credential work against every instance a person owns and against a load
536
+ * balancer in front of them. A person using web UIs at two origins holds two
537
+ * credentials; using three instances behind one origin holds one. */
538
+ origin: Origin,
539
+ /** The authenticator's counter, when it keeps one. Synced passkeys report
540
+ * zero forever, so only a pair of non-zero readings says anything, and a
541
+ * reading below the last one is a refusal. */
542
+ sign_count: Type.Optional(Type.Integer({ minimum: 0 })),
543
+ /** The BE flag of the authenticator data at registration: whether this
544
+ * credential is one the authenticator may back up, which in practice is
545
+ * what separates a passkey synced across a person's devices from one that
546
+ * lives on the single device it was made on.
547
+ *
548
+ * A hint and nothing else, like the address and the user agent beside it:
549
+ * nothing is admitted or refused by it. It is here so the person reading
550
+ * their own list can tell "this is my iCloud passkey, it is on every device
551
+ * I own" from "this is the key on the stick in my drawer" — which decides
552
+ * what removing the line actually costs them. */
553
+ backup_eligible: Type.Optional(Type.Boolean()),
554
+ /** The BS flag of the same authenticator data: whether the credential was
555
+ * backed up at that moment. Read beside `backup_eligible` — eligible and
556
+ * not yet backed up is an ordinary state on a device that has just made the
557
+ * key, and it too decides nothing. */
558
+ backup_state: Type.Optional(Type.Boolean()),
559
+ /** The label the administrator put on the enrolment URL, carried over from
560
+ * the claims it was spent against. */
561
+ issued_label: Type.Optional(Type.String({ maxLength: 128 })),
562
+ /** The label the person put on this device as they registered it. */
563
+ device_label: Type.Optional(Type.String({ maxLength: 128 })),
564
+ registered_at: Timestamp,
565
+ /** Where the registration came from and what browser sent it.
566
+ *
567
+ * None of this authenticates anything, and nothing is ever admitted or
568
+ * refused by it — an address is trivially chosen by whoever is making the
569
+ * request. They are here to be recognised by the one person reading their
570
+ * own list: an address that is their home provider's and a browser that is
571
+ * the one they use is how they place a line as theirs, or fail to, which is
572
+ * the whole reason to keep it. The same holds of the pair below. */
573
+ registered_ip: Type.Optional(Type.String({ minLength: 1, maxLength: 45 })),
574
+ registered_user_agent: Type.Optional(Type.String({ maxLength: 512 })),
575
+ /** When this credential last answered a challenge, and from where. A
576
+ * credential the person no longer recognises is one they remove. */
577
+ last_used_at: Type.Optional(Timestamp),
578
+ last_used_ip: Type.Optional(Type.String({ minLength: 1, maxLength: 45 })),
579
+ last_used_user_agent: Type.Optional(Type.String({ maxLength: 512 })),
580
+ } as const;
581
+
582
+ /** A credential as a person reads it back: what an instance holds, less the
583
+ * public key. */
584
+ export const CredentialRecordPublic = Type.Object(CREDENTIAL_PUBLIC_FIELDS, {
585
+ $id: "CredentialRecordPublic",
378
586
  });
379
- export type AuthRotateResult = Static<typeof AuthRotateResult>;
380
-
381
- export const AuthRotateRequest = request("auth.rotate", AuthRotateArgs);
382
- export const AuthRotateResponse = response("auth.rotate", AuthRotateResult);
383
-
384
- // --- the replicated records ------------------------------------------------
587
+ export type CredentialRecordPublic = Static<typeof CredentialRecordPublic>;
385
588
 
386
589
  /** A registered passkey, as every instance in the mesh holds it.
387
590
  *
388
591
  * Complete once it is written: the instance that registered it is not asked
389
- * about it again, which is what lets a person authenticate anywhere in the
390
- * mesh while the instance they registered at is down. */
592
+ * about it again, which is what lets a person authenticate at any instance they
593
+ * own while the one they registered at is down. */
391
594
  export const CredentialRecord = Type.Object(
392
595
  {
393
- kind: Type.Literal("credential"),
394
- sub: Subject,
395
- /** The credential's id as the authenticator names it, which is also what an
396
- * assertion is looked up by. */
397
- credential_id: Base64Url,
596
+ ...CREDENTIAL_PUBLIC_FIELDS,
398
597
  /** The public key, COSE-encoded. */
399
598
  public_key: Base64Url,
400
- /** The `user_id` of the registration's claims, which is what the credential
401
- * was created against and what an assertion naming a handle is checked
402
- * against. */
403
- user_handle: Base64Url,
404
- /** The endpoint this credential was registered for, as the registration's
405
- * claims stated it.
406
- *
407
- * Which instance the credential admits its holder to: an assertion is
408
- * accepted only where the request arrived at this base URL — the same
409
- * scheme and authority, and a path below it. (The authority the request
410
- * reached, which is a property of the connection; where the page asking was
411
- * served from is `webui` below and a separate question.)
412
- * `https://h.example/` and `https://h.example/personal/` are
413
- * two endpoints and take two registrations, even on one host and one
414
- * relying party — the RP ID says which domain an authenticator will answer
415
- * for, which is a coarser thing than which instance a person has been
416
- * admitted to. Binding to the base URL rather than the origin is what keeps
417
- * one instance's credential from being a way into its neighbour. */
418
- endpoint: Endpoint,
419
- /** The web UI the page that created this credential was served from, whose
420
- * origin is the one it may ever be used from.
421
- *
422
- * Holding it to one origin is this contract's rule rather than WebAuthn's. A
423
- * passkey is bound to its relying party, which may be a suffix of the host,
424
- * so the authenticator alone would answer for every origin under that
425
- * suffix. What holds a credential to one is the check made against
426
- * this: the `clientDataJSON.origin` of every ceremony, registration and
427
- * assertion alike, has to equal `originOf` this URL. The relying party is
428
- * `rpIdOf` the same URL, which is what makes the authenticator's own
429
- * binding say the same thing rather than something wider.
430
- *
431
- * The URL is what is kept, and the origin read off it where a header is
432
- * matched — a token minted here carries the same URL and its connection's
433
- * `Origin` is held to the origin of it, and the origins of an endpoint's
434
- * credentials are the set the HTTP auth ops answer CORS for. Keeping the
435
- * origin alongside instead would be a second copy of one fact, able to
436
- * disagree with the URL a person is actually sent to. A person using web
437
- * UIs at two origins holds two credentials, one per origin; two UIs under
438
- * one origin are one place to every check here, there being no path in an
439
- * `Origin` header to tell them apart by.
440
- *
441
- * Apart from `endpoint` because the two answer different questions: which
442
- * page may speak, and which instance it may speak to. */
443
- webui: WebUi,
444
- /** The authenticator's counter, when it keeps one. Synced passkeys report
445
- * zero forever, so only a pair of non-zero readings says anything, and a
446
- * reading below the last one is a refusal. */
447
- sign_count: Type.Optional(Type.Integer({ minimum: 0 })),
448
- /** The BE flag of the authenticator data at registration: whether this
449
- * credential is one the authenticator may back up, which in practice is
450
- * what separates a passkey synced across a person's devices from one that
451
- * lives on the single device it was made on.
452
- *
453
- * A hint and nothing else, like the address and the user agent beside it:
454
- * nothing is admitted or refused by it. It is here so the person reading
455
- * their own list can tell "this is my iCloud passkey, it is on every device
456
- * I own" from "this is the key on the stick in my drawer" — which decides
457
- * what removing the line actually costs them. */
458
- backup_eligible: Type.Optional(Type.Boolean()),
459
- /** The BS flag of the same authenticator data: whether the credential was
460
- * backed up at that moment. Read beside `backup_eligible` — eligible and
461
- * not yet backed up is an ordinary state on a device that has just made the
462
- * key, and it too decides nothing. */
463
- backup_state: Type.Optional(Type.Boolean()),
464
- /** The label the administrator put on the registration URL, carried over
465
- * from the claims it was spent against. */
466
- issued_label: Type.Optional(Type.String({ maxLength: 128 })),
467
- /** The label the person put on this device as they registered it. */
468
- device_label: Type.Optional(Type.String({ maxLength: 128 })),
469
- registered_at: Timestamp,
470
- /** Where the registration came from and what browser sent it.
471
- *
472
- * None of this authenticates anything, and nothing is ever admitted or
473
- * refused by it — an address is trivially chosen by whoever is making the
474
- * request. They are here to be recognised by the one person reading their
475
- * own list: an address that is their home provider's and a browser that is
476
- * the one they use is how they place a line as theirs, or fail to, which is
477
- * the whole reason to keep it. The same holds of the pair below. */
478
- registered_ip: Type.Optional(Type.String({ minLength: 1, maxLength: 45 })),
479
- registered_user_agent: Type.Optional(Type.String({ maxLength: 512 })),
480
- /** When this credential last answered a challenge, and from where. A
481
- * credential the person no longer recognises is one they remove. */
482
- last_used_at: Type.Optional(Timestamp),
483
- last_used_ip: Type.Optional(Type.String({ minLength: 1, maxLength: 45 })),
484
- last_used_user_agent: Type.Optional(Type.String({ maxLength: 512 })),
485
599
  },
486
600
  { $id: "CredentialRecord" },
487
601
  );
488
602
  export type CredentialRecord = Static<typeof CredentialRecord>;
489
603
 
604
+ /** That one person owns one instance, which is the whole of what admits them to
605
+ * it.
606
+ *
607
+ * A record rather than something read off the mesh. Belonging to a mesh admits
608
+ * nobody: if it did, adding an instance would widen every person's reach at
609
+ * once and there would be no way to take one instance back. Here, granting and
610
+ * revoking are each one line, and neither says anything about how the instances
611
+ * are wired to each other.
612
+ *
613
+ * Several people may own one instance and one person may own several. Which
614
+ * endpoint a request arrived at is not part of this and is not compared with
615
+ * anything: an instance reached through a load balancer it shares with its
616
+ * peers admits the same people as one reached directly. */
617
+ /** Who put a granting there, which is either a person acting on an
618
+ * authenticated channel or an instance whose command line was used.
619
+ *
620
+ * Two shapes rather than one id, because the two are not the same kind of
621
+ * thing and a bare string would leave a reader guessing which they held. A
622
+ * granting made from a terminal has no user behind it at all — what authorizes
623
+ * it is standing at that machine — so a field that could only name a person
624
+ * would be empty exactly where the first granting of every instance is made.
625
+ *
626
+ * A hint, like the labels and the addresses: it decides nothing, and it is the
627
+ * caller's own account of itself. Vouching for who actually wrote a record is
628
+ * not this field's job and never was; that is what signing the replicated
629
+ * records would answer (see the extension in Consequences). */
630
+ export const GrantedBy = Type.Union(
631
+ [
632
+ Type.Object({ kind: Type.Literal("user"), user: UserId }),
633
+ Type.Object({ kind: Type.Literal("instance"), instance: InstanceId }),
634
+ ],
635
+ { $id: "GrantedBy" },
636
+ );
637
+ export type GrantedBy = Static<typeof GrantedBy>;
638
+
639
+ export const OwnershipRecord = Type.Object(
640
+ {
641
+ kind: Type.Literal("ownership"),
642
+ user: UserId,
643
+ instance: InstanceId,
644
+ /** Names this granting, and nothing else. Random, settled when the record
645
+ * is written, and never reused.
646
+ *
647
+ * It is in the key (`ownership/<instance>/<user>/<grant>`) so that giving an
648
+ * instance up and taking it again are two records rather than one key
649
+ * written twice. A tombstone refuses every later write to its key and is
650
+ * kept without end, so a key made only of the instance and the person would
651
+ * make the first removal final: the person could never be an owner of that
652
+ * instance again, and nothing here could undo it. With this, the removal
653
+ * ends one granting and a later one begins another.
654
+ *
655
+ * What answers "does this person own this instance" is therefore not one
656
+ * record but whether any granting for the pair is still alive. */
657
+ grant: Base64Url,
658
+ granted_at: Timestamp,
659
+ /** Who added this owner: the person who did it from an authenticated
660
+ * channel, or the instance whose command line was used. Here so that a
661
+ * person reading a list of several owners can see how each came to be
662
+ * there. */
663
+ granted_by: Type.Optional(GrantedBy),
664
+ },
665
+ { $id: "OwnershipRecord" },
666
+ );
667
+ export type OwnershipRecord = Static<typeof OwnershipRecord>;
668
+
490
669
  /** One person's tokens, in the generation that stands and the one before it.
491
670
  *
492
671
  * The previous generation is kept so that a reply lost on the way — the client
@@ -496,24 +675,30 @@ export type CredentialRecord = Static<typeof CredentialRecord>;
496
675
  export const TokenFamily = Type.Object(
497
676
  {
498
677
  kind: Type.Literal("token_family"),
499
- sub: Subject,
500
- /** The instance that minted the family and the only one that may write it. */
678
+ user: UserId,
679
+ /** The instance that minted the family.
680
+ *
681
+ * A record of where it came from and not a restriction on who may write it:
682
+ * any instance the person owns rotates the family where the request landed,
683
+ * which is what keeps a refresh working while the minting instance is down.
684
+ * Two of them rotating at once is a collision the losing generation does not
685
+ * survive, and the client it belonged to signs in again. */
501
686
  iss: InstanceId,
502
- /** The web UI the page that authenticated was served from, carried over
503
- * from the credential that answered.
687
+ /** The origin of the page that authenticated, carried over from the
688
+ * credential that answered.
504
689
  *
505
690
  * What a connection presenting one of these tokens is held to: the
506
- * handshake compares `originOf` this with the `Origin` the browser states,
507
- * and a page from anywhere else is refused however good the token is —
508
- * refused as an upgrade that does not happen, there being no connection yet
509
- * to answer an error on. A handshake that states no `Origin` at all is
510
- * refused the same way: every gate has to be passed, and a caller with
511
- * nothing to compare has not passed this one. Without it a token that leaked would be usable
512
- * from any page at all, since it says who the person is and nothing about
513
- * what is holding it. It lives on the family rather than inside the token's
514
- * own spelling because every instance has the family and none of them has
515
- * the minting instance's reading of an opaque value. */
516
- webui: WebUi,
691
+ * handshake compares this with the `Origin` the browser states, and a page
692
+ * from anywhere else is refused however good the token is — refused as an
693
+ * upgrade that does not happen, there being no connection yet to answer an
694
+ * error on. A handshake that states no `Origin` at all is refused the same
695
+ * way: every gate has to be passed, and a caller with nothing to compare has
696
+ * not passed this one. Without it a token that leaked would be usable from
697
+ * any page at all, since it says who the person is and nothing about what is
698
+ * holding it. It lives on the family rather than inside the token's own
699
+ * spelling because every instance has the family and none of them has the
700
+ * minting instance's reading of an opaque value. */
701
+ origin: Origin,
517
702
  access: Type.Object({ value: Base64Url, expires_at: Timestamp }),
518
703
  refresh: Type.Object({ value: Base64Url, expires_at: Timestamp }),
519
704
  /** When the family was last rotated, and what the client said prompted it.
@@ -542,11 +727,13 @@ export const TokenFamily = Type.Object(
542
727
  * retired token still inside its lifetime would be a live secret copied
543
728
  * around for no purpose it could serve. A digest answers the one question
544
729
  * asked of it, that a value presented now was once issued here and is no
545
- * longer, which fails the whole family.
730
+ * longer, which fails the whole family. Matching one of these is the only
731
+ * thing that does: a value this family knows nothing of was never issued by
732
+ * it, and refusing the call is the whole of the answer.
546
733
  *
547
- * Written by the `iss` alone, like the rest of the family, and replicated,
548
- * so the memory survives that instance restarting and holds wherever the
549
- * reused value is presented. */
734
+ * Written by whichever owned instance rotated, like the rest of the family,
735
+ * and replicated, so the memory survives an instance restarting and holds
736
+ * wherever the reused value is presented. */
550
737
  retired: Type.Optional(
551
738
  Type.Array(
552
739
  Type.Object({
@@ -568,41 +755,146 @@ export type TokenFamily = Static<typeof TokenFamily>;
568
755
  * a set of changes says nothing.
569
756
  *
570
757
  * A tombstone refuses every later write to its key, so a returning peer cannot
571
- * bring back what a person revoked. */
758
+ * bring back what a person revoked. It names no subject of its own: the key it
759
+ * arrives under says what was removed, and a field repeating it would be a
760
+ * second answer able to disagree with the first. */
572
761
  export const AuthTombstone = Type.Object(
573
762
  {
574
763
  kind: Type.Literal("tombstone"),
575
- sub: Subject,
576
764
  deleted_at: Timestamp,
577
- /** When the mark itself may be dropped. Absent on a credential's, which is
578
- * kept without end because the credential it refuses has none either. */
765
+ /** When the mark itself may be dropped. Absent on a credential's and an
766
+ * ownership's, which are kept without end because what they refuse has no
767
+ * expiry of its own to fall back on. */
579
768
  expires_at: Type.Optional(Timestamp),
580
769
  },
581
770
  { $id: "AuthTombstone" },
582
771
  );
583
772
  export type AuthTombstone = Static<typeof AuthTombstone>;
584
773
 
585
- /** One entry of the replicated set, under the key it is matched by. */
774
+ /** One entry of the replicated set, under the key it is matched by.
775
+ *
776
+ * The keys are `user/<user>`, `credential/<credential_id>`,
777
+ * `ownership/<instance>/<user>/<grant>` and `family/<id>`. What a key names is
778
+ * what a tombstone under it removes — one granting rather than the pair, which
779
+ * is what lets an instance be given up and taken again. */
586
780
  export const AuthRecord = Type.Object(
587
781
  {
588
782
  /** What this entry is, mesh-wide. Two instances writing one key hold the
589
783
  * same thing, and the later `updated_at` is what stands. */
590
784
  key: Type.String({ minLength: 1, maxLength: 256 }),
591
785
  updated_at: Timestamp,
592
- body: Type.Union([CredentialRecord, TokenFamily, AuthTombstone]),
786
+ body: Type.Union([UserRecord, CredentialRecord, OwnershipRecord, TokenFamily, AuthTombstone]),
593
787
  },
594
788
  { $id: "AuthRecord" },
595
789
  );
596
790
  export type AuthRecord = Static<typeof AuthRecord>;
597
791
 
598
- /** The `auth.records` topic: how credentials and token families reach every
599
- * instance.
792
+ // --- reading one's own account --------------------------------------------
793
+
794
+ export const AuthAccountReadArgs = Type.Object({});
795
+ export type AuthAccountReadArgs = Static<typeof AuthAccountReadArgs>;
796
+
797
+ /** Who the caller is, what answers for them, and what they own — the three
798
+ * things a person has, in one reply.
799
+ *
800
+ * One op rather than three because they are one picture: a person reading this
801
+ * is deciding whether a line is theirs and whether to remove it, and a passkey
802
+ * read apart from the instances it opens does not answer that. Named for the
803
+ * account instead of for any of the three, so none of them reads as an
804
+ * appendage of another.
805
+ *
806
+ * It answers about the caller and nobody else. There is no shape here for
807
+ * reading another person's account: owning an instance with someone else does
808
+ * not make either of them an administrator of the other. */
809
+ export const AuthAccountReadResult = Type.Object(
810
+ {
811
+ user: UserRecord,
812
+ /** Every passkey that answers for this person, at every origin. Without the
813
+ * public keys: a public key is how an assertion is verified and is of no
814
+ * use to a person reading a list, and what a reply does not carry cannot be
815
+ * read out of one. */
816
+ credentials: Type.Array(CredentialRecordPublic),
817
+ /** The instances this person owns, with where each is reached when it
818
+ * publishes an address at all. The endpoint is stated for the person's sake
819
+ * — an instance id names but does not locate — and it is read off the
820
+ * instance rather than out of the ownership, which holds no address. */
821
+ instances: Type.Array(
822
+ Type.Object({
823
+ instance: InstanceId,
824
+ endpoint: Type.Optional(Endpoint),
825
+ granted_at: Timestamp,
826
+ granted_by: Type.Optional(GrantedBy),
827
+ }),
828
+ ),
829
+ },
830
+ { $id: "AuthAccountReadResult" },
831
+ );
832
+ export type AuthAccountReadResult = Static<typeof AuthAccountReadResult>;
833
+
834
+ export const AuthAccountReadRequest = request("auth.account.read", AuthAccountReadArgs);
835
+ export const AuthAccountReadResponse = response("auth.account.read", AuthAccountReadResult);
836
+
837
+ // --- letting go of an instance and of a passkey ---------------------------
838
+
839
+ /** Gives up one instance: the ownership that admitted this person to it is
840
+ * removed, and nothing else of theirs changes.
841
+ *
842
+ * Named by the instance alone. An ownership is keyed by the instance and the
843
+ * person, and the person is the caller — there is no shape here for removing
844
+ * somebody else's ownership, an instance's owners not being its administrators
845
+ * of one another.
846
+ *
847
+ * Every granting of that instance to this person ends, there being no shape
848
+ * here for giving up one of two grantings of the same thing. Being made an
849
+ * owner again afterwards is a new granting and is not refused by what this
850
+ * left behind.
851
+ *
852
+ * Removing the ownership of the instance the connection is on is refused
853
+ * (`auth_in_use`). It is theirs to remove; asking from another instance they
854
+ * own, or from the command line, is all it takes. */
855
+ export const AuthOwnershipRemoveArgs = Type.Object({ instance: InstanceId });
856
+ export type AuthOwnershipRemoveArgs = Static<typeof AuthOwnershipRemoveArgs>;
857
+
858
+ export const AuthOwnershipRemoveResult = Type.Object({});
859
+ export type AuthOwnershipRemoveResult = Static<typeof AuthOwnershipRemoveResult>;
860
+
861
+ export const AuthOwnershipRemoveRequest = request("auth.ownership.remove", AuthOwnershipRemoveArgs);
862
+ export const AuthOwnershipRemoveResponse = response(
863
+ "auth.ownership.remove",
864
+ AuthOwnershipRemoveResult,
865
+ );
866
+
867
+ /** Removes one passkey. The origin it was made at leaves the allowed set with
868
+ * the last credential naming it, which is the only way an origin ever leaves.
869
+ *
870
+ * The credential this session authenticated with is refused (`auth_in_use`),
871
+ * for the same reason an ownership underfoot is: a person removing the key they
872
+ * are holding would be locking themselves out mid-sentence. Another passkey, or
873
+ * another session, removes it. */
874
+ export const AuthCredentialRemoveArgs = Type.Object({ credential_id: Base64Url });
875
+ export type AuthCredentialRemoveArgs = Static<typeof AuthCredentialRemoveArgs>;
876
+
877
+ export const AuthCredentialRemoveResult = Type.Object({});
878
+ export type AuthCredentialRemoveResult = Static<typeof AuthCredentialRemoveResult>;
879
+
880
+ export const AuthCredentialRemoveRequest = request(
881
+ "auth.credential.remove",
882
+ AuthCredentialRemoveArgs,
883
+ );
884
+ export const AuthCredentialRemoveResponse = response(
885
+ "auth.credential.remove",
886
+ AuthCredentialRemoveResult,
887
+ );
888
+
889
+ /** The `auth.records` topic: how users, credentials, ownerships and token
890
+ * families reach every instance.
600
891
  *
601
892
  * Apart from the store because of who may read it. The store is the person's to
602
893
  * read and write, and these are secrets that authenticate them — a token read
603
- * out of the store would be the person's session, and a credential written into
604
- * it would be a new way in. Only instances subscribe, and a relay carries the
605
- * frames as the instance it is rather than on a person's behalf. */
894
+ * out of the store would be the person's session, and a credential or an
895
+ * ownership written into it would be a new way in. Only instances subscribe,
896
+ * and a relay carries the frames as the instance it is rather than on a
897
+ * person's behalf. */
606
898
  export const AuthRecordsFrame = topicFrame(
607
899
  "auth.records",
608
900
  Type.Object({ records: Type.Array(AuthRecord) }),