@statewalker/httpeers-share 0.0.0-stage → 0.1.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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -2
  3. package/dist/bridge/bridge.d.ts +37 -0
  4. package/dist/bridge/bridge.d.ts.map +1 -0
  5. package/dist/bridge/bridge.js +195 -0
  6. package/dist/bridge/index.d.ts +8 -0
  7. package/dist/bridge/index.d.ts.map +1 -0
  8. package/dist/bridge/index.js +7 -0
  9. package/dist/bridge/serve.d.ts +20 -0
  10. package/dist/bridge/serve.d.ts.map +1 -0
  11. package/dist/bridge/serve.js +66 -0
  12. package/dist/bridge/sockets.d.ts +47 -0
  13. package/dist/bridge/sockets.d.ts.map +1 -0
  14. package/dist/bridge/sockets.js +86 -0
  15. package/dist/index.d.ts +20 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +19 -0
  18. package/dist/invite-link.d.ts +21 -0
  19. package/dist/invite-link.d.ts.map +1 -0
  20. package/dist/invite-link.js +38 -0
  21. package/dist/join-outcome.d.ts +16 -0
  22. package/dist/join-outcome.d.ts.map +1 -0
  23. package/dist/join-outcome.js +32 -0
  24. package/dist/lifecycle.d.ts +19 -0
  25. package/dist/lifecycle.d.ts.map +1 -0
  26. package/dist/lifecycle.js +28 -0
  27. package/dist/policy.d.ts +19 -0
  28. package/dist/policy.d.ts.map +1 -0
  29. package/dist/policy.js +40 -0
  30. package/dist/probe.d.ts +20 -0
  31. package/dist/probe.d.ts.map +1 -0
  32. package/dist/probe.js +60 -0
  33. package/dist/service-table.d.ts +33 -0
  34. package/dist/service-table.d.ts.map +1 -0
  35. package/dist/service-table.js +59 -0
  36. package/dist/share-router.d.ts +15 -0
  37. package/dist/share-router.d.ts.map +1 -0
  38. package/dist/share-router.js +31 -0
  39. package/dist/socket-frames.d.ts +30 -0
  40. package/dist/socket-frames.d.ts.map +1 -0
  41. package/dist/socket-frames.js +81 -0
  42. package/dist/socket-tunnel.d.ts +46 -0
  43. package/dist/socket-tunnel.d.ts.map +1 -0
  44. package/dist/socket-tunnel.js +157 -0
  45. package/dist/stamp-guest.d.ts +7 -0
  46. package/dist/stamp-guest.d.ts.map +1 -0
  47. package/dist/stamp-guest.js +17 -0
  48. package/dist/targets.d.ts +18 -0
  49. package/dist/targets.d.ts.map +1 -0
  50. package/dist/targets.js +48 -0
  51. package/dist/upstreams.d.ts +47 -0
  52. package/dist/upstreams.d.ts.map +1 -0
  53. package/dist/upstreams.js +141 -0
  54. package/package.json +59 -3
  55. package/src/bridge/bridge.ts +229 -0
  56. package/src/bridge/index.ts +17 -0
  57. package/src/bridge/serve.ts +88 -0
  58. package/src/bridge/sockets.ts +119 -0
  59. package/src/index.ts +50 -0
  60. package/src/invite-link.ts +48 -0
  61. package/src/join-outcome.ts +33 -0
  62. package/src/lifecycle.ts +31 -0
  63. package/src/policy.ts +52 -0
  64. package/src/probe.ts +86 -0
  65. package/src/service-table.ts +91 -0
  66. package/src/share-router.ts +43 -0
  67. package/src/socket-frames.ts +83 -0
  68. package/src/socket-tunnel.ts +167 -0
  69. package/src/stamp-guest.ts +18 -0
  70. package/src/targets.ts +51 -0
  71. package/src/upstreams.ts +149 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Upstream kinds: how the Sharer's tab reaches a Service's local URL.
