@ccmsg/protocol 1.6.0 → 1.8.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 +69 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/protocol",
3
- "version": "1.6.0",
3
+ "version": "1.8.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;
@@ -287,7 +306,21 @@ export const AuthResolveResponse = response("auth_resolve", AuthResolveResult);
287
306
  * parallel would merge by last write and lose a generation, which reads exactly
288
307
  * like a stolen token being replayed — so the rotation is forwarded rather than
289
308
  * done where the request landed. */
290
- export const AuthRotateArgs = Type.Object({ refresh_token: Base64Url });
309
+ export const AuthRotateArgs = Type.Object({
310
+ refresh_token: Base64Url,
311
+ /** What the receiving instance observed of the caller, carried to the issuer
312
+ * for `last_refresh`. The person is at the other end of the receiver's
313
+ * connection, not the issuer's, so these are only knowable there; forwarded
314
+ * without them, a rotation would be remembered as a time and nothing else.
315
+ *
316
+ * Stated by the receiver and never checked by the issuer — the same standing
317
+ * as the values on a rotation that was not forwarded, which the client and
318
+ * its connection are equally the only source of. Nothing may be decided by
319
+ * them. */
320
+ reason: Type.Optional(AuthRefreshReason),
321
+ ip: Type.Optional(Type.String({ minLength: 1, maxLength: 45 })),
322
+ user_agent: Type.Optional(Type.String({ maxLength: 512 })),
323
+ });
291
324
  export type AuthRotateArgs = Static<typeof AuthRotateArgs>;
292
325
 
293
326
  /** Both halves, unlike the person-facing ops: the instance that asked for the
@@ -345,6 +378,22 @@ export const CredentialRecord = Type.Object(
345
378
  * zero forever, so only a pair of non-zero readings says anything, and a
346
379
  * reading below the last one is a refusal. */
347
380
  sign_count: Type.Optional(Type.Integer({ minimum: 0 })),
381
+ /** The BE flag of the authenticator data at registration: whether this
382
+ * credential is one the authenticator may back up, which in practice is
383
+ * what separates a passkey synced across a person's devices from one that
384
+ * lives on the single device it was made on.
385
+ *
386
+ * A hint and nothing else, like the address and the user agent beside it:
387
+ * nothing is admitted or refused by it. It is here so the person reading
388
+ * their own list can tell "this is my iCloud passkey, it is on every device
389
+ * I own" from "this is the key on the stick in my drawer" — which decides
390
+ * what removing the line actually costs them. */
391
+ backup_eligible: Type.Optional(Type.Boolean()),
392
+ /** The BS flag of the same authenticator data: whether the credential was
393
+ * backed up at that moment. Read beside `backup_eligible` — eligible and
394
+ * not yet backed up is an ordinary state on a device that has just made the
395
+ * key, and it too decides nothing. */
396
+ backup_state: Type.Optional(Type.Boolean()),
348
397
  /** The label the administrator put on the registration URL, carried over
349
398
  * from the claims it was spent against. */
350
399
  issued_label: Type.Optional(Type.String({ maxLength: 128 })),
@@ -385,6 +434,22 @@ export const TokenFamily = Type.Object(
385
434
  iss: InstanceId,
386
435
  access: Type.Object({ value: Base64Url, expires_at: Timestamp }),
387
436
  refresh: Type.Object({ value: Base64Url, expires_at: Timestamp }),
437
+ /** When the family was last rotated, and what the client said prompted it.
438
+ *
439
+ * The same kind of thing as a credential's `last_used_*`: a hint for the
440
+ * one person reading their own sessions, never a reason to admit or refuse
441
+ * anything. `reason` in particular is the caller's unchecked word. Only the
442
+ * most recent rotation is kept — a family holds what stands now, and a log
443
+ * of every generation would be a second store hidden inside a record that
444
+ * travels to every instance. */
445
+ last_refresh: Type.Optional(
446
+ Type.Object({
447
+ at: Timestamp,
448
+ reason: Type.Optional(AuthRefreshReason),
449
+ ip: Type.Optional(Type.String({ minLength: 1, maxLength: 45 })),
450
+ user_agent: Type.Optional(Type.String({ maxLength: 512 })),
451
+ }),
452
+ ),
388
453
  /** The generation before the current one, while the grace for it lasts. */
389
454
  previous_refresh: Type.Optional(Type.Object({ value: Base64Url, expires_at: Timestamp })),
390
455
  /** What every generation retired before that was, kept only as a digest and