@freqhole/haruspex 0.3.4 → 0.3.5
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/dist/flows/add-peer/machine.d.ts +5 -0
- package/dist/flows/add-peer/machine.d.ts.map +1 -1
- package/dist/flows/add-peer/machine.js +74 -8
- package/dist/flows/add-peer/machine.test.js +91 -13
- package/dist/flows/add-peer/runner.d.ts.map +1 -1
- package/dist/flows/add-peer/runner.js +15 -3
- package/dist/flows/add-peer/runner.test.js +44 -3
- package/dist/flows/add-peer/types.d.ts +6 -2
- package/dist/flows/add-peer/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -25,6 +25,11 @@ export interface AddPeerContext {
|
|
|
25
25
|
/** the pending record a knock-status re-check is running for. */
|
|
26
26
|
retryPending: PendingRemote | null;
|
|
27
27
|
remote: SavedRemote | null;
|
|
28
|
+
/** an already-saved remote matching the current attempt's target
|
|
29
|
+
* (set at DUPLICATE_RESULT, consulted at CONNECTION_RESULT - see its
|
|
30
|
+
* handling below for why this doesn't block outright the way an http
|
|
31
|
+
* duplicate still does). */
|
|
32
|
+
existingRemote: SavedRemote | null;
|
|
28
33
|
}
|
|
29
34
|
export declare function initialContext(): AddPeerContext;
|
|
30
35
|
export declare function projectState(ctx: AddPeerContext): AddPeerState;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../src/flows/add-peer/machine.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,cAAc,EACd,UAAU,EACV,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAE1C;oCACoC;AACpC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,UAAU,CAAC;IAC7D,OAAO,EAAE,OAAO,GAAG,YAAY,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,kDAAkD;IAClD,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,cAAc,GAAG,IAAI,CAAC;IAClC,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,kEAAkE;IAClE,UAAU,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,iEAAiE;IACjE,YAAY,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../src/flows/add-peer/machine.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,cAAc,EACd,UAAU,EACV,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAE1C;oCACoC;AACpC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,UAAU,CAAC;IAC7D,OAAO,EAAE,OAAO,GAAG,YAAY,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,kDAAkD;IAClD,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,cAAc,GAAG,IAAI,CAAC;IAClC,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,kEAAkE;IAClE,UAAU,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,iEAAiE;IACjE,YAAY,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B;;;iCAG6B;IAC7B,cAAc,EAAE,WAAW,GAAG,IAAI,CAAC;CACpC;AAED,wBAAgB,cAAc,IAAI,cAAc,CAiB/C;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,YAAY,CA2B9D;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,cAAc,CAAC;IACpB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AA4BD,wBAAgB,UAAU,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,GAAG,gBAAgB,CAyerF"}
|
|
@@ -38,6 +38,7 @@ export function initialContext() {
|
|
|
38
38
|
knockDraft: null,
|
|
39
39
|
retryPending: null,
|
|
40
40
|
remote: null,
|
|
41
|
+
existingRemote: null,
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
export function projectState(ctx) {
|
|
@@ -64,8 +65,8 @@ export function projectState(ctx) {
|
|
|
64
65
|
case "knock_sent":
|
|
65
66
|
return { step: "knock_sent" };
|
|
66
67
|
case "complete":
|
|
67
|
-
// remote is always set by the
|
|
68
|
-
return { step: "complete", remote: ctx.remote };
|
|
68
|
+
// remote is always set by the transition that enters this step
|
|
69
|
+
return { step: "complete", remote: ctx.remote, alreadyExisted: ctx.existingRemote !== null };
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
function noop(ctx) {
|
|
@@ -135,18 +136,27 @@ export function transition(ctx, event) {
|
|
|
135
136
|
progress: null,
|
|
136
137
|
serverInfo: null,
|
|
137
138
|
retryPending: null,
|
|
139
|
+
existingRemote: null,
|
|
138
140
|
};
|
|
139
141
|
return { ctx: next, effects: [{ type: "CHECK_DUPLICATE", target }] };
|
|
140
142
|
}
|
|
141
143
|
case "DUPLICATE_RESULT": {
|
|
142
144
|
if (ctx.step !== "testing")
|
|
143
145
|
return noop(ctx);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
// an http duplicate always blocks outright - there's no "maybe it's
|
|
147
|
+
// a player now" reprobe scenario for a plain web remote, and this
|
|
148
|
+
// preserves the tested "each server can only be added once" rule.
|
|
149
|
+
if (event.duplicate && ctx.target?.type === "http") {
|
|
150
|
+
return noop(toUrlInput(ctx, `this server is already added as "${event.duplicate.name}". each server can only be added once.`));
|
|
146
151
|
}
|
|
147
|
-
//
|
|
152
|
+
// a p2p duplicate does NOT block here - it might be an already-
|
|
153
|
+
// paired player whose trust was revoked, or one that's simply in
|
|
154
|
+
// player mode again right now - CONNECTION_RESULT below decides
|
|
155
|
+
// for real off the freshly-probed `player_device` flag, never a
|
|
156
|
+
// stored one (there isn't one - see `SavedRemote`'s doc comment).
|
|
157
|
+
// persist the attempt BEFORE probing, so a closed tab can resume it.
|
|
148
158
|
return {
|
|
149
|
-
ctx,
|
|
159
|
+
ctx: { ...ctx, existingRemote: event.duplicate },
|
|
150
160
|
effects: [
|
|
151
161
|
{
|
|
152
162
|
type: "UPSERT_PENDING",
|
|
@@ -162,6 +172,55 @@ export function transition(ctx, event) {
|
|
|
162
172
|
if (ctx.step !== "testing")
|
|
163
173
|
return noop(ctx);
|
|
164
174
|
const { outcome } = event;
|
|
175
|
+
// a player device is handled identically regardless of `outcome.kind`
|
|
176
|
+
// or whether it's brand-new or already saved: pin-pairing (or, for an
|
|
177
|
+
// already-saved remote, the host UI's live `queryPlayerPresence`
|
|
178
|
+
// check - see AddRemoteModal.tsx's `playerAccess` resource) is the
|
|
179
|
+
// ONLY way in - never the generic knock-request form (`needs_knock`),
|
|
180
|
+
// and never auto-`CREATE_REMOTE`d from a bare `already_authed` (that
|
|
181
|
+
// only proves this peer redeemed SOME pairing code in the past, not
|
|
182
|
+
// that it's currently in the player's live, ephemeral session - see
|
|
183
|
+
// this file's own history for that exact regression). always routes
|
|
184
|
+
// to "auth", on any non-failed outcome, never re-creating the remote
|
|
185
|
+
// from here either way.
|
|
186
|
+
if (outcome.kind !== "failed" && outcome.serverInfo.player_device) {
|
|
187
|
+
return {
|
|
188
|
+
ctx: {
|
|
189
|
+
...ctx,
|
|
190
|
+
step: "auth",
|
|
191
|
+
serverInfo: outcome.serverInfo,
|
|
192
|
+
error: null,
|
|
193
|
+
progress: null,
|
|
194
|
+
},
|
|
195
|
+
effects: [
|
|
196
|
+
{
|
|
197
|
+
type: "UPSERT_PENDING",
|
|
198
|
+
peerAddr: addrKey(ctx),
|
|
199
|
+
patch: { stage: "connected", ...serverInfoPatch(outcome.serverInfo) },
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (ctx.existingRemote && outcome.kind !== "failed") {
|
|
205
|
+
// already have this (non-player) remote saved, and whoami
|
|
206
|
+
// already succeeds against it - nothing new to do. gracefully
|
|
207
|
+
// land on "complete" instead of re-running knock/auth (or
|
|
208
|
+
// erroring on a duplicate-peer_addr create) for something the
|
|
209
|
+
// user already has access to.
|
|
210
|
+
return {
|
|
211
|
+
ctx: {
|
|
212
|
+
...ctx,
|
|
213
|
+
step: "complete",
|
|
214
|
+
remote: ctx.existingRemote,
|
|
215
|
+
error: null,
|
|
216
|
+
progress: null,
|
|
217
|
+
},
|
|
218
|
+
effects: [
|
|
219
|
+
{ type: "DELETE_PENDING_BY_ADDR", peerAddr: addrKey(ctx) },
|
|
220
|
+
{ type: "SCHEDULE_TIMER", id: DISMISS_TIMER_ID, ms: COMPLETE_DISMISS_MS },
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
165
224
|
switch (outcome.kind) {
|
|
166
225
|
case "already_authed":
|
|
167
226
|
return {
|
|
@@ -224,7 +283,11 @@ export function transition(ctx, event) {
|
|
|
224
283
|
return noop({ ...ctx, error: "no peer address available" });
|
|
225
284
|
}
|
|
226
285
|
return {
|
|
227
|
-
ctx: {
|
|
286
|
+
ctx: {
|
|
287
|
+
...ctx,
|
|
288
|
+
knockDraft: { username: event.username, message: event.message },
|
|
289
|
+
error: null,
|
|
290
|
+
},
|
|
228
291
|
effects: [
|
|
229
292
|
{
|
|
230
293
|
type: "SEND_KNOCK",
|
|
@@ -442,7 +505,10 @@ export function transition(ctx, event) {
|
|
|
442
505
|
};
|
|
443
506
|
case "denied":
|
|
444
507
|
return {
|
|
445
|
-
ctx: {
|
|
508
|
+
ctx: {
|
|
509
|
+
...toUrlInput(ctx, "your access request was rejected by the server admin"),
|
|
510
|
+
retryPending: null,
|
|
511
|
+
},
|
|
446
512
|
effects: pending
|
|
447
513
|
? [
|
|
448
514
|
{
|
|
@@ -71,20 +71,36 @@ describe("SUBMIT_URL classification", () => {
|
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
describe("duplicate rejection", () => {
|
|
74
|
-
it("returns to url/input with a specific error
|
|
74
|
+
it("an http duplicate returns to url/input with a specific error", () => {
|
|
75
75
|
const { ctx, effects } = run([
|
|
76
|
-
{ type: "SUBMIT_URL", input:
|
|
77
|
-
{ type: "DUPLICATE_RESULT",
|
|
76
|
+
{ type: "SUBMIT_URL", input: "https://music.example.com" },
|
|
77
|
+
{ type: "DUPLICATE_RESULT", duplicate: { remote_id: "r0", name: "my server" } },
|
|
78
78
|
]);
|
|
79
79
|
expect(ctx.step).toBe("url");
|
|
80
80
|
expect(ctx.subStep).toBe("input");
|
|
81
81
|
expect(ctx.error).toContain('already added as "my server"');
|
|
82
82
|
expect(effects).toEqual([]);
|
|
83
83
|
});
|
|
84
|
+
it("a p2p duplicate does NOT block outright - it proceeds to a connection probe", () => {
|
|
85
|
+
const { ctx, effects } = run([
|
|
86
|
+
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
87
|
+
{
|
|
88
|
+
type: "DUPLICATE_RESULT",
|
|
89
|
+
duplicate: { remote_id: "r0", name: "old player", peer_addr: NODE_ID },
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
expect(ctx.step).toBe("testing");
|
|
93
|
+
expect(ctx.existingRemote).toEqual({ remote_id: "r0", name: "old player", peer_addr: NODE_ID });
|
|
94
|
+
expect(effectTypes(effects)).toEqual([
|
|
95
|
+
"UPSERT_PENDING",
|
|
96
|
+
"CLEAR_QUERY_PARAM",
|
|
97
|
+
"CHECK_CONNECTION",
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
84
100
|
it("persists the pending record (stage testing) BEFORE probing", () => {
|
|
85
101
|
const { effects } = run([
|
|
86
102
|
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
87
|
-
{ type: "DUPLICATE_RESULT",
|
|
103
|
+
{ type: "DUPLICATE_RESULT", duplicate: null },
|
|
88
104
|
]);
|
|
89
105
|
// order is load-bearing: persist, clear query param, then probe
|
|
90
106
|
expect(effectTypes(effects)).toEqual([
|
|
@@ -98,10 +114,69 @@ describe("duplicate rejection", () => {
|
|
|
98
114
|
expect(upsert.patch.error_message).toBeNull();
|
|
99
115
|
});
|
|
100
116
|
});
|
|
117
|
+
describe("re-scanning an already-saved p2p peer", () => {
|
|
118
|
+
const existingPlayer = {
|
|
119
|
+
remote_id: "r0",
|
|
120
|
+
name: "old player",
|
|
121
|
+
peer_addr: NODE_ID,
|
|
122
|
+
};
|
|
123
|
+
const toReprobe = [
|
|
124
|
+
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
125
|
+
{ type: "DUPLICATE_RESULT", duplicate: existingPlayer },
|
|
126
|
+
];
|
|
127
|
+
it("a live player_device probe re-opens the pairing ui, even though it's already saved", () => {
|
|
128
|
+
const { ctx } = run([
|
|
129
|
+
...toReprobe,
|
|
130
|
+
{
|
|
131
|
+
type: "CONNECTION_RESULT",
|
|
132
|
+
outcome: { kind: "needs_auth", serverInfo: { ...INFO, player_device: true } },
|
|
133
|
+
},
|
|
134
|
+
]);
|
|
135
|
+
expect(ctx.step).toBe("auth");
|
|
136
|
+
expect(ctx.serverInfo?.player_device).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
it("a live probe that ISN'T a player gracefully completes against the existing remote", () => {
|
|
139
|
+
const { ctx, effects } = run([
|
|
140
|
+
...toReprobe,
|
|
141
|
+
{ type: "CONNECTION_RESULT", outcome: { kind: "needs_auth", serverInfo: INFO } },
|
|
142
|
+
]);
|
|
143
|
+
expect(projectState(ctx)).toEqual({
|
|
144
|
+
step: "complete",
|
|
145
|
+
remote: existingPlayer,
|
|
146
|
+
alreadyExisted: true,
|
|
147
|
+
});
|
|
148
|
+
expect(effects).toEqual([
|
|
149
|
+
{ type: "DELETE_PENDING_BY_ADDR", peerAddr: NODE_ID },
|
|
150
|
+
{ type: "SCHEDULE_TIMER", id: DISMISS_TIMER_ID, ms: COMPLETE_DISMISS_MS },
|
|
151
|
+
]);
|
|
152
|
+
});
|
|
153
|
+
it("already_authed against an existing, non-player remote also gracefully completes", () => {
|
|
154
|
+
const { ctx } = run([
|
|
155
|
+
...toReprobe,
|
|
156
|
+
{ type: "CONNECTION_RESULT", outcome: { kind: "already_authed", serverInfo: INFO } },
|
|
157
|
+
]);
|
|
158
|
+
expect(ctx.step).toBe("complete");
|
|
159
|
+
});
|
|
160
|
+
it("already_authed against an existing PLAYER remote routes to auth (not complete, not create) - regression: whoami only proves base peer trust, not current player-session membership, so it can't be trusted to skip the pin/session-access check (queryPlayerPresence, done by the host ui in the auth step) the way it safely can for a plain remote", () => {
|
|
161
|
+
const { ctx, effects } = run([
|
|
162
|
+
...toReprobe,
|
|
163
|
+
{
|
|
164
|
+
type: "CONNECTION_RESULT",
|
|
165
|
+
outcome: { kind: "already_authed", serverInfo: { ...INFO, player_device: true } },
|
|
166
|
+
},
|
|
167
|
+
]);
|
|
168
|
+
expect(ctx.step).toBe("auth");
|
|
169
|
+
expect(ctx.serverInfo?.player_device).toBe(true);
|
|
170
|
+
expect(ctx.existingRemote).toEqual(existingPlayer);
|
|
171
|
+
const upsert = effects[0];
|
|
172
|
+
expect(upsert.type).toBe("UPSERT_PENDING");
|
|
173
|
+
expect(upsert.patch.stage).toBe("connected");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
101
176
|
describe("connection outcomes", () => {
|
|
102
177
|
const toProbe = [
|
|
103
178
|
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
104
|
-
{ type: "DUPLICATE_RESULT",
|
|
179
|
+
{ type: "DUPLICATE_RESULT", duplicate: null },
|
|
105
180
|
];
|
|
106
181
|
it("already_authed short-circuits to CREATE_REMOTE", () => {
|
|
107
182
|
const { ctx, effects } = run([
|
|
@@ -151,7 +226,7 @@ describe("connection outcomes", () => {
|
|
|
151
226
|
describe("knock flow", () => {
|
|
152
227
|
const toKnockForm = [
|
|
153
228
|
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
154
|
-
{ type: "DUPLICATE_RESULT",
|
|
229
|
+
{ type: "DUPLICATE_RESULT", duplicate: null },
|
|
155
230
|
{
|
|
156
231
|
type: "CONNECTION_RESULT",
|
|
157
232
|
outcome: { kind: "needs_knock", serverInfo: { ...INFO, knocking_enabled: true } },
|
|
@@ -250,12 +325,12 @@ describe("knock-status re-check (RETRY_PENDING resume)", () => {
|
|
|
250
325
|
describe("auth step", () => {
|
|
251
326
|
const toP2pAuth = [
|
|
252
327
|
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
253
|
-
{ type: "DUPLICATE_RESULT",
|
|
328
|
+
{ type: "DUPLICATE_RESULT", duplicate: null },
|
|
254
329
|
{ type: "CONNECTION_RESULT", outcome: { kind: "needs_auth", serverInfo: INFO } },
|
|
255
330
|
];
|
|
256
331
|
const toHttpAuth = [
|
|
257
332
|
{ type: "SUBMIT_URL", input: "https://music.example.com" },
|
|
258
|
-
{ type: "DUPLICATE_RESULT",
|
|
333
|
+
{ type: "DUPLICATE_RESULT", duplicate: null },
|
|
259
334
|
{ type: "CONNECTION_RESULT", outcome: { kind: "needs_auth", serverInfo: INFO } },
|
|
260
335
|
];
|
|
261
336
|
it("p2p login is an explicit error", () => {
|
|
@@ -284,7 +359,10 @@ describe("auth step", () => {
|
|
|
284
359
|
]);
|
|
285
360
|
});
|
|
286
361
|
it("http auth emits AUTH_HTTP", () => {
|
|
287
|
-
const { effects } = run([
|
|
362
|
+
const { effects } = run([
|
|
363
|
+
...toHttpAuth,
|
|
364
|
+
{ type: "SUBMIT_AUTH", mode: "login", username: "ed" },
|
|
365
|
+
]);
|
|
288
366
|
expect(effects).toEqual([
|
|
289
367
|
{
|
|
290
368
|
type: "AUTH_HTTP",
|
|
@@ -319,12 +397,12 @@ describe("completion", () => {
|
|
|
319
397
|
const remote = { remote_id: "r1", name: "test peer", peer_addr: NODE_ID };
|
|
320
398
|
const toCreate = [
|
|
321
399
|
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
322
|
-
{ type: "DUPLICATE_RESULT",
|
|
400
|
+
{ type: "DUPLICATE_RESULT", duplicate: null },
|
|
323
401
|
{ type: "CONNECTION_RESULT", outcome: { kind: "already_authed", serverInfo: INFO } },
|
|
324
402
|
];
|
|
325
403
|
it("REMOTE_CREATED enters complete, deletes the pending record, and schedules the dismiss timer", () => {
|
|
326
404
|
const { ctx, effects } = run([...toCreate, { type: "REMOTE_CREATED", ok: true, remote }]);
|
|
327
|
-
expect(projectState(ctx)).toEqual({ step: "complete", remote });
|
|
405
|
+
expect(projectState(ctx)).toEqual({ step: "complete", remote, alreadyExisted: false });
|
|
328
406
|
expect(effects).toEqual([
|
|
329
407
|
{ type: "DELETE_PENDING_BY_ADDR", peerAddr: NODE_ID },
|
|
330
408
|
{ type: "SCHEDULE_TIMER", id: DISMISS_TIMER_ID, ms: COMPLETE_DISMISS_MS },
|
|
@@ -353,7 +431,7 @@ describe("COMPLETE_PEER_ADDR external push", () => {
|
|
|
353
431
|
it("completes from mid-knock when the address matches the staged peer", () => {
|
|
354
432
|
const { ctx, effects } = run([
|
|
355
433
|
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
356
|
-
{ type: "DUPLICATE_RESULT",
|
|
434
|
+
{ type: "DUPLICATE_RESULT", duplicate: null },
|
|
357
435
|
{
|
|
358
436
|
type: "CONNECTION_RESULT",
|
|
359
437
|
outcome: { kind: "needs_knock", serverInfo: { ...INFO, knocking_enabled: true } },
|
|
@@ -393,7 +471,7 @@ describe("back / cancel / lifecycle", () => {
|
|
|
393
471
|
it("BACK from auth returns to url/input without effects", () => {
|
|
394
472
|
const { ctx, effects } = run([
|
|
395
473
|
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
396
|
-
{ type: "DUPLICATE_RESULT",
|
|
474
|
+
{ type: "DUPLICATE_RESULT", duplicate: null },
|
|
397
475
|
{ type: "CONNECTION_RESULT", outcome: { kind: "needs_auth", serverInfo: INFO } },
|
|
398
476
|
{ type: "BACK" },
|
|
399
477
|
]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/flows/add-peer/runner.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/flows/add-peer/runner.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,eAAe,EACf,YAAY,EAKb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,WAAW;IAC1B,KAAK,IAAI,YAAY,CAAC;IACtB;;iBAEa;IACb,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,aAAa,EAAE,CAAC;IAC3C;6DACyD;IACzD,SAAS,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,OAAO,IAAI,IAAI,CAAC;CACjB;AAuBD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CA4UpE"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// `testing` and by MODAL_CLOSE) bumps it, and a superseded run's outcome
|
|
15
15
|
// event is discarded instead of being fed to the machine.
|
|
16
16
|
import { parsePeerAddress } from "../../share/peer-addr.js";
|
|
17
|
-
import { initialContext, projectState, transition
|
|
17
|
+
import { initialContext, projectState, transition } from "./machine.js";
|
|
18
18
|
function describeConnectError(err) {
|
|
19
19
|
const msg = err instanceof Error ? err.message : String(err);
|
|
20
20
|
if (/timeout/i.test(msg)) {
|
|
@@ -72,10 +72,14 @@ export function createAddPeerFlow(deps) {
|
|
|
72
72
|
catch (err) {
|
|
73
73
|
// an auth rejection on the probe itself can still mean "knockable":
|
|
74
74
|
// the hello surface is public, so re-read it to route to the knock
|
|
75
|
-
// form instead of a dead-end error.
|
|
75
|
+
// form instead of a dead-end error. a player_device peer routes to
|
|
76
|
+
// auth instead (see the non-error path below for why).
|
|
76
77
|
if (isAuthRejection(err)) {
|
|
77
78
|
try {
|
|
78
79
|
const helloInfo = await deps.getServerInfo(target);
|
|
80
|
+
if (helloInfo?.player_device) {
|
|
81
|
+
return { kind: "needs_auth", serverInfo: helloInfo };
|
|
82
|
+
}
|
|
79
83
|
if (helloInfo?.knocking_enabled) {
|
|
80
84
|
return {
|
|
81
85
|
kind: "needs_knock",
|
|
@@ -105,6 +109,14 @@ export function createAddPeerFlow(deps) {
|
|
|
105
109
|
}
|
|
106
110
|
if (authed)
|
|
107
111
|
return { kind: "already_authed", serverInfo: info };
|
|
112
|
+
// a player_device peer (e.g. rathole) can ALSO have knocking enabled -
|
|
113
|
+
// it's genuinely both a real remote and a pairable player (see
|
|
114
|
+
// grimoire's server_info() doc comment) - route to "auth" either way,
|
|
115
|
+
// since that's the step that renders the player pairing ui (with its
|
|
116
|
+
// own "don't have an invite code? request access" knock fallback
|
|
117
|
+
// link when knocking_enabled), not the separate knock-only step.
|
|
118
|
+
if (info.player_device)
|
|
119
|
+
return { kind: "needs_auth", serverInfo: info };
|
|
108
120
|
if (info.knocking_enabled)
|
|
109
121
|
return { kind: "needs_knock", serverInfo: info };
|
|
110
122
|
return { kind: "needs_auth", serverInfo: info };
|
|
@@ -194,7 +206,7 @@ export function createAddPeerFlow(deps) {
|
|
|
194
206
|
const duplicate = target.type === "http"
|
|
195
207
|
? remotes.find((r) => r.base_url === target.url)
|
|
196
208
|
: remotes.find((r) => r.peer_addr === target.peerAddr);
|
|
197
|
-
await feed({ type: "DUPLICATE_RESULT",
|
|
209
|
+
await feed({ type: "DUPLICATE_RESULT", duplicate: duplicate ?? null });
|
|
198
210
|
return;
|
|
199
211
|
}
|
|
200
212
|
case "UPSERT_PENDING":
|
|
@@ -97,17 +97,58 @@ describe("createAddPeerFlow (bundled runner)", () => {
|
|
|
97
97
|
expect(onSuccess).toHaveBeenCalledWith(expect.objectContaining({ peer_addr: NODE_ID }));
|
|
98
98
|
expect(flow.state().step).toBe("url");
|
|
99
99
|
});
|
|
100
|
-
it("
|
|
100
|
+
it("an http duplicate is rejected before persisting anything", async () => {
|
|
101
101
|
const { deps, pendingStore } = makeDeps({
|
|
102
|
-
getAllRemotes: async () => [
|
|
102
|
+
getAllRemotes: async () => [
|
|
103
|
+
{ remote_id: "r0", name: "old hub", base_url: "https://music.example.com" },
|
|
104
|
+
],
|
|
103
105
|
});
|
|
104
106
|
const flow = createAddPeerFlow(deps);
|
|
105
|
-
await flow.dispatch({ type: "SUBMIT_URL", input:
|
|
107
|
+
await flow.dispatch({ type: "SUBMIT_URL", input: "https://music.example.com" });
|
|
106
108
|
const state = flow.state();
|
|
107
109
|
expect(state.step).toBe("url");
|
|
108
110
|
expect(state.step === "url" && state.error).toContain('already added as "old hub"');
|
|
109
111
|
expect(pendingStore.size).toBe(0);
|
|
110
112
|
});
|
|
113
|
+
it("a p2p duplicate that probes as a non-player gracefully completes against the existing remote", async () => {
|
|
114
|
+
const existing = { remote_id: "r0", name: "old player", peer_addr: NODE_ID };
|
|
115
|
+
const { deps, pendingStore } = makeDeps({
|
|
116
|
+
getAllRemotes: async () => [existing],
|
|
117
|
+
});
|
|
118
|
+
const flow = createAddPeerFlow(deps);
|
|
119
|
+
await flow.dispatch({ type: "SUBMIT_URL", input: NODE_ID });
|
|
120
|
+
const state = flow.state();
|
|
121
|
+
expect(state.step).toBe("complete");
|
|
122
|
+
expect(state.step === "complete" && state.remote).toEqual(existing);
|
|
123
|
+
// no new remote was created, no pending record left behind
|
|
124
|
+
expect(pendingStore.size).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
it("a p2p duplicate that probes as a player re-opens the pairing ui instead", async () => {
|
|
127
|
+
const existing = {
|
|
128
|
+
remote_id: "r0",
|
|
129
|
+
name: "old player",
|
|
130
|
+
peer_addr: NODE_ID,
|
|
131
|
+
};
|
|
132
|
+
const { deps } = makeDeps({
|
|
133
|
+
getAllRemotes: async () => [existing],
|
|
134
|
+
getServerInfo: async () => ({ ...INFO, player_device: true }),
|
|
135
|
+
});
|
|
136
|
+
const flow = createAddPeerFlow(deps);
|
|
137
|
+
await flow.dispatch({ type: "SUBMIT_URL", input: NODE_ID });
|
|
138
|
+
const state = flow.state();
|
|
139
|
+
expect(state.step).toBe("auth");
|
|
140
|
+
expect(state.step === "auth" && state.serverInfo?.player_device).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
it("routes to auth (not the knock form) for a player_device peer, even when knocking is also enabled", async () => {
|
|
143
|
+
const { deps } = makeDeps({
|
|
144
|
+
getServerInfo: async () => ({ ...INFO, player_device: true, knocking_enabled: true }),
|
|
145
|
+
});
|
|
146
|
+
const flow = createAddPeerFlow(deps);
|
|
147
|
+
await flow.dispatch({ type: "SUBMIT_URL", input: NODE_ID });
|
|
148
|
+
const state = flow.state();
|
|
149
|
+
expect(state.step).toBe("auth");
|
|
150
|
+
expect(state.step === "auth" && state.serverInfo?.player_device).toBe(true);
|
|
151
|
+
});
|
|
111
152
|
it("marks the pending record failed with a mapped error when the probe throws", async () => {
|
|
112
153
|
const { deps, pendingStore } = makeDeps({
|
|
113
154
|
getServerInfo: async () => {
|
|
@@ -38,7 +38,10 @@ export interface PeerServerInfo {
|
|
|
38
38
|
* remote. absent/false for every real grimoire-backed remote. */
|
|
39
39
|
player_device?: boolean | null;
|
|
40
40
|
}
|
|
41
|
-
/** a saved remote, as returned by the app's remote store.
|
|
41
|
+
/** a saved remote, as returned by the app's remote store. "is this a
|
|
42
|
+
* player" is deliberately NOT one of these fields - it's a live,
|
|
43
|
+
* point-in-time fact (see `ConnectionOutcome`'s `serverInfo.player_device`),
|
|
44
|
+
* never a persisted property of the remote itself. */
|
|
42
45
|
export interface SavedRemote {
|
|
43
46
|
remote_id: string;
|
|
44
47
|
name: string;
|
|
@@ -68,6 +71,7 @@ export type AddPeerState = {
|
|
|
68
71
|
} | {
|
|
69
72
|
step: "complete";
|
|
70
73
|
remote: SavedRemote;
|
|
74
|
+
alreadyExisted: boolean;
|
|
71
75
|
};
|
|
72
76
|
/** outcome of a connection probe (hello + whoami against the target). */
|
|
73
77
|
export type ConnectionOutcome = {
|
|
@@ -145,7 +149,7 @@ export type AddPeerEvent = {
|
|
|
145
149
|
records: PendingRemote[];
|
|
146
150
|
} | {
|
|
147
151
|
type: "DUPLICATE_RESULT";
|
|
148
|
-
|
|
152
|
+
duplicate: SavedRemote | null;
|
|
149
153
|
} | {
|
|
150
154
|
type: "CONNECTION_RESULT";
|
|
151
155
|
outcome: ConnectionOutcome;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/flows/add-peer/types.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAE3D,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B;qDACqD;AACrD,MAAM,MAAM,kBAAkB,GAC5B,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,eAAe,GAAG,gBAAgB,GAAG,gBAAgB,CAAC;AAE7F;wEACwE;AACxE,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,kBAAkB,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;8BAC8B;AAC9B,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,mBAAmB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACrC;;;sEAGkE;IAClE,aAAa,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAChC;AAED
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/flows/add-peer/types.ts"],"names":[],"mappings":"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"}
|