3
+ */
4
+
5
+ import { MESH_CREDENTIAL_HEADERS } from "@statewalker/httpeers-core";
6
+ import { MARKER, type Upstream, urlUpstream } from "@statewalker/webrun-http-proxy";
7
+
8
+ /** Which local URL the Bridge relays to. Set by the Sharer, never by a Guest. */
9
+ export const BRIDGE_TARGET_HEADER = "x-peershare-target";
10
+ /** The Pairing token the Bridge printed at launch. */
11
+ export const BRIDGE_TOKEN_HEADER = "x-peershare-bridge-token";
12
+ /** Marks the Bridge's own responses, so they are never mistaken for the Service's. */
13
+ export const BRIDGE_MARKER = "x-peershare-bridge";
14
+ /**
15
+ * A Service's redirect, restated by the Bridge as a 200 with these two
16
+ * headers: a browser makes every `redirect: "manual"` 3xx opaque (status 0,
17
+ * no `Location`), so a real 3xx would reach the page with nothing in it.
18
+ */
19
+ export const BRIDGE_REDIRECT_STATUS_HEADER = "x-peershare-redirect-status";
20
+ export const BRIDGE_REDIRECT_LOCATION_HEADER = "x-peershare-redirect-location";
21
+ /**
22
+ * How many cookies the Service tried to set. `Set-Cookie` is a forbidden
23
+ * response header: no page script can read it, so the Bridge says it instead.
24
+ */
25
+ export const BRIDGE_SET_COOKIE_HEADER = "x-peershare-set-cookie";
26
+ /**
27
+ * Which Guest a request is for, as the Sharer states it from the mesh-proven
28
+ * peer id (rung 16); a Guest's own copy is overwritten. The Bridge keeps one
29
+ * cookie jar per value.
30
+ */
31
+ export const BRIDGE_GUEST_HEADER = "x-peershare-guest";
32
+ /** Set by a Bridge with cookie jars when it kept the Service's cookies for that Guest. */
33
+ export const BRIDGE_COOKIES_KEPT_HEADER = "x-peershare-cookies";
34
+ /** The Guest id the Probe uses, so a Bridge with cookie jars treats it like any Guest. */
35
+ export const PROBE_GUEST = "peershare-probe";
36
+
37
+ export type UpstreamConfig =
38
+ | { kind: "direct"; target: string }
39
+ | { kind: "bridged"; target: string; bridge: { base: string; token: string } };
40
+
41
+ export function createUpstream(config: UpstreamConfig): Upstream {
42
+ // A Guest's membership token and peer id are consumed by the mesh hop:
43
+ // Dirk's app has no business seeing them, whichever way it is reached.
44
+ // A Guest's copy of the Bridge headers goes too: the target and token are
45
+ // the Sharer's to set, and Direct would otherwise hand them to the app.
46
+ const stripRequestHeaders = [
47
+ ...MESH_CREDENTIAL_HEADERS,
48
+ BRIDGE_TARGET_HEADER,
49
+ BRIDGE_TOKEN_HEADER,
50
+ ];
51
+ if (config.kind === "direct") {
52
+ return inGuestOrigin(
53
+ urlUpstream({
54
+ base: config.target,
55
+ stripRequestHeaders,
56
+ fetchImpl: directFetch(needsLocalAddressSpace(config.target)),
57
+ }),
58
+ config.target,
59
+ );
60
+ }
61
+ // Bridged is a URL upstream too: re-issued at the Bridge instead of the
62
+ // Service, with the real target and the Pairing token as headers.
63
+ const viaBridge = urlUpstream({
64
+ base: config.bridge.base,
65
+ stripRequestHeaders,
66
+ headers: {
67
+ [BRIDGE_TARGET_HEADER]: config.target,
68
+ [BRIDGE_TOKEN_HEADER]: config.bridge.token,
69
+ },
70
+ });
71
+ return inGuestOrigin(async (request) => {
72
+ const response = await viaBridge(request);
73
+ const redirect = response.headers.get(BRIDGE_REDIRECT_STATUS_HEADER);
74
+ if (redirect != null) {
75
+ // Back to a real redirect for the Guest (see BRIDGE_REDIRECT_STATUS_HEADER).
76
+ const headers = new Headers(response.headers);
77
+ headers.set("location", headers.get(BRIDGE_REDIRECT_LOCATION_HEADER) ?? "/");
78
+ headers.delete(BRIDGE_REDIRECT_STATUS_HEADER);
79
+ headers.delete(BRIDGE_REDIRECT_LOCATION_HEADER);
80
+ return new Response(null, { status: Number(redirect), headers });
81
+ }
82
+ // To `urlUpstream` the Bridge IS the upstream, so its "unreachable" means
83
+ // the Bridge is not running, not that the Service is down.
84
+ if (response.headers.get(MARKER) !== "upstream-unreachable") return response;
85
+ const headers = new Headers(response.headers);
86
+ headers.delete(MARKER);
87
+ headers.set(BRIDGE_MARKER, "bridge-unreachable");
88
+ return new Response(response.body, { status: response.status, headers });
89
+ }, config.target);
90
+ }
91
+
92
+ /**
93
+ * A Guest sees the Service at the root of their own isolated origin (rung
94
+ * 08), so a redirect the Service writes absolutely to ITSELF
95
+ * (`https://wiki.example.com/login`, `http://127.0.0.1:5173/login`) must
96
+ * become a path, or the Guest's frame leaves that origin. Redirects anywhere
97
+ * else stay absolute.
98
+ */
99
+ function inGuestOrigin(upstream: Upstream, target: string): Upstream {
100
+ const own = new URL(target).origin;
101
+ return async (request) => {
102
+ const response = await upstream(request);
103
+ const location = response.headers.get("location");
104
+ if (response.status < 300 || response.status >= 400 || location == null) return response;
105
+ const resolved = new URL(location, target);
106
+ if (resolved.origin !== own) return response;
107
+ const headers = new Headers(response.headers);
108
+ headers.set("location", `${resolved.pathname}${resolved.search}${resolved.hash}`);
109
+ return new Response(response.body, { status: response.status, headers });
110
+ };
111
+ }
112
+
113
+ /** Which hop did not answer, read off an `Upstream`'s response; `undefined` when something did. */
114
+ export function unreachable(response: Response): "service" | "bridge" | undefined {
115
+ const bridge = response.headers.get(BRIDGE_MARKER);
116
+ if (bridge === "bridge-unreachable") return "bridge";
117
+ if (bridge === "target-unreachable") return "service";
118
+ if (response.headers.get(MARKER) === "upstream-unreachable") return "service";
119
+ return undefined;
120
+ }
121
+
122
+ /**
123
+ * A plain-http Service on the LAN, called from the https PeerShare page, is
124
+ * mixed content and Chromium blocks it — unless the call says the target is
125
+ * on the local network, which also lets Local Network Access ask the Sharer
126
+ * (measured in rung 03). Loopback needs nothing: it is trusted already.
127
+ */
128
+ function needsLocalAddressSpace(target: string): boolean {
129
+ const url = new URL(target);
130
+ if (url.protocol !== "http:") return false;
131
+ const host = url.hostname.replace(/^\[|\]$/g, "");
132
+ return !(host === "localhost" || host === "::1" || /^127\./.test(host));
133
+ }
134
+
135
+ /**
136
+ * How Direct calls the Service. Redirects are FOLLOWED: in the Sharer's
137
+ * browser tab a `redirect: "manual"` response is opaque (status 0, no
138
+ * `Location`), so following is the only way to serve a site that redirects
139
+ * (rung 04). Same everywhere, so Node and the browser agree.
140
+ * `targetAddressSpace` is Chromium's; other runtimes ignore it.
141
+ */
142
+ function directFetch(local: boolean): typeof fetch {
143
+ return (input, init) =>
144
+ fetch(input, {
145
+ ...init,
146
+ redirect: "follow",
147
+ ...(local ? { targetAddressSpace: "local" } : {}),
148
+ } as RequestInit);
149
+ }