@ccmsg/protocol 1.6.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/common/auth.ts +54 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/protocol",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Wire contract (schema + types + op attribute table) shared by the ccmsg daemon and web UI",
5
5
  "license": "MIT",
6
6
  "author": "kawaz",
@@ -212,9 +212,28 @@ export const AuthAssertResponse = response("auth_assert", AuthAssertResult);
212
212
 
213
213
  // --- refreshing a token pair ----------------------------------------------
214
214
 
215
- /** Takes no arguments: the refresh token is a cookie the carrier already holds,
216
- * and a caller that could state it is a caller that could read it. */
217
- export const AuthRefreshTokenArgs = Type.Object({});
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
+ });
218
237
  export type AuthRefreshTokenArgs = Static<typeof AuthRefreshTokenArgs>;
219
238
 
220
239
  export const AuthRefreshTokenResult = AuthSession;
@@ -345,6 +364,22 @@ export const CredentialRecord = Type.Object(
345
364
  * zero forever, so only a pair of non-zero readings says anything, and a
346
365
  * reading below the last one is a refusal. */
347
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()),
348
383
  /** The label the administrator put on the registration URL, carried over
349
384
  * from the claims it was spent against. */
350
385
  issued_label: Type.Optional(Type.String({ maxLength: 128 })),
@@ -385,6 +420,22 @@ export const TokenFamily = Type.Object(
385
420
  iss: InstanceId,
386
421
  access: Type.Object({ value: Base64Url, expires_at: Timestamp }),
387
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
+ ),
388
439
  /** The generation before the current one, while the grace for it lasts. */
389
440
  previous_refresh: Type.Optional(Type.Object({ value: Base64Url, expires_at: Timestamp })),
390
441
  /** What every generation retired before that was, kept only as a digest and