@ccmsg/cli 0.2.4 → 0.2.7
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/auth/auth.ts +21 -1
- package/src/mesh/relay.ts +21 -5
- package/src/sessions/last-live.ts +17 -3
- package/src/sessions/registry.ts +1 -1
package/package.json
CHANGED
package/src/auth/auth.ts
CHANGED
|
@@ -59,6 +59,19 @@ export const REFRESH_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
59
59
|
* short to be a second usable token. */
|
|
60
60
|
export const PREVIOUS_GRACE_MS = 60_000;
|
|
61
61
|
|
|
62
|
+
/** How much of an access token's life must be left for a rotation to keep it.
|
|
63
|
+
*
|
|
64
|
+
* A family has one access token and every page a person has open presents it,
|
|
65
|
+
* so minting a new one on each rotation would take the token out from under the
|
|
66
|
+
* pages that are already holding it. Rotation therefore keeps the standing
|
|
67
|
+
* access token while it has this much life left, and mints only when it is
|
|
68
|
+
* running out — the pages share one token and renew it together.
|
|
69
|
+
*
|
|
70
|
+
* Half, because it is the largest share that still leaves a full half of the
|
|
71
|
+
* token's life to notice the new value in: the window is what bounds how long a
|
|
72
|
+
* page may go on holding a token whose family has already moved on. */
|
|
73
|
+
export const ACCESS_KEEP_MS = ACCESS_TTL_MS / 2;
|
|
74
|
+
|
|
62
75
|
/** How many times a six-digit code may be got wrong before the registration URL
|
|
63
76
|
* is spent.
|
|
64
77
|
*
|
|
@@ -706,11 +719,18 @@ export class Auth {
|
|
|
706
719
|
return { sub: held.body.sub, access: held.body.access, refresh: held.body.refresh };
|
|
707
720
|
}
|
|
708
721
|
const at = this.#now();
|
|
722
|
+
// The refresh token rotates every time; the access token is the family's
|
|
723
|
+
// one token and is shared by every page the person has open, so it is kept
|
|
724
|
+
// until it is close enough to running out to be worth replacing.
|
|
725
|
+
const access =
|
|
726
|
+
held.body.access.expires_at - at >= ACCESS_KEEP_MS
|
|
727
|
+
? held.body.access
|
|
728
|
+
: { value: token(), expires_at: at + ACCESS_TTL_MS };
|
|
709
729
|
const rotated: TokenFamily = {
|
|
710
730
|
kind: "token_family",
|
|
711
731
|
sub: held.body.sub,
|
|
712
732
|
iss: this.deps.self,
|
|
713
|
-
access
|
|
733
|
+
access,
|
|
714
734
|
refresh: { value: token(), expires_at: at + REFRESH_TTL_MS },
|
|
715
735
|
previous_refresh: { value: held.body.refresh.value, expires_at: at + PREVIOUS_GRACE_MS },
|
|
716
736
|
// The value going out of service is remembered as a digest for as long as
|
package/src/mesh/relay.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type AgentInfo,
|
|
2
3
|
type InstanceId,
|
|
3
4
|
LAST_LIVE_RETENTION_MS,
|
|
5
|
+
type LastLiveSession,
|
|
4
6
|
type PeerInfo,
|
|
5
7
|
PLAIN_TOPICS,
|
|
6
8
|
type Sid,
|
|
@@ -116,12 +118,16 @@ export class Relay {
|
|
|
116
118
|
return values;
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
/** Which instance a session belongs to, read from the
|
|
120
|
-
*
|
|
121
|
+
/** Which instance a session belongs to, read from the cluster values the
|
|
122
|
+
* peers stated (§7.3).
|
|
121
123
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
124
|
+
* `peers` names every session an instance currently holds — connected or in
|
|
125
|
+
* `last_live` — and is checked first. A session hello has not reached yet
|
|
126
|
+
* has no row there but the harness may already know of it, so `agents` is
|
|
127
|
+
* checked next; `last_live` is the last resort for one whose instance has
|
|
128
|
+
* not stated `agents` at all. Every row names its own instance rather than
|
|
129
|
+
* the one that relayed it, so a value that travelled through a third
|
|
130
|
+
* instance still points at the session's own. */
|
|
125
131
|
owner(sid: Sid): InstanceId | undefined {
|
|
126
132
|
this.#sweep();
|
|
127
133
|
for (const held of this.#held.values()) {
|
|
@@ -129,6 +135,16 @@ export class Relay {
|
|
|
129
135
|
const row = value?.peers?.find((peer) => peer.sid === sid);
|
|
130
136
|
if (row !== undefined) return row.instance;
|
|
131
137
|
}
|
|
138
|
+
for (const held of this.#held.values()) {
|
|
139
|
+
const value = held.get("agents") as { agents?: AgentInfo[] } | undefined;
|
|
140
|
+
const row = value?.agents?.find((agent) => agent.sid === sid);
|
|
141
|
+
if (row !== undefined) return row.instance;
|
|
142
|
+
}
|
|
143
|
+
for (const held of this.#held.values()) {
|
|
144
|
+
const value = held.get("peers") as { last_live?: LastLiveSession[] } | undefined;
|
|
145
|
+
const row = value?.last_live?.find((session) => session.sid === sid);
|
|
146
|
+
if (row !== undefined) return row.instance;
|
|
147
|
+
}
|
|
132
148
|
return undefined;
|
|
133
149
|
}
|
|
134
150
|
|
|
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
LAST_LIVE_RETENTION_MS,
|
|
5
|
+
type InstanceId,
|
|
5
6
|
type LastLiveSession,
|
|
6
7
|
type Sid,
|
|
7
8
|
type Timestamp,
|
|
@@ -35,7 +36,14 @@ const VERSION = 1;
|
|
|
35
36
|
export class LastLiveStore {
|
|
36
37
|
#entries = new Map<Sid, StoredEntry>();
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
/** `id` is this instance's own: every entry this store holds is by
|
|
40
|
+
* definition an observation *this* instance made, so `instance` is forced
|
|
41
|
+
* to it on both ends (load and record) rather than trusted from whatever
|
|
42
|
+
* the field on disk happens to say. */
|
|
43
|
+
constructor(
|
|
44
|
+
private readonly file: string,
|
|
45
|
+
private readonly id: InstanceId,
|
|
46
|
+
) {}
|
|
39
47
|
|
|
40
48
|
/** Read at startup (§8.3 step 4), before anything can ask for the list. A
|
|
41
49
|
* file that is missing or unreadable starts an empty list: the daemon has no
|
|
@@ -50,7 +58,9 @@ export class LastLiveStore {
|
|
|
50
58
|
const sessions = (document as Document | null)?.sessions;
|
|
51
59
|
if (!Array.isArray(sessions)) return;
|
|
52
60
|
for (const entry of sessions as StoredEntry[]) {
|
|
53
|
-
if (typeof entry?.sid === "string")
|
|
61
|
+
if (typeof entry?.sid === "string") {
|
|
62
|
+
this.#entries.set(entry.sid, { ...entry, instance: this.id });
|
|
63
|
+
}
|
|
54
64
|
}
|
|
55
65
|
this.#prune(now);
|
|
56
66
|
}
|
|
@@ -71,7 +81,11 @@ export class LastLiveStore {
|
|
|
71
81
|
* Paused rather than Disappeared (§5.2). */
|
|
72
82
|
record(entry: StoredEntry): void {
|
|
73
83
|
const stopped = this.#entries.get(entry.sid)?.stopped_at ?? entry.stopped_at;
|
|
74
|
-
this.#entries.set(entry.sid,
|
|
84
|
+
this.#entries.set(entry.sid, {
|
|
85
|
+
...entry,
|
|
86
|
+
instance: this.id,
|
|
87
|
+
...(stopped === undefined ? {} : { stopped_at: stopped }),
|
|
88
|
+
});
|
|
75
89
|
this.#save();
|
|
76
90
|
}
|
|
77
91
|
|
package/src/sessions/registry.ts
CHANGED
|
@@ -190,7 +190,7 @@ export class Sessions implements UpstreamResource {
|
|
|
190
190
|
() => this.changed(),
|
|
191
191
|
deps.pollMs,
|
|
192
192
|
);
|
|
193
|
-
this.#lastLive = new LastLiveStore(join(deps.stateDir, "last-live.json"));
|
|
193
|
+
this.#lastLive = new LastLiveStore(join(deps.stateDir, "last-live.json"), deps.self);
|
|
194
194
|
this.#lastLive.load();
|
|
195
195
|
this.#terminals =
|
|
196
196
|
deps.terminals === undefined
|