@freqhole/haruspex 0.0.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/dist/flows/add-peer/machine.d.ts +36 -0
- package/dist/flows/add-peer/machine.d.ts.map +1 -0
- package/dist/flows/add-peer/machine.js +500 -0
- package/dist/flows/add-peer/machine.test.d.ts +2 -0
- package/dist/flows/add-peer/machine.test.d.ts.map +1 -0
- package/dist/flows/add-peer/machine.test.js +421 -0
- package/dist/flows/add-peer/runner.d.ts +16 -0
- package/dist/flows/add-peer/runner.d.ts.map +1 -0
- package/dist/flows/add-peer/runner.js +349 -0
- package/dist/flows/add-peer/runner.test.d.ts +2 -0
- package/dist/flows/add-peer/runner.test.d.ts.map +1 -0
- package/dist/flows/add-peer/runner.test.js +205 -0
- package/dist/flows/add-peer/types.d.ts +271 -0
- package/dist/flows/add-peer/types.d.ts.map +1 -0
- package/dist/flows/add-peer/types.js +10 -0
- package/dist/flows/index.d.ts +6 -0
- package/dist/flows/index.d.ts.map +1 -0
- package/dist/flows/index.js +6 -0
- package/dist/identity/idbStore.d.ts +51 -0
- package/dist/identity/idbStore.d.ts.map +1 -0
- package/dist/identity/idbStore.js +183 -0
- package/dist/identity/idbStore.test.d.ts +2 -0
- package/dist/identity/idbStore.test.d.ts.map +1 -0
- package/dist/identity/idbStore.test.js +103 -0
- package/dist/identity/index.d.ts +8 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +3 -0
- package/dist/identity/resolve.d.ts +31 -0
- package/dist/identity/resolve.d.ts.map +1 -0
- package/dist/identity/resolve.js +37 -0
- package/dist/identity/resolve.test.d.ts +2 -0
- package/dist/identity/resolve.test.d.ts.map +1 -0
- package/dist/identity/resolve.test.js +105 -0
- package/dist/identity/types.d.ts +21 -0
- package/dist/identity/types.d.ts.map +1 -0
- package/dist/identity/types.js +1 -0
- package/dist/identity/webLocks.d.ts +22 -0
- package/dist/identity/webLocks.d.ts.map +1 -0
- package/dist/identity/webLocks.js +67 -0
- package/dist/identity/webLocks.test.d.ts +2 -0
- package/dist/identity/webLocks.test.d.ts.map +1 -0
- package/dist/identity/webLocks.test.js +125 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +7 -0
- package/dist/knock/index.d.ts +10 -0
- package/dist/knock/index.d.ts.map +1 -0
- package/dist/knock/index.js +5 -0
- package/dist/knock/pending-checker.d.ts +19 -0
- package/dist/knock/pending-checker.d.ts.map +1 -0
- package/dist/knock/pending-checker.js +27 -0
- package/dist/knock/pending-checker.test.d.ts +2 -0
- package/dist/knock/pending-checker.test.d.ts.map +1 -0
- package/dist/knock/pending-checker.test.js +93 -0
- package/dist/knock/requester.d.ts +26 -0
- package/dist/knock/requester.d.ts.map +1 -0
- package/dist/knock/requester.js +72 -0
- package/dist/knock/requester.test.d.ts +2 -0
- package/dist/knock/requester.test.d.ts.map +1 -0
- package/dist/knock/requester.test.js +108 -0
- package/dist/knock/responder.d.ts +13 -0
- package/dist/knock/responder.d.ts.map +1 -0
- package/dist/knock/responder.js +40 -0
- package/dist/knock/responder.test.d.ts +2 -0
- package/dist/knock/responder.test.d.ts.map +1 -0
- package/dist/knock/responder.test.js +89 -0
- package/dist/knock/store.d.ts +11 -0
- package/dist/knock/store.d.ts.map +1 -0
- package/dist/knock/store.js +211 -0
- package/dist/knock/store.test.d.ts +2 -0
- package/dist/knock/store.test.d.ts.map +1 -0
- package/dist/knock/store.test.js +195 -0
- package/dist/knock/types.d.ts +144 -0
- package/dist/knock/types.d.ts.map +1 -0
- package/dist/knock/types.js +26 -0
- package/dist/permissions/access.d.ts +54 -0
- package/dist/permissions/access.d.ts.map +1 -0
- package/dist/permissions/access.js +56 -0
- package/dist/permissions/access.test.d.ts +2 -0
- package/dist/permissions/access.test.d.ts.map +1 -0
- package/dist/permissions/access.test.js +72 -0
- package/dist/permissions/index.d.ts +5 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +2 -0
- package/dist/permissions/role-table.d.ts +11 -0
- package/dist/permissions/role-table.d.ts.map +1 -0
- package/dist/permissions/role-table.js +16 -0
- package/dist/protocol/client.d.ts +67 -0
- package/dist/protocol/client.d.ts.map +1 -0
- package/dist/protocol/client.js +195 -0
- package/dist/protocol/client.test.d.ts +2 -0
- package/dist/protocol/client.test.d.ts.map +1 -0
- package/dist/protocol/client.test.js +282 -0
- package/dist/protocol/codec.d.ts +45 -0
- package/dist/protocol/codec.d.ts.map +1 -0
- package/dist/protocol/codec.js +111 -0
- package/dist/protocol/codec.test.d.ts +2 -0
- package/dist/protocol/codec.test.d.ts.map +1 -0
- package/dist/protocol/codec.test.js +93 -0
- package/dist/protocol/extensions.d.ts +33 -0
- package/dist/protocol/extensions.d.ts.map +1 -0
- package/dist/protocol/extensions.js +36 -0
- package/dist/protocol/extensions.test.d.ts +2 -0
- package/dist/protocol/extensions.test.d.ts.map +1 -0
- package/dist/protocol/extensions.test.js +65 -0
- package/dist/protocol/fixtures.test.d.ts +2 -0
- package/dist/protocol/fixtures.test.d.ts.map +1 -0
- package/dist/protocol/fixtures.test.js +51 -0
- package/dist/protocol/index.d.ts +9 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +4 -0
- package/dist/protocol/messages.d.ts +480 -0
- package/dist/protocol/messages.d.ts.map +1 -0
- package/dist/protocol/messages.js +255 -0
- package/dist/protocol/messages.test.d.ts +2 -0
- package/dist/protocol/messages.test.d.ts.map +1 -0
- package/dist/protocol/messages.test.js +75 -0
- package/dist/share/codec.d.ts +58 -0
- package/dist/share/codec.d.ts.map +1 -0
- package/dist/share/codec.js +222 -0
- package/dist/share/codec.test.d.ts +2 -0
- package/dist/share/codec.test.d.ts.map +1 -0
- package/dist/share/codec.test.js +150 -0
- package/dist/share/detect.d.ts +24 -0
- package/dist/share/detect.d.ts.map +1 -0
- package/dist/share/detect.js +25 -0
- package/dist/share/detect.test.d.ts +2 -0
- package/dist/share/detect.test.d.ts.map +1 -0
- package/dist/share/detect.test.js +31 -0
- package/dist/share/index.d.ts +7 -0
- package/dist/share/index.d.ts.map +1 -0
- package/dist/share/index.js +3 -0
- package/dist/share/peer-addr.d.ts +32 -0
- package/dist/share/peer-addr.d.ts.map +1 -0
- package/dist/share/peer-addr.js +72 -0
- package/dist/share/peer-addr.test.d.ts +2 -0
- package/dist/share/peer-addr.test.d.ts.map +1 -0
- package/dist/share/peer-addr.test.js +58 -0
- package/dist/solid/auth-form.d.ts +43 -0
- package/dist/solid/auth-form.d.ts.map +1 -0
- package/dist/solid/auth-form.js +55 -0
- package/dist/solid/auth-form.test.d.ts +2 -0
- package/dist/solid/auth-form.test.d.ts.map +1 -0
- package/dist/solid/auth-form.test.js +131 -0
- package/dist/solid/auth-status.d.ts +21 -0
- package/dist/solid/auth-status.d.ts.map +1 -0
- package/dist/solid/auth-status.js +58 -0
- package/dist/solid/auth-status.test.d.ts +2 -0
- package/dist/solid/auth-status.test.d.ts.map +1 -0
- package/dist/solid/auth-status.test.js +60 -0
- package/dist/solid/index.d.ts +5 -0
- package/dist/solid/index.d.ts.map +1 -0
- package/dist/solid/index.js +10 -0
- package/dist/solid/knock-inbox.d.ts +61 -0
- package/dist/solid/knock-inbox.d.ts.map +1 -0
- package/dist/solid/knock-inbox.js +123 -0
- package/dist/solid/knock-inbox.test.d.ts +2 -0
- package/dist/solid/knock-inbox.test.d.ts.map +1 -0
- package/dist/solid/knock-inbox.test.js +153 -0
- package/dist/solid/reauth-flow.d.ts +26 -0
- package/dist/solid/reauth-flow.d.ts.map +1 -0
- package/dist/solid/reauth-flow.js +34 -0
- package/dist/solid/reauth-flow.test.d.ts +2 -0
- package/dist/solid/reauth-flow.test.d.ts.map +1 -0
- package/dist/solid/reauth-flow.test.js +91 -0
- package/dist/state/endpoint-control.d.ts +22 -0
- package/dist/state/endpoint-control.d.ts.map +1 -0
- package/dist/state/endpoint-control.js +34 -0
- package/dist/state/endpoint-control.test.d.ts +2 -0
- package/dist/state/endpoint-control.test.d.ts.map +1 -0
- package/dist/state/endpoint-control.test.js +63 -0
- package/dist/state/friend-directory.d.ts +37 -0
- package/dist/state/friend-directory.d.ts.map +1 -0
- package/dist/state/friend-directory.js +44 -0
- package/dist/state/friend-directory.test.d.ts +2 -0
- package/dist/state/friend-directory.test.d.ts.map +1 -0
- package/dist/state/friend-directory.test.js +63 -0
- package/dist/state/index.d.ts +6 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +3 -0
- package/dist/state/peer-names.d.ts +7 -0
- package/dist/state/peer-names.d.ts.map +1 -0
- package/dist/state/peer-names.js +21 -0
- package/dist/state/peer-names.test.d.ts +2 -0
- package/dist/state/peer-names.test.d.ts.map +1 -0
- package/dist/state/peer-names.test.js +29 -0
- package/dist/testing/idb-harness.d.ts +9 -0
- package/dist/testing/idb-harness.d.ts.map +1 -0
- package/dist/testing/idb-harness.js +19 -0
- package/dist/testing/idb-harness.test.d.ts +2 -0
- package/dist/testing/idb-harness.test.d.ts.map +1 -0
- package/dist/testing/idb-harness.test.js +18 -0
- package/dist/testing/identity-fixtures.d.ts +10 -0
- package/dist/testing/identity-fixtures.d.ts.map +1 -0
- package/dist/testing/identity-fixtures.js +21 -0
- package/dist/testing/identity-fixtures.test.d.ts +2 -0
- package/dist/testing/identity-fixtures.test.d.ts.map +1 -0
- package/dist/testing/identity-fixtures.test.js +28 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/knock-fixtures.d.ts +13 -0
- package/dist/testing/knock-fixtures.d.ts.map +1 -0
- package/dist/testing/knock-fixtures.js +45 -0
- package/dist/testing/knock-fixtures.test.d.ts +2 -0
- package/dist/testing/knock-fixtures.test.d.ts.map +1 -0
- package/dist/testing/knock-fixtures.test.js +65 -0
- package/dist/testing/protocol-fixtures.d.ts +7 -0
- package/dist/testing/protocol-fixtures.d.ts.map +1 -0
- package/dist/testing/protocol-fixtures.js +28 -0
- package/dist/testing/protocol-fixtures.test.d.ts +2 -0
- package/dist/testing/protocol-fixtures.test.d.ts.map +1 -0
- package/dist/testing/protocol-fixtures.test.js +19 -0
- package/dist/testing/state-fixtures.d.ts +33 -0
- package/dist/testing/state-fixtures.d.ts.map +1 -0
- package/dist/testing/state-fixtures.js +67 -0
- package/dist/testing/state-fixtures.test.d.ts +2 -0
- package/dist/testing/state-fixtures.test.d.ts.map +1 -0
- package/dist/testing/state-fixtures.test.js +68 -0
- package/dist/testing/webauthn-mocks.d.ts +27 -0
- package/dist/testing/webauthn-mocks.d.ts.map +1 -0
- package/dist/testing/webauthn-mocks.js +78 -0
- package/dist/testing/webauthn-mocks.test.d.ts +2 -0
- package/dist/testing/webauthn-mocks.test.d.ts.map +1 -0
- package/dist/testing/webauthn-mocks.test.js +40 -0
- package/dist/webauthn/ceremony.d.ts +76 -0
- package/dist/webauthn/ceremony.d.ts.map +1 -0
- package/dist/webauthn/ceremony.js +136 -0
- package/dist/webauthn/ceremony.test.d.ts +2 -0
- package/dist/webauthn/ceremony.test.d.ts.map +1 -0
- package/dist/webauthn/ceremony.test.js +245 -0
- package/dist/webauthn/codec.d.ts +39 -0
- package/dist/webauthn/codec.d.ts.map +1 -0
- package/dist/webauthn/codec.js +118 -0
- package/dist/webauthn/codec.test.d.ts +2 -0
- package/dist/webauthn/codec.test.d.ts.map +1 -0
- package/dist/webauthn/codec.test.js +126 -0
- package/dist/webauthn/index.d.ts +7 -0
- package/dist/webauthn/index.d.ts.map +1 -0
- package/dist/webauthn/index.js +3 -0
- package/dist/webauthn/transport.d.ts +58 -0
- package/dist/webauthn/transport.d.ts.map +1 -0
- package/dist/webauthn/transport.js +6 -0
- package/dist/webauthn/types.d.ts +78 -0
- package/dist/webauthn/types.d.ts.map +1 -0
- package/dist/webauthn/types.js +29 -0
- package/package.json +83 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { COMPLETE_DISMISS_MS, DISMISS_TIMER_ID, initialContext, projectState, transition, } from "./machine.js";
|
|
3
|
+
const NODE_ID = "a".repeat(64);
|
|
4
|
+
const INFO = {
|
|
5
|
+
name: "test peer",
|
|
6
|
+
description: null,
|
|
7
|
+
version: "1.0",
|
|
8
|
+
requires_auth: true,
|
|
9
|
+
knocking_enabled: false,
|
|
10
|
+
};
|
|
11
|
+
function pendingRecord(overrides = {}) {
|
|
12
|
+
return {
|
|
13
|
+
id: "pending-1",
|
|
14
|
+
peer_addr: NODE_ID,
|
|
15
|
+
transport: "wasm",
|
|
16
|
+
stage: "knock_pending",
|
|
17
|
+
server_name: "test peer",
|
|
18
|
+
server_description: null,
|
|
19
|
+
server_version: "1.0",
|
|
20
|
+
server_image_data: null,
|
|
21
|
+
server_image_type: null,
|
|
22
|
+
knock_username: "ed",
|
|
23
|
+
knock_message: "hi",
|
|
24
|
+
error_message: null,
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** run a sequence of events, returning the final ctx and the effects of
|
|
29
|
+
* the LAST event. */
|
|
30
|
+
function run(events, start) {
|
|
31
|
+
let ctx = start ?? initialContext();
|
|
32
|
+
let effects = [];
|
|
33
|
+
for (const event of events) {
|
|
34
|
+
const result = transition(ctx, event);
|
|
35
|
+
ctx = result.ctx;
|
|
36
|
+
effects = result.effects;
|
|
37
|
+
}
|
|
38
|
+
return { ctx, effects };
|
|
39
|
+
}
|
|
40
|
+
function effectTypes(effects) {
|
|
41
|
+
return effects.map((e) => e.type);
|
|
42
|
+
}
|
|
43
|
+
describe("SUBMIT_URL classification", () => {
|
|
44
|
+
it("rejects empty input with an error, no effects", () => {
|
|
45
|
+
const { ctx, effects } = run([{ type: "SUBMIT_URL", input: " " }]);
|
|
46
|
+
expect(ctx.step).toBe("url");
|
|
47
|
+
expect(ctx.error).toMatch(/please enter/);
|
|
48
|
+
expect(effects).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
it("classifies a 64-hex node id as p2p and starts the duplicate check", () => {
|
|
51
|
+
const { ctx, effects } = run([{ type: "SUBMIT_URL", input: NODE_ID }]);
|
|
52
|
+
expect(ctx.step).toBe("testing");
|
|
53
|
+
expect(ctx.peerAddr).toBe(NODE_ID);
|
|
54
|
+
expect(ctx.url).toBe("");
|
|
55
|
+
expect(effects).toEqual([
|
|
56
|
+
{ type: "CHECK_DUPLICATE", target: { type: "p2p", peerAddr: NODE_ID } },
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
it("classifies a hostname as http, normalizing scheme + trailing slash", () => {
|
|
60
|
+
const { ctx, effects } = run([{ type: "SUBMIT_URL", input: "music.example.com/" }]);
|
|
61
|
+
expect(ctx.step).toBe("testing");
|
|
62
|
+
expect(ctx.url).toBe("https://music.example.com");
|
|
63
|
+
expect(ctx.peerAddr).toBeNull();
|
|
64
|
+
expect(effectTypes(effects)).toEqual(["CHECK_DUPLICATE"]);
|
|
65
|
+
});
|
|
66
|
+
it("rejects an unparseable url in place", () => {
|
|
67
|
+
const { ctx, effects } = run([{ type: "SUBMIT_URL", input: "http://" }]);
|
|
68
|
+
expect(ctx.step).toBe("url");
|
|
69
|
+
expect(ctx.error).toMatch(/valid url/);
|
|
70
|
+
expect(effects).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe("duplicate rejection", () => {
|
|
74
|
+
it("returns to url/input with a specific error on a duplicate", () => {
|
|
75
|
+
const { ctx, effects } = run([
|
|
76
|
+
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
77
|
+
{ type: "DUPLICATE_RESULT", duplicateName: "my server" },
|
|
78
|
+
]);
|
|
79
|
+
expect(ctx.step).toBe("url");
|
|
80
|
+
expect(ctx.subStep).toBe("input");
|
|
81
|
+
expect(ctx.error).toContain('already added as "my server"');
|
|
82
|
+
expect(effects).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
it("persists the pending record (stage testing) BEFORE probing", () => {
|
|
85
|
+
const { effects } = run([
|
|
86
|
+
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
87
|
+
{ type: "DUPLICATE_RESULT", duplicateName: null },
|
|
88
|
+
]);
|
|
89
|
+
// order is load-bearing: persist, clear query param, then probe
|
|
90
|
+
expect(effectTypes(effects)).toEqual([
|
|
91
|
+
"UPSERT_PENDING",
|
|
92
|
+
"CLEAR_QUERY_PARAM",
|
|
93
|
+
"CHECK_CONNECTION",
|
|
94
|
+
]);
|
|
95
|
+
const upsert = effects[0];
|
|
96
|
+
expect(upsert.peerAddr).toBe(NODE_ID);
|
|
97
|
+
expect(upsert.patch.stage).toBe("testing");
|
|
98
|
+
expect(upsert.patch.error_message).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe("connection outcomes", () => {
|
|
102
|
+
const toProbe = [
|
|
103
|
+
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
104
|
+
{ type: "DUPLICATE_RESULT", duplicateName: null },
|
|
105
|
+
];
|
|
106
|
+
it("already_authed short-circuits to CREATE_REMOTE", () => {
|
|
107
|
+
const { ctx, effects } = run([
|
|
108
|
+
...toProbe,
|
|
109
|
+
{ type: "CONNECTION_RESULT", outcome: { kind: "already_authed", serverInfo: INFO } },
|
|
110
|
+
]);
|
|
111
|
+
expect(ctx.step).toBe("testing");
|
|
112
|
+
expect(effects).toEqual([{ type: "CREATE_REMOTE", peerAddr: NODE_ID, url: "" }]);
|
|
113
|
+
});
|
|
114
|
+
it("needs_knock routes to url/knock_form and persists stage connected", () => {
|
|
115
|
+
const { ctx, effects } = run([
|
|
116
|
+
...toProbe,
|
|
117
|
+
{
|
|
118
|
+
type: "CONNECTION_RESULT",
|
|
119
|
+
outcome: { kind: "needs_knock", serverInfo: { ...INFO, knocking_enabled: true } },
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
expect(ctx.step).toBe("url");
|
|
123
|
+
expect(ctx.subStep).toBe("knock_form");
|
|
124
|
+
expect(ctx.serverInfo?.name).toBe("test peer");
|
|
125
|
+
const upsert = effects[0];
|
|
126
|
+
expect(upsert.patch.stage).toBe("connected");
|
|
127
|
+
expect(upsert.patch.server_name).toBe("test peer");
|
|
128
|
+
});
|
|
129
|
+
it("needs_auth routes to auth and persists stage connected", () => {
|
|
130
|
+
const { ctx, effects } = run([
|
|
131
|
+
...toProbe,
|
|
132
|
+
{ type: "CONNECTION_RESULT", outcome: { kind: "needs_auth", serverInfo: INFO } },
|
|
133
|
+
]);
|
|
134
|
+
expect(ctx.step).toBe("auth");
|
|
135
|
+
const upsert = effects[0];
|
|
136
|
+
expect(upsert.patch.stage).toBe("connected");
|
|
137
|
+
});
|
|
138
|
+
it("failed persists stage failed with the error and returns to url/input", () => {
|
|
139
|
+
const { ctx, effects } = run([
|
|
140
|
+
...toProbe,
|
|
141
|
+
{ type: "CONNECTION_RESULT", outcome: { kind: "failed", error: "peer offline" } },
|
|
142
|
+
]);
|
|
143
|
+
expect(ctx.step).toBe("url");
|
|
144
|
+
expect(ctx.subStep).toBe("input");
|
|
145
|
+
expect(ctx.error).toBe("peer offline");
|
|
146
|
+
const upsert = effects[0];
|
|
147
|
+
expect(upsert.patch.stage).toBe("failed");
|
|
148
|
+
expect(upsert.patch.error_message).toBe("peer offline");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe("knock flow", () => {
|
|
152
|
+
const toKnockForm = [
|
|
153
|
+
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
154
|
+
{ type: "DUPLICATE_RESULT", duplicateName: null },
|
|
155
|
+
{
|
|
156
|
+
type: "CONNECTION_RESULT",
|
|
157
|
+
outcome: { kind: "needs_knock", serverInfo: { ...INFO, knocking_enabled: true } },
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
it("SUBMIT_KNOCK emits SEND_KNOCK with the form values", () => {
|
|
161
|
+
const { effects } = run([
|
|
162
|
+
...toKnockForm,
|
|
163
|
+
{ type: "SUBMIT_KNOCK", username: "ed", message: "let me in" },
|
|
164
|
+
]);
|
|
165
|
+
expect(effects).toEqual([
|
|
166
|
+
{ type: "SEND_KNOCK", peerAddr: NODE_ID, username: "ed", message: "let me in" },
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
it("a successful knock persists stage knock_pending with username/message and enters knock_sent", () => {
|
|
170
|
+
const { ctx, effects } = run([
|
|
171
|
+
...toKnockForm,
|
|
172
|
+
{ type: "SUBMIT_KNOCK", username: "ed", message: "let me in" },
|
|
173
|
+
{ type: "KNOCK_SENT_RESULT", ok: true },
|
|
174
|
+
]);
|
|
175
|
+
expect(ctx.step).toBe("knock_sent");
|
|
176
|
+
const upsert = effects[0];
|
|
177
|
+
expect(upsert.patch.stage).toBe("knock_pending");
|
|
178
|
+
expect(upsert.patch.knock_username).toBe("ed");
|
|
179
|
+
expect(upsert.patch.knock_message).toBe("let me in");
|
|
180
|
+
});
|
|
181
|
+
it("a failed knock stays on the form with the error", () => {
|
|
182
|
+
const { ctx, effects } = run([
|
|
183
|
+
...toKnockForm,
|
|
184
|
+
{ type: "SUBMIT_KNOCK", username: "ed", message: "hi" },
|
|
185
|
+
{ type: "KNOCK_SENT_RESULT", ok: false, error: "knock request failed" },
|
|
186
|
+
]);
|
|
187
|
+
expect(ctx.step).toBe("url");
|
|
188
|
+
expect(ctx.subStep).toBe("knock_form");
|
|
189
|
+
expect(ctx.error).toBe("knock request failed");
|
|
190
|
+
expect(effects).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
it("USE_INVITE_CODE escapes the knock form to auth", () => {
|
|
193
|
+
const { ctx } = run([...toKnockForm, { type: "USE_INVITE_CODE" }]);
|
|
194
|
+
expect(ctx.step).toBe("auth");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
describe("knock-status re-check (RETRY_PENDING resume)", () => {
|
|
198
|
+
const pending = pendingRecord();
|
|
199
|
+
it("a knock-stage pending record triggers CHECK_KNOCK_STATUS", () => {
|
|
200
|
+
const { ctx, effects } = run([{ type: "RETRY_PENDING", pending }]);
|
|
201
|
+
expect(ctx.step).toBe("testing");
|
|
202
|
+
expect(ctx.peerAddr).toBe(NODE_ID);
|
|
203
|
+
expect(effects).toEqual([{ type: "CHECK_KNOCK_STATUS", pending }]);
|
|
204
|
+
});
|
|
205
|
+
it("a non-knock pending record re-runs the normal connection flow", () => {
|
|
206
|
+
const { ctx, effects } = run([
|
|
207
|
+
{ type: "RETRY_PENDING", pending: pendingRecord({ stage: "failed" }) },
|
|
208
|
+
]);
|
|
209
|
+
expect(ctx.step).toBe("testing");
|
|
210
|
+
expect(effectTypes(effects)).toEqual(["CHECK_DUPLICATE"]);
|
|
211
|
+
});
|
|
212
|
+
it("accepted + authed deletes the pending record and creates the remote", () => {
|
|
213
|
+
const { effects } = run([
|
|
214
|
+
{ type: "RETRY_PENDING", pending },
|
|
215
|
+
{ type: "KNOCK_STATUS_RESULT", outcome: { kind: "accepted_authed", serverInfo: INFO } },
|
|
216
|
+
]);
|
|
217
|
+
expect(effects).toEqual([
|
|
218
|
+
{ type: "DELETE_PENDING", id: "pending-1" },
|
|
219
|
+
{ type: "CREATE_REMOTE", peerAddr: NODE_ID, url: "" },
|
|
220
|
+
]);
|
|
221
|
+
});
|
|
222
|
+
it("accepted + not authed deletes the pending record and routes to auth", () => {
|
|
223
|
+
const { ctx, effects } = run([
|
|
224
|
+
{ type: "RETRY_PENDING", pending },
|
|
225
|
+
{ type: "KNOCK_STATUS_RESULT", outcome: { kind: "accepted_needs_auth", serverInfo: INFO } },
|
|
226
|
+
]);
|
|
227
|
+
expect(ctx.step).toBe("auth");
|
|
228
|
+
expect(effects).toEqual([{ type: "DELETE_PENDING", id: "pending-1" }]);
|
|
229
|
+
});
|
|
230
|
+
it("denied persists stage knock_rejected and returns to url/input with the error", () => {
|
|
231
|
+
const { ctx, effects } = run([
|
|
232
|
+
{ type: "RETRY_PENDING", pending },
|
|
233
|
+
{ type: "KNOCK_STATUS_RESULT", outcome: { kind: "denied" } },
|
|
234
|
+
]);
|
|
235
|
+
expect(ctx.step).toBe("url");
|
|
236
|
+
expect(ctx.error).toMatch(/rejected/);
|
|
237
|
+
const upsert = effects[0];
|
|
238
|
+
expect(upsert.patch.stage).toBe("knock_rejected");
|
|
239
|
+
});
|
|
240
|
+
it("still-pending returns to url/input with a status message, no effects", () => {
|
|
241
|
+
const { ctx, effects } = run([
|
|
242
|
+
{ type: "RETRY_PENDING", pending },
|
|
243
|
+
{ type: "KNOCK_STATUS_RESULT", outcome: { kind: "pending" } },
|
|
244
|
+
]);
|
|
245
|
+
expect(ctx.step).toBe("url");
|
|
246
|
+
expect(ctx.error).toMatch(/still pending/);
|
|
247
|
+
expect(effects).toEqual([]);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
describe("auth step", () => {
|
|
251
|
+
const toP2pAuth = [
|
|
252
|
+
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
253
|
+
{ type: "DUPLICATE_RESULT", duplicateName: null },
|
|
254
|
+
{ type: "CONNECTION_RESULT", outcome: { kind: "needs_auth", serverInfo: INFO } },
|
|
255
|
+
];
|
|
256
|
+
const toHttpAuth = [
|
|
257
|
+
{ type: "SUBMIT_URL", input: "https://music.example.com" },
|
|
258
|
+
{ type: "DUPLICATE_RESULT", duplicateName: null },
|
|
259
|
+
{ type: "CONNECTION_RESULT", outcome: { kind: "needs_auth", serverInfo: INFO } },
|
|
260
|
+
];
|
|
261
|
+
it("p2p login is an explicit error", () => {
|
|
262
|
+
const { ctx, effects } = run([
|
|
263
|
+
...toP2pAuth,
|
|
264
|
+
{ type: "SUBMIT_AUTH", mode: "login", username: "ed" },
|
|
265
|
+
]);
|
|
266
|
+
expect(ctx.error).toMatch(/login not yet supported for p2p/);
|
|
267
|
+
expect(effects).toEqual([]);
|
|
268
|
+
});
|
|
269
|
+
it("p2p register without an invite code is an error", () => {
|
|
270
|
+
const { ctx, effects } = run([
|
|
271
|
+
...toP2pAuth,
|
|
272
|
+
{ type: "SUBMIT_AUTH", mode: "register", username: "ed" },
|
|
273
|
+
]);
|
|
274
|
+
expect(ctx.error).toMatch(/invite code required/);
|
|
275
|
+
expect(effects).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
it("p2p register with an invite code emits AUTH_REDEEM_INVITE", () => {
|
|
278
|
+
const { effects } = run([
|
|
279
|
+
...toP2pAuth,
|
|
280
|
+
{ type: "SUBMIT_AUTH", mode: "register", username: "ed", inviteCode: "abc" },
|
|
281
|
+
]);
|
|
282
|
+
expect(effects).toEqual([
|
|
283
|
+
{ type: "AUTH_REDEEM_INVITE", peerAddr: NODE_ID, username: "ed", inviteCode: "abc" },
|
|
284
|
+
]);
|
|
285
|
+
});
|
|
286
|
+
it("http auth emits AUTH_HTTP", () => {
|
|
287
|
+
const { effects } = run([...toHttpAuth, { type: "SUBMIT_AUTH", mode: "login", username: "ed" }]);
|
|
288
|
+
expect(effects).toEqual([
|
|
289
|
+
{
|
|
290
|
+
type: "AUTH_HTTP",
|
|
291
|
+
url: "https://music.example.com",
|
|
292
|
+
mode: "login",
|
|
293
|
+
username: "ed",
|
|
294
|
+
inviteCode: undefined,
|
|
295
|
+
},
|
|
296
|
+
]);
|
|
297
|
+
});
|
|
298
|
+
it("p2p passkey register requires an invite code", () => {
|
|
299
|
+
const { ctx } = run([...toP2pAuth, { type: "PASSKEY_AUTH", mode: "register", username: "ed" }]);
|
|
300
|
+
expect(ctx.error).toMatch(/invite code is required/);
|
|
301
|
+
});
|
|
302
|
+
it("auth success emits CREATE_REMOTE; failure stays with the error", () => {
|
|
303
|
+
const ok = run([
|
|
304
|
+
...toP2pAuth,
|
|
305
|
+
{ type: "SUBMIT_AUTH", mode: "register", username: "ed", inviteCode: "abc" },
|
|
306
|
+
{ type: "AUTH_RESULT", ok: true },
|
|
307
|
+
]);
|
|
308
|
+
expect(ok.effects).toEqual([{ type: "CREATE_REMOTE", peerAddr: NODE_ID, url: "" }]);
|
|
309
|
+
const fail = run([
|
|
310
|
+
...toP2pAuth,
|
|
311
|
+
{ type: "SUBMIT_AUTH", mode: "register", username: "ed", inviteCode: "abc" },
|
|
312
|
+
{ type: "AUTH_RESULT", ok: false, error: "bad invite" },
|
|
313
|
+
]);
|
|
314
|
+
expect(fail.ctx.step).toBe("auth");
|
|
315
|
+
expect(fail.ctx.error).toBe("bad invite");
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
describe("completion", () => {
|
|
319
|
+
const remote = { remote_id: "r1", name: "test peer", peer_addr: NODE_ID };
|
|
320
|
+
const toCreate = [
|
|
321
|
+
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
322
|
+
{ type: "DUPLICATE_RESULT", duplicateName: null },
|
|
323
|
+
{ type: "CONNECTION_RESULT", outcome: { kind: "already_authed", serverInfo: INFO } },
|
|
324
|
+
];
|
|
325
|
+
it("REMOTE_CREATED enters complete, deletes the pending record, and schedules the dismiss timer", () => {
|
|
326
|
+
const { ctx, effects } = run([...toCreate, { type: "REMOTE_CREATED", ok: true, remote }]);
|
|
327
|
+
expect(projectState(ctx)).toEqual({ step: "complete", remote });
|
|
328
|
+
expect(effects).toEqual([
|
|
329
|
+
{ type: "DELETE_PENDING_BY_ADDR", peerAddr: NODE_ID },
|
|
330
|
+
{ type: "SCHEDULE_TIMER", id: DISMISS_TIMER_ID, ms: COMPLETE_DISMISS_MS },
|
|
331
|
+
]);
|
|
332
|
+
});
|
|
333
|
+
it("a duplicate-remote creation failure returns to url/input with the error", () => {
|
|
334
|
+
const { ctx } = run([
|
|
335
|
+
...toCreate,
|
|
336
|
+
{ type: "REMOTE_CREATED", ok: false, error: "remote already exists" },
|
|
337
|
+
]);
|
|
338
|
+
expect(ctx.step).toBe("url");
|
|
339
|
+
expect(ctx.subStep).toBe("input");
|
|
340
|
+
expect(ctx.error).toBe("remote already exists");
|
|
341
|
+
});
|
|
342
|
+
it("the dismiss timer resets the machine and fires close + success", () => {
|
|
343
|
+
const { ctx, effects } = run([
|
|
344
|
+
...toCreate,
|
|
345
|
+
{ type: "REMOTE_CREATED", ok: true, remote },
|
|
346
|
+
{ type: "TIMER_FIRED", id: DISMISS_TIMER_ID },
|
|
347
|
+
]);
|
|
348
|
+
expect(ctx.step).toBe("url");
|
|
349
|
+
expect(effects).toEqual([{ type: "CALL_ON_CLOSE" }, { type: "CALL_ON_SUCCESS", remote }]);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
describe("COMPLETE_PEER_ADDR external push", () => {
|
|
353
|
+
it("completes from mid-knock when the address matches the staged peer", () => {
|
|
354
|
+
const { ctx, effects } = run([
|
|
355
|
+
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
356
|
+
{ type: "DUPLICATE_RESULT", duplicateName: null },
|
|
357
|
+
{
|
|
358
|
+
type: "CONNECTION_RESULT",
|
|
359
|
+
outcome: { kind: "needs_knock", serverInfo: { ...INFO, knocking_enabled: true } },
|
|
360
|
+
},
|
|
361
|
+
{ type: "SUBMIT_KNOCK", username: "ed", message: "hi" },
|
|
362
|
+
{ type: "KNOCK_SENT_RESULT", ok: true },
|
|
363
|
+
// now in knock_sent; the acceptance push arrives over p2p
|
|
364
|
+
{ type: "COMPLETE_PEER_ADDR", peerAddr: NODE_ID },
|
|
365
|
+
]);
|
|
366
|
+
expect(ctx.step).toBe("testing");
|
|
367
|
+
expect(effects).toEqual([{ type: "CREATE_REMOTE", peerAddr: NODE_ID, url: "" }]);
|
|
368
|
+
});
|
|
369
|
+
it("matches a loaded pending record even when nothing is staged", () => {
|
|
370
|
+
const { ctx, effects } = run([
|
|
371
|
+
{ type: "MODAL_OPEN" },
|
|
372
|
+
{ type: "PENDING_LOADED", records: [pendingRecord()] },
|
|
373
|
+
{ type: "COMPLETE_PEER_ADDR", peerAddr: NODE_ID },
|
|
374
|
+
]);
|
|
375
|
+
expect(ctx.step).toBe("testing");
|
|
376
|
+
expect(effectTypes(effects)).toEqual(["CREATE_REMOTE"]);
|
|
377
|
+
});
|
|
378
|
+
it("ignores a non-matching address", () => {
|
|
379
|
+
const { ctx, effects } = run([
|
|
380
|
+
{ type: "MODAL_OPEN" },
|
|
381
|
+
{ type: "COMPLETE_PEER_ADDR", peerAddr: "b".repeat(64) },
|
|
382
|
+
]);
|
|
383
|
+
expect(ctx.step).toBe("url");
|
|
384
|
+
expect(effects).toEqual([]);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
describe("back / cancel / lifecycle", () => {
|
|
388
|
+
it("BACK from testing cancels the in-flight probe", () => {
|
|
389
|
+
const { ctx, effects } = run([{ type: "SUBMIT_URL", input: NODE_ID }, { type: "BACK" }]);
|
|
390
|
+
expect(ctx.step).toBe("url");
|
|
391
|
+
expect(effects).toEqual([{ type: "CANCEL_IN_FLIGHT" }]);
|
|
392
|
+
});
|
|
393
|
+
it("BACK from auth returns to url/input without effects", () => {
|
|
394
|
+
const { ctx, effects } = run([
|
|
395
|
+
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
396
|
+
{ type: "DUPLICATE_RESULT", duplicateName: null },
|
|
397
|
+
{ type: "CONNECTION_RESULT", outcome: { kind: "needs_auth", serverInfo: INFO } },
|
|
398
|
+
{ type: "BACK" },
|
|
399
|
+
]);
|
|
400
|
+
expect(ctx.step).toBe("url");
|
|
401
|
+
expect(effects).toEqual([]);
|
|
402
|
+
});
|
|
403
|
+
it("MODAL_OPEN resets and loads pending remotes; MODAL_CLOSE cancels and closes", () => {
|
|
404
|
+
const open = run([{ type: "MODAL_OPEN", initialInput: NODE_ID }]);
|
|
405
|
+
expect(open.ctx.input).toBe(NODE_ID);
|
|
406
|
+
expect(open.effects).toEqual([{ type: "LOAD_PENDING_REMOTES" }]);
|
|
407
|
+
const close = run([{ type: "SUBMIT_URL", input: NODE_ID }, { type: "MODAL_CLOSE" }]);
|
|
408
|
+
expect(close.ctx).toEqual(initialContext());
|
|
409
|
+
expect(close.effects).toEqual([{ type: "CANCEL_IN_FLIGHT" }, { type: "CALL_ON_CLOSE" }]);
|
|
410
|
+
});
|
|
411
|
+
it("stale async results in the wrong step are ignored", () => {
|
|
412
|
+
// a CONNECTION_RESULT arriving after BACK returned to url must not move the machine
|
|
413
|
+
const { ctx, effects } = run([
|
|
414
|
+
{ type: "SUBMIT_URL", input: NODE_ID },
|
|
415
|
+
{ type: "BACK" },
|
|
416
|
+
{ type: "CONNECTION_RESULT", outcome: { kind: "needs_auth", serverInfo: INFO } },
|
|
417
|
+
]);
|
|
418
|
+
expect(ctx.step).toBe("url");
|
|
419
|
+
expect(effects).toEqual([]);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AddPeerEffect, AddPeerEvent, AddPeerFlowDeps, AddPeerState } from "./types.js";
|
|
2
|
+
export interface AddPeerFlow {
|
|
3
|
+
state(): AddPeerState;
|
|
4
|
+
/** pure transition: applies `event`, returns the effects it produced.
|
|
5
|
+
* the caller (or `runEffect`) is responsible for executing them in
|
|
6
|
+
* order. */
|
|
7
|
+
send(event: AddPeerEvent): AddPeerEffect[];
|
|
8
|
+
/** execute one effect against the deps, feed its outcome event back
|
|
9
|
+
* into the machine, and drain any follow-on effects. */
|
|
10
|
+
runEffect(effect: AddPeerEffect): Promise<void>;
|
|
11
|
+
/** send + drain in one call: the usual adapter entry point. */
|
|
12
|
+
dispatch(event: AddPeerEvent): Promise<void>;
|
|
13
|
+
dispose(): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function createAddPeerFlow(deps: AddPeerFlowDeps): AddPeerFlow;
|
|
16
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/flows/add-peer/runner.ts"],"names":[],"mappings":"AAuBA,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,CAiUpE"}
|