@ccmsg/protocol 1.4.0 → 1.7.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/package.json +1 -1
- package/src/common/auth.ts +69 -4
- package/src/common/hello.ts +10 -6
- package/src/identifiers.ts +17 -4
package/package.json
CHANGED
package/src/common/auth.ts
CHANGED
|
@@ -84,7 +84,9 @@ export const RegisterClaims = Type.Object(
|
|
|
84
84
|
sub: Subject,
|
|
85
85
|
/** The instance's name as a person operates it, for display. */
|
|
86
86
|
unit: Type.String({ minLength: 1 }),
|
|
87
|
-
/** The endpoint the credential is being registered for.
|
|
87
|
+
/** The endpoint the credential is being registered for. The base URL: the
|
|
88
|
+
* registration is posted to `<endpoint>auth/register`, and the cookie set
|
|
89
|
+
* for it hangs under the same prefix. */
|
|
88
90
|
endpoint: Endpoint,
|
|
89
91
|
/** The WebAuthn relying party: a domain, not an origin. Either the
|
|
90
92
|
* endpoint's host or a registrable suffix of it. */
|
|
@@ -210,9 +212,28 @@ export const AuthAssertResponse = response("auth_assert", AuthAssertResult);
|
|
|
210
212
|
|
|
211
213
|
// --- refreshing a token pair ----------------------------------------------
|
|
212
214
|
|
|
213
|
-
/**
|
|
214
|
-
*
|
|
215
|
-
|
|
215
|
+
/** Why a client asked for a fresh pair. Stated by the caller and never checked,
|
|
216
|
+
* so nothing may be decided by it; it is kept only to be recognised later by
|
|
217
|
+
* the person whose sessions they are. */
|
|
218
|
+
export const AuthRefreshReason = Type.Union(
|
|
219
|
+
[Type.Literal("reload"), Type.Literal("expiring"), Type.Literal("reconnect")],
|
|
220
|
+
{ $id: "AuthRefreshReason" },
|
|
221
|
+
);
|
|
222
|
+
export type AuthRefreshReason = Static<typeof AuthRefreshReason>;
|
|
223
|
+
|
|
224
|
+
/** The refresh token is not among the arguments: it is a cookie the carrier
|
|
225
|
+
* already holds, and a caller that could state it is a caller that could read
|
|
226
|
+
* it. What is left is why the caller is asking, which nothing is decided by. */
|
|
227
|
+
export const AuthRefreshTokenArgs = Type.Object({
|
|
228
|
+
/** What prompted this refresh, as the client knows it: the page was loaded
|
|
229
|
+
* again, the access token was about to expire, or a dropped connection is
|
|
230
|
+
* being remade. A hint kept on the family (`last_refresh`) for a person
|
|
231
|
+
* reading their own sessions back — a run of `reconnect` at an hour they were
|
|
232
|
+
* asleep is something to recognise. The value is the caller's word and is
|
|
233
|
+
* never checked, so nothing may turn on it; an unstated reason is as valid a
|
|
234
|
+
* refresh as any. */
|
|
235
|
+
reason: Type.Optional(AuthRefreshReason),
|
|
236
|
+
});
|
|
216
237
|
export type AuthRefreshTokenArgs = Static<typeof AuthRefreshTokenArgs>;
|
|
217
238
|
|
|
218
239
|
export const AuthRefreshTokenResult = AuthSession;
|
|
@@ -320,6 +341,18 @@ export const CredentialRecord = Type.Object(
|
|
|
320
341
|
* was created against and what an assertion naming a handle is checked
|
|
321
342
|
* against. */
|
|
322
343
|
user_handle: Base64Url,
|
|
344
|
+
/** The endpoint this credential was registered for, as the registration's
|
|
345
|
+
* claims stated it.
|
|
346
|
+
*
|
|
347
|
+
* What the credential is good for, and the whole of it: an assertion is
|
|
348
|
+
* accepted only where the origin matches and the request's path falls under
|
|
349
|
+
* this base URL. `https://h.example/` and `https://h.example/personal/` are
|
|
350
|
+
* two endpoints and take two registrations, even on one host and one
|
|
351
|
+
* relying party — the RP ID says which domain an authenticator will answer
|
|
352
|
+
* for, which is a coarser thing than which instance a person has been
|
|
353
|
+
* admitted to. Binding to the base URL rather than the origin is what keeps
|
|
354
|
+
* one instance's credential from being a way into its neighbour. */
|
|
355
|
+
endpoint: Endpoint,
|
|
323
356
|
/** The relying party this credential was created under, as the claims of
|
|
324
357
|
* the registration that made it stated. Written by the registration and not
|
|
325
358
|
* derived later: a passkey only answers for the domain it was made under,
|
|
@@ -331,6 +364,22 @@ export const CredentialRecord = Type.Object(
|
|
|
331
364
|
* zero forever, so only a pair of non-zero readings says anything, and a
|
|
332
365
|
* reading below the last one is a refusal. */
|
|
333
366
|
sign_count: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
367
|
+
/** The BE flag of the authenticator data at registration: whether this
|
|
368
|
+
* credential is one the authenticator may back up, which in practice is
|
|
369
|
+
* what separates a passkey synced across a person's devices from one that
|
|
370
|
+
* lives on the single device it was made on.
|
|
371
|
+
*
|
|
372
|
+
* A hint and nothing else, like the address and the user agent beside it:
|
|
373
|
+
* nothing is admitted or refused by it. It is here so the person reading
|
|
374
|
+
* their own list can tell "this is my iCloud passkey, it is on every device
|
|
375
|
+
* I own" from "this is the key on the stick in my drawer" — which decides
|
|
376
|
+
* what removing the line actually costs them. */
|
|
377
|
+
backup_eligible: Type.Optional(Type.Boolean()),
|
|
378
|
+
/** The BS flag of the same authenticator data: whether the credential was
|
|
379
|
+
* backed up at that moment. Read beside `backup_eligible` — eligible and
|
|
380
|
+
* not yet backed up is an ordinary state on a device that has just made the
|
|
381
|
+
* key, and it too decides nothing. */
|
|
382
|
+
backup_state: Type.Optional(Type.Boolean()),
|
|
334
383
|
/** The label the administrator put on the registration URL, carried over
|
|
335
384
|
* from the claims it was spent against. */
|
|
336
385
|
issued_label: Type.Optional(Type.String({ maxLength: 128 })),
|
|
@@ -371,6 +420,22 @@ export const TokenFamily = Type.Object(
|
|
|
371
420
|
iss: InstanceId,
|
|
372
421
|
access: Type.Object({ value: Base64Url, expires_at: Timestamp }),
|
|
373
422
|
refresh: Type.Object({ value: Base64Url, expires_at: Timestamp }),
|
|
423
|
+
/** When the family was last rotated, and what the client said prompted it.
|
|
424
|
+
*
|
|
425
|
+
* The same kind of thing as a credential's `last_used_*`: a hint for the
|
|
426
|
+
* one person reading their own sessions, never a reason to admit or refuse
|
|
427
|
+
* anything. `reason` in particular is the caller's unchecked word. Only the
|
|
428
|
+
* most recent rotation is kept — a family holds what stands now, and a log
|
|
429
|
+
* of every generation would be a second store hidden inside a record that
|
|
430
|
+
* travels to every instance. */
|
|
431
|
+
last_refresh: Type.Optional(
|
|
432
|
+
Type.Object({
|
|
433
|
+
at: Timestamp,
|
|
434
|
+
reason: Type.Optional(AuthRefreshReason),
|
|
435
|
+
ip: Type.Optional(Type.String({ minLength: 1, maxLength: 45 })),
|
|
436
|
+
user_agent: Type.Optional(Type.String({ maxLength: 512 })),
|
|
437
|
+
}),
|
|
438
|
+
),
|
|
374
439
|
/** The generation before the current one, while the grace for it lasts. */
|
|
375
440
|
previous_refresh: Type.Optional(Type.Object({ value: Base64Url, expires_at: Timestamp })),
|
|
376
441
|
/** What every generation retired before that was, kept only as a digest and
|
package/src/common/hello.ts
CHANGED
|
@@ -13,9 +13,10 @@ export const MeshHello = Type.Object(
|
|
|
13
13
|
/** Generation of the mesh handshake format, apart from the protocol
|
|
14
14
|
* generation so the handshake can change without the wire changing. */
|
|
15
15
|
ver: Type.Integer({ minimum: 1 }),
|
|
16
|
-
/** The
|
|
16
|
+
/** The base URL the connecting instance claims to be published at — the
|
|
17
|
+
* endpoint itself, not the `<endpoint>ws` it dialed to get here. */
|
|
17
18
|
iss: Endpoint,
|
|
18
|
-
/** The
|
|
19
|
+
/** The base URL it believes it is connecting to. Compared whole
|
|
19
20
|
* against the receiver's own URL, which is what stops a signature made for
|
|
20
21
|
* one instance from being replayed at another on the same host. */
|
|
21
22
|
aud: Endpoint,
|
|
@@ -90,9 +91,11 @@ export const InstanceInfo = Type.Object(
|
|
|
90
91
|
* configured is known before anything answers there, and leaving such a
|
|
91
92
|
* peer out of the list would hide the very entry whose link is down. */
|
|
92
93
|
id: Type.Optional(InstanceId),
|
|
93
|
-
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
94
|
+
/** The base URL it is published at, which a peer dials as `<endpoint>ws`,
|
|
95
|
+
* that scheme included — the WebSocket upgrades from an HTTP request.
|
|
96
|
+
* An attribute of the instance like the host below: it is what a peer
|
|
97
|
+
* connects to and authenticates against, and it may change under a fixed
|
|
98
|
+
* `id` when the instance moves. Absent for the same
|
|
96
99
|
* reason it is absent from the reply's own `endpoint`: an instance in no
|
|
97
100
|
* mesh has no URL to be dialed at, including on its own line. */
|
|
98
101
|
endpoint: Type.Optional(Endpoint),
|
|
@@ -110,7 +113,8 @@ export const HelloResult = Type.Object({
|
|
|
110
113
|
protocol_version: Type.Integer({ minimum: 1 }),
|
|
111
114
|
/** The instance answering. Every other id in the reply is relative to it. */
|
|
112
115
|
instance: InstanceId,
|
|
113
|
-
/**
|
|
116
|
+
/** The base URL the answering instance is published at, under which its own
|
|
117
|
+
* routes (`ws`, `mesh/…`, `auth/…`) sit. Stated beside the id because the
|
|
114
118
|
* caller reached it by some URL of its own — a proxy's, an alias — and what a
|
|
115
119
|
* peer is to dial is neither that nor derivable from the id. Absent on an
|
|
116
120
|
* instance that joins no mesh: it is reached by the people and sessions on
|
package/src/identifiers.ts
CHANGED
|
@@ -43,9 +43,22 @@ export const InstanceId = Type.String({
|
|
|
43
43
|
});
|
|
44
44
|
export type InstanceId = Static<typeof InstanceId>;
|
|
45
45
|
|
|
46
|
-
/** Where an instance is
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
/** Where an instance is published: the base URL everything it serves hangs
|
|
47
|
+
* under, ending in a slash and naming no route of its own.
|
|
48
|
+
*
|
|
49
|
+
* The routes are below it and are not part of it — `<endpoint>ws` for the
|
|
50
|
+
* WebSocket, `<endpoint>mesh/…`, `<endpoint>auth/…`, `<endpoint>webhook/…`. All
|
|
51
|
+
* of them keep the endpoint's own scheme: a WebSocket starts as an HTTP request
|
|
52
|
+
* that upgrades, so there is no second spelling of the URL and nothing to
|
|
53
|
+
* rewrite. Naming the base rather than one of
|
|
54
|
+
* them is what lets a transport be added or replaced without the value that
|
|
55
|
+
* identifies where an instance lives changing with it, and what lets the HTTP
|
|
56
|
+
* routes be spelled without stripping a suffix off first.
|
|
57
|
+
*
|
|
58
|
+
* Compared as a whole string, path included (mesh-peer-auth §4.2 — one origin
|
|
59
|
+
* may host several instances, so an origin-level comparison would confuse
|
|
60
|
+
* them). The trailing slash is required so that comparison is exact: `/ccmsg`
|
|
61
|
+
* and `/ccmsg/` would otherwise be two spellings of one instance.
|
|
49
62
|
*
|
|
50
63
|
* Apart from `InstanceId` because the two answer different questions and change
|
|
51
64
|
* on different occasions. This is what a peer dials, what the TLS certificate
|
|
@@ -55,7 +68,7 @@ export type InstanceId = Static<typeof InstanceId>;
|
|
|
55
68
|
* not alter. */
|
|
56
69
|
export const Endpoint = Type.String({
|
|
57
70
|
$id: "Endpoint",
|
|
58
|
-
pattern: "^
|
|
71
|
+
pattern: "^https?://[^/?#\\s]+(/[^?#\\s]*)?/$",
|
|
59
72
|
});
|
|
60
73
|
export type Endpoint = Static<typeof Endpoint>;
|
|
61
74
|
|