@drfed/web 0.0.0-stage → 0.1.0-dev.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/.output/nitro.json +17 -0
- package/.output/public/.vite/manifest.json +493 -0
- package/.output/public/_build/assets/ActorDetailQuery.graphql-D96mwq1k.js +60 -0
- package/.output/public/_build/assets/ActorDetailQuery.graphql-DrxSNh1X.js +60 -0
- package/.output/public/_build/assets/CompleteLoginChallenge.graphql-C5Zq3wNh.js +10 -0
- package/.output/public/_build/assets/CopyButton-B2l484wE.js +1 -0
- package/.output/public/_build/assets/CopyButton-Bi-ZR8x9.css +1 -0
- package/.output/public/_build/assets/CreateInstanceMutation.graphql-B4CRy6cl.js +19 -0
- package/.output/public/_build/assets/CreateObjectMutation.graphql-B4A01dgV.js +23 -0
- package/.output/public/_build/assets/GenerateActorsMutation.graphql-mlHJNIbe.js +23 -0
- package/.output/public/_build/assets/GetHostQuery.graphql-CuzDdOdW.js +17 -0
- package/.output/public/_build/assets/GetHostQuery.graphql-o9K0Qkg0.js +17 -0
- package/.output/public/_build/assets/HomeViewerQuery.graphql-B903WWKs.js +8 -0
- package/.output/public/_build/assets/HttpStatusCode-bgaVzuLy.js +1 -0
- package/.output/public/_build/assets/InstanceActorListQuery.graphql-DmNwiEhM.js +24 -0
- package/.output/public/_build/assets/InstanceActorListQuery.graphql-paa7teUO.js +24 -0
- package/.output/public/_build/assets/InstanceDetailQuery.graphql-B52nR132.js +66 -0
- package/.output/public/_build/assets/InstanceDetailQuery.graphql-DSA2BcAV.js +66 -0
- package/.output/public/_build/assets/InstanceSummary_instance.graphql-BoQAa5Od.js +1 -0
- package/.output/public/_build/assets/JNCCF6MP-CNMywyiC.js +1 -0
- package/.output/public/_build/assets/LP6E37CW-Cwv6G3n1.js +1 -0
- package/.output/public/_build/assets/SignInMutation.graphql-BalZYNFu.js +9 -0
- package/.output/public/_build/assets/TSB6MOZK-CpiH97G1.js +1 -0
- package/.output/public/_build/assets/Toast-3-eXCAKc.css +1 -0
- package/.output/public/_build/assets/Toast-C1zr9crB.js +1 -0
- package/.output/public/_build/assets/WorkspaceQuery.graphql-UoI7QDs4.js +23 -0
- package/.output/public/_build/assets/_...404_-WZ8kAljY.js +1 -0
- package/.output/public/_build/assets/_challengeId_-BgL-Berz.js +1 -0
- package/.output/public/_build/assets/_id_-YcZZShAv.css +1 -0
- package/.output/public/_build/assets/_id_-bpxdclBj.js +1 -0
- package/.output/public/_build/assets/_slug_-CyTA2aW7.css +1 -0
- package/.output/public/_build/assets/_slug_-DXfRknzJ.js +200 -0
- package/.output/public/_build/assets/about-DIrbzRsi.js +1 -0
- package/.output/public/_build/assets/actors-HfZh_afh.js +24 -0
- package/.output/public/_build/assets/client-CGFoRcqH.js +3 -0
- package/.output/public/_build/assets/components-5MVu7IcD.js +1 -0
- package/.output/public/_build/assets/dist-BrJ_B8nU.js +1 -0
- package/.output/public/_build/assets/dist-DVo0HS5G.js +1 -0
- package/.output/public/_build/assets/dist-WrlGlK67.js +1 -0
- package/.output/public/_build/assets/entry-client-BurSTYr9.css +1 -0
- package/.output/public/_build/assets/entry-client-RIC5nXxX.js +8 -0
- package/.output/public/_build/assets/form-BxigYd7J.css +1 -0
- package/.output/public/_build/assets/form.module-BecmEfV_.js +1 -0
- package/.output/public/_build/assets/index-BHn6sAgH.js +23 -0
- package/.output/public/_build/assets/index-BSLqnqN7.js +8 -0
- package/.output/public/_build/assets/index-B_xLBDOo.css +1 -0
- package/.output/public/_build/assets/instance-tpMrvJNZ.js +35 -0
- package/.output/public/_build/assets/label-Mv8GZ4jc.js +1 -0
- package/.output/public/_build/assets/objects-DPb8DI8A.css +1 -0
- package/.output/public/_build/assets/objects-DR9TfL-V.js +25 -0
- package/.output/public/_build/assets/relay-runtime-BX3uubtA.js +7 -0
- package/.output/public/_build/assets/routing-DMN2NHu6.js +1 -0
- package/.output/public/_build/assets/sign-in-B67sv7z8.js +10 -0
- package/.output/public/_build/assets/web-DB_Nkq2D.js +1 -0
- package/.output/public/apple-touch-icon.png +0 -0
- package/.output/public/favicon.ico +0 -0
- package/.output/public/icon-192.png +0 -0
- package/.output/public/icon-512.png +0 -0
- package/.output/public/icon-mask.png +0 -0
- package/.output/public/icon.svg +395 -0
- package/.output/public/manifest.webmanifest +13 -0
- package/.output/server/_...404_-DDEH5apf.mjs +16 -0
- package/.output/server/_challengeId_-C7BBdG0r.mjs +100 -0
- package/.output/server/_chunks/ssr-renderer.mjs +26 -0
- package/.output/server/_id_-DqytvZEf.mjs +505 -0
- package/.output/server/_libs/@shikijs/core+[...].mjs +7173 -0
- package/.output/server/_libs/@shikijs/engine-javascript+[...].mjs +3110 -0
- package/.output/server/_libs/babel__runtime.mjs +243 -0
- package/.output/server/_libs/dequal.mjs +25 -0
- package/.output/server/_libs/faker-js__faker.mjs +13150 -0
- package/.output/server/_libs/fbjs.mjs +115 -0
- package/.output/server/_libs/h3+rou3+srvx.mjs +650 -0
- package/.output/server/_libs/hookable.mjs +41 -0
- package/.output/server/_libs/invariant.mjs +36 -0
- package/.output/server/_libs/relay-runtime.mjs +9704 -0
- package/.output/server/_libs/seroval-plugins.mjs +563 -0
- package/.output/server/_libs/seroval.mjs +2956 -0
- package/.output/server/_libs/shikijs__langs.mjs +3 -0
- package/.output/server/_libs/shikijs__themes.mjs +7 -0
- package/.output/server/_libs/solid-js.mjs +1248 -0
- package/.output/server/_libs/solid-relay+tiny-invariant.mjs +1029 -0
- package/.output/server/_libs/ufo.mjs +64 -0
- package/.output/server/_libs/valibot.mjs +711 -0
- package/.output/server/_runtime.mjs +33 -0
- package/.output/server/_slug_-CBOaSqoF.mjs +978 -0
- package/.output/server/_ssr/ActorDetailQuery.graphql-C1mWRP1B.mjs +283 -0
- package/.output/server/_ssr/CompleteLoginChallenge.graphql-CM7o_Md1.mjs +78 -0
- package/.output/server/_ssr/CopyButton-CQNR5D1y.mjs +43 -0
- package/.output/server/_ssr/CreateInstanceMutation.graphql-C-h2lGxz.mjs +116 -0
- package/.output/server/_ssr/CreateObjectMutation.graphql-C393Hp7q.mjs +166 -0
- package/.output/server/_ssr/GenerateActorsMutation.graphql-D2ADtn1j.mjs +163 -0
- package/.output/server/_ssr/GetHostQuery.graphql-C45XQo1i.mjs +127 -0
- package/.output/server/_ssr/HomeViewerQuery.graphql-Dote4Pkz.mjs +81 -0
- package/.output/server/_ssr/InstanceActorListQuery.graphql-DgItgN5t.mjs +167 -0
- package/.output/server/_ssr/InstanceDetailQuery.graphql-DP5wWWjX.mjs +367 -0
- package/.output/server/_ssr/InstanceSummary_instance.graphql-10tilF-Z.mjs +34 -0
- package/.output/server/_ssr/JNCCF6MP-BAZlBnvx.mjs +419 -0
- package/.output/server/_ssr/LP6E37CW-D2Ih_b_K.mjs +163 -0
- package/.output/server/_ssr/SignInMutation.graphql-BBXmiay6.mjs +72 -0
- package/.output/server/_ssr/TSB6MOZK-D06WZb6o.mjs +166 -0
- package/.output/server/_ssr/Toast-Bb1gcxED.mjs +947 -0
- package/.output/server/_ssr/WorkspaceQuery.graphql-DnCO8t-4.mjs +153 -0
- package/.output/server/_ssr/about-CD3QIgf_.mjs +14 -0
- package/.output/server/_ssr/actors-CMWf-xQU.mjs +1160 -0
- package/.output/server/_ssr/dist-D6rgyh-L.mjs +81 -0
- package/.output/server/_ssr/dist-I7KRMRbA.mjs +432 -0
- package/.output/server/_ssr/form.module-CTvIl1lm.mjs +192 -0
- package/.output/server/_ssr/index-C4crD6q-.mjs +347 -0
- package/.output/server/_ssr/index-yuhuc6-X.mjs +111 -0
- package/.output/server/_ssr/instance-ZYWGeWd3.mjs +280 -0
- package/.output/server/_ssr/label-DAXzanI9.mjs +9 -0
- package/.output/server/_ssr/objects-D5MVukzB.mjs +4316 -0
- package/.output/server/_ssr/routing-DKTLkkbY.mjs +620 -0
- package/.output/server/_ssr/sign-in-CNHZb_zL.mjs +264 -0
- package/.output/server/_ssr/ssr.mjs +6138 -0
- package/.output/server/index.mjs +691 -0
- package/LICENSE +661 -0
- package/README.md +143 -2
- package/dist/bridge.d.mts +39 -0
- package/dist/bridge.mjs +104 -0
- package/dist/bridge.mjs.map +1 -0
- package/dist/request-policy.d.mts +8 -0
- package/dist/request-policy.mjs +94 -0
- package/dist/request-policy.mjs.map +1 -0
- package/dist/runtime.d.mts +14 -0
- package/dist/runtime.mjs +44 -0
- package/dist/runtime.mjs.map +1 -0
- package/package.json +111 -3
package/README.md
CHANGED
|
@@ -1,3 +1,144 @@
|
|
|
1
|
-
|
|
1
|
+
@drfed/web
|
|
2
|
+
==========
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
SolidStart frontend and embedded Node.js runtime for [DrFed].
|
|
5
|
+
|
|
6
|
+
[DrFed]: https://drfed.org/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Installed standalone frontend
|
|
10
|
+
-----------------------------
|
|
11
|
+
|
|
12
|
+
Install the application package to obtain the `drfed-web` command:
|
|
13
|
+
|
|
14
|
+
~~~~ sh
|
|
15
|
+
npm install --global @drfed/drfed
|
|
16
|
+
drfed-web --backend-url http://localhost:8888 \
|
|
17
|
+
--public-origin https://drfed.example --listen localhost:3000
|
|
18
|
+
~~~~
|
|
19
|
+
|
|
20
|
+
The backend must already be running, and its `--login-origin` allowlist must
|
|
21
|
+
include the frontend's public origin. The backend value is normalized to an
|
|
22
|
+
origin; requests always use its `/graphql` endpoint. Frontend deployment
|
|
23
|
+
settings are resolved at startup and do not require rebuilding the frontend.
|
|
24
|
+
|
|
25
|
+
All DrFed commands read the same explicit TOML file through `--config FILE` or
|
|
26
|
+
`DRFED_CONFIG`, with CLI values overriding environment values and TOML.
|
|
27
|
+
`drfed-web` uses `web.public_origin`/`DRFED_PUBLIC_ORIGIN`,
|
|
28
|
+
`web.backend_url`/`DRFED_BACKEND_URL` and
|
|
29
|
+
`web.listen`/`DRFED_WEB_LISTEN`. Its default listen address is
|
|
30
|
+
`localhost:3000`; `server.listen`/`DRFED_LISTEN` belongs to the backend.
|
|
31
|
+
The common `telemetry` settings control logging. Database and mail sections are
|
|
32
|
+
unused by this command. `--print-config` explains effective settings and sources
|
|
33
|
+
without starting the runtime or opening log files.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
Development and builds
|
|
37
|
+
----------------------
|
|
38
|
+
|
|
39
|
+
Run `mise run dev` from the repository root to start both development servers.
|
|
40
|
+
`mise run dev:web` starts only Vite; its GraphQL backend must already be
|
|
41
|
+
running. Development uses `DRFED_BACKEND_URL` (default
|
|
42
|
+
`http://drfed.localhost:8888`) and `DRFED_PUBLIC_ORIGIN` (default
|
|
43
|
+
`http://localhost:3000`). Add the public origin to the backend's login
|
|
44
|
+
allowlist when using a different origin.
|
|
45
|
+
|
|
46
|
+
Run `mise run build:web` to build the Node adapter, shared binding, SSR bundle
|
|
47
|
+
and static assets. `mise run build:web-runtime` builds just the adapter and
|
|
48
|
+
binding for Vite development.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Embedded runtime
|
|
52
|
+
----------------
|
|
53
|
+
|
|
54
|
+
Initialize the web runtime with a public origin and GraphQL handler:
|
|
55
|
+
|
|
56
|
+
~~~~ ts
|
|
57
|
+
import { createWebRuntime } from "@drfed/web";
|
|
58
|
+
|
|
59
|
+
const web = await createWebRuntime({
|
|
60
|
+
publicOrigin: new URL("https://drfed.example"),
|
|
61
|
+
graphqlFetch: (request) => yoga.fetch(request),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const response = await web.fetch(request);
|
|
65
|
+
await web.close();
|
|
66
|
+
~~~~
|
|
67
|
+
|
|
68
|
+
Importing the package and creating the runtime open no listening socket or
|
|
69
|
+
process signal/error handlers. `fetch()` serves SolidStart SSR, server functions
|
|
70
|
+
and bundled static assets. Deployment settings live in one external Node module
|
|
71
|
+
shared by the adapter and Nitro bundle, rather than argv or serialized runtime
|
|
72
|
+
configuration. Each server-side Relay request reads its own SolidStart request
|
|
73
|
+
context and forwards only a validated session token as Bearer authorization.
|
|
74
|
+
Other cookies and incoming authorization/proxy headers are not forwarded.
|
|
75
|
+
|
|
76
|
+
Nitro uses process-wide state, so initialize one runtime per process. A second
|
|
77
|
+
initialization is rejected, including after close. `close()` is idempotent,
|
|
78
|
+
cancels active GraphQL work and waits for its response bodies to finish before
|
|
79
|
+
running Nitro close hooks. An injected GraphQL handler remains caller-owned;
|
|
80
|
+
a handler that ignores cancellation can delay close until the caller's deadline.
|
|
81
|
+
Vite uses Nitro's in-process development runner so SSR shares the external
|
|
82
|
+
binding. Frontend HMR retains it; a Vite config or environment restart installs
|
|
83
|
+
a new development binding and drains the previous server's GraphQL work.
|
|
84
|
+
|
|
85
|
+
The [Nitro configuration] controls the listener-free Node entry and external
|
|
86
|
+
binding. [Solid's request context] supplies the request cancellation signal.
|
|
87
|
+
|
|
88
|
+
[Nitro configuration]: https://nitro.build/docs/configuration
|
|
89
|
+
[Solid's request context]: https://docs.solidjs.com/reference/server-utilities/get-request-event
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
Request and cookie policy
|
|
93
|
+
-------------------------
|
|
94
|
+
|
|
95
|
+
Serve the web app only through the configured public origin's authority.
|
|
96
|
+
Host-preserving reverse proxies may terminate TLS: DrFed restores the request
|
|
97
|
+
URL's scheme from the public origin and strips forwarded headers. It does not
|
|
98
|
+
trust forwarded host or protocol headers. A different authority receives 421;
|
|
99
|
+
malformed or inconsistent URL/Host values receive 400.
|
|
100
|
+
|
|
101
|
+
[Server functions] accept POST only, with an Origin matching the public origin.
|
|
102
|
+
When Origin is absent, a same-origin Referer is required. Missing, opaque,
|
|
103
|
+
malformed and cross-origin sources are rejected before body deserialization;
|
|
104
|
+
instance subdomains are also cross-origin. The same policy runs in embedded
|
|
105
|
+
and development servers, including encoded server-function path aliases.
|
|
106
|
+
Regular login-link GET pages still render, while confirmation uses POST.
|
|
107
|
+
|
|
108
|
+
Session cookies use HttpOnly, SameSite=Lax and path `/`, without Domain.
|
|
109
|
+
Secure follows the configured public origin's HTTPS scheme, including behind
|
|
110
|
+
TLS termination; forwarded headers cannot downgrade it.
|
|
111
|
+
|
|
112
|
+
[Server functions]: https://docs.solidjs.com/solid-start/v2/guides/security#csrf-cross-site-request-forgery
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
Verification
|
|
116
|
+
------------
|
|
117
|
+
|
|
118
|
+
`mise run test:web-runtime` checks SSR, static assets, binding identity,
|
|
119
|
+
controlled GraphQL headers, cancellation, idempotent draining close and the
|
|
120
|
+
absence of listeners or process-handler changes. `mise run test` includes this
|
|
121
|
+
test after building the frontend. `mise run test:packaging` also exercises the
|
|
122
|
+
runtime from local and global npm tarball installations.
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
Combined serving
|
|
126
|
+
----------------
|
|
127
|
+
|
|
128
|
+
`drfed-server --root-origin https://drfed.example.com` serves this frontend
|
|
129
|
+
and `/graphql` on its single listener. The web runtime calls Yoga directly in
|
|
130
|
+
that process, using the same controlled GraphQL bridge as standalone mode.
|
|
131
|
+
Use `--no-web` with explicit backend login origins when running `drfed-web`
|
|
132
|
+
separately. Repository `mise run dev` uses this split mode for Vite HMR.
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
Standalone backend security
|
|
136
|
+
---------------------------
|
|
137
|
+
|
|
138
|
+
`drfed-web` requires HTTPS for non-loopback backends because requests carry
|
|
139
|
+
session tokens. HTTP is allowed for `localhost`, its subdomains, IPv4
|
|
140
|
+
loopback addresses and `::1`. For a trusted internal network, explicitly use
|
|
141
|
+
`--allow-insecure-backend`, `DRFED_ALLOW_INSECURE_BACKEND=true`, or
|
|
142
|
+
`web.allow_insecure_backend = true` in TOML. This permits unencrypted session
|
|
143
|
+
tokens and logs a warning at startup. `--no-allow-insecure-backend` overrides
|
|
144
|
+
permission from the environment or configuration file.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { GraphQLResponse } from "relay-runtime";
|
|
2
|
+
//#region src/bridge.d.ts
|
|
3
|
+
/** A GraphQL fetch handler owned by the embedding application. */
|
|
4
|
+
type GraphqlFetch = (request: Request) => Response | Promise<Response>;
|
|
5
|
+
/** Immutable deployment settings shared by the adapter and Nitro server. */
|
|
6
|
+
interface WebRuntimeOptions {
|
|
7
|
+
readonly publicOrigin: URL;
|
|
8
|
+
readonly graphqlFetch: GraphqlFetch;
|
|
9
|
+
}
|
|
10
|
+
/** Install the process's single web binding before importing Nitro.
|
|
11
|
+
* @returns Runtime cancellation and shared idempotent cleanup.
|
|
12
|
+
*/
|
|
13
|
+
declare function installWebBinding(options: WebRuntimeOptions): {
|
|
14
|
+
signal: AbortSignal;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
/** Replace the development binding during a Vite restart.
|
|
18
|
+
* Production initialization remains limited to one runtime per process.
|
|
19
|
+
* @returns Cancellation and cleanup for this development server only.
|
|
20
|
+
*/
|
|
21
|
+
declare function installDevelopmentWebBinding(options: WebRuntimeOptions): {
|
|
22
|
+
signal: AbortSignal;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
/** Run a server-side Relay request through the configured GraphQL handler.
|
|
26
|
+
* @returns The parsed GraphQL response after its body has been consumed.
|
|
27
|
+
*/
|
|
28
|
+
declare function queryGraphql(query: string, variables: Readonly<Record<string, unknown>>, accessToken: string | undefined, sourceSignal: AbortSignal): Promise<GraphQLResponse>;
|
|
29
|
+
/** The immutable public web origin used for redirects and cookie security.
|
|
30
|
+
* @returns The configured normalized web origin.
|
|
31
|
+
*/
|
|
32
|
+
declare function getPublicOrigin(): string;
|
|
33
|
+
/** Apply the shared web authority, public scheme and server-function policy.
|
|
34
|
+
* @returns The prepared request or an early rejection.
|
|
35
|
+
*/
|
|
36
|
+
declare function prepareWebRequest(request: Request): Request | Response;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { GraphqlFetch, WebRuntimeOptions, getPublicOrigin, installDevelopmentWebBinding, installWebBinding, prepareWebRequest, queryGraphql };
|
|
39
|
+
//# sourceMappingURL=bridge.d.mts.map
|
package/dist/bridge.mjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { prepareWebRequest as prepareWebRequest$1 } from "./request-policy.mjs";
|
|
2
|
+
//#region src/bridge.ts
|
|
3
|
+
let binding;
|
|
4
|
+
let initialized = false;
|
|
5
|
+
/** Install the process's single web binding before importing Nitro.
|
|
6
|
+
* @returns Runtime cancellation and shared idempotent cleanup.
|
|
7
|
+
*/
|
|
8
|
+
function installWebBinding(options) {
|
|
9
|
+
if (initialized) throw new Error("A web runtime was already initialized in this process.");
|
|
10
|
+
const owner = createBinding(options);
|
|
11
|
+
initialized = true;
|
|
12
|
+
return owner;
|
|
13
|
+
}
|
|
14
|
+
/** Replace the development binding during a Vite restart.
|
|
15
|
+
* Production initialization remains limited to one runtime per process.
|
|
16
|
+
* @returns Cancellation and cleanup for this development server only.
|
|
17
|
+
*/
|
|
18
|
+
function installDevelopmentWebBinding(options) {
|
|
19
|
+
if (initialized) throw new Error("A production web runtime was already initialized.");
|
|
20
|
+
return createBinding(options);
|
|
21
|
+
}
|
|
22
|
+
function createBinding(options) {
|
|
23
|
+
const origin = new URL(options.publicOrigin.href);
|
|
24
|
+
if (!["http:", "https:"].includes(origin.protocol) || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash || typeof options.graphqlFetch !== "function") throw new TypeError("Invalid web runtime settings.");
|
|
25
|
+
const current = {
|
|
26
|
+
publicOrigin: origin.origin,
|
|
27
|
+
graphqlFetch: options.graphqlFetch,
|
|
28
|
+
abort: new AbortController(),
|
|
29
|
+
pending: /* @__PURE__ */ new Set()
|
|
30
|
+
};
|
|
31
|
+
binding = current;
|
|
32
|
+
let closing;
|
|
33
|
+
return {
|
|
34
|
+
signal: current.abort.signal,
|
|
35
|
+
close() {
|
|
36
|
+
closing ??= (async () => {
|
|
37
|
+
await Promise.resolve();
|
|
38
|
+
current.abort.abort();
|
|
39
|
+
await Promise.allSettled(current.pending);
|
|
40
|
+
if (binding === current) binding = void 0;
|
|
41
|
+
})();
|
|
42
|
+
return closing;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Run a server-side Relay request through the configured GraphQL handler.
|
|
47
|
+
* @returns The parsed GraphQL response after its body has been consumed.
|
|
48
|
+
*/
|
|
49
|
+
async function queryGraphql(query, variables, accessToken, sourceSignal) {
|
|
50
|
+
const current = binding;
|
|
51
|
+
if (current === void 0 || current.abort.signal.aborted) throw new Error("Web runtime is unavailable.");
|
|
52
|
+
const signal = AbortSignal.any([sourceSignal, current.abort.signal]);
|
|
53
|
+
signal.throwIfAborted();
|
|
54
|
+
const headers = { "content-type": "application/json" };
|
|
55
|
+
if (accessToken !== void 0 && /^[A-Za-z0-9_-]{43}$/u.test(accessToken)) headers.Authorization = `Bearer ${accessToken}`;
|
|
56
|
+
const request = new Request(new URL("/graphql", current.publicOrigin), {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers,
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
query,
|
|
61
|
+
variables
|
|
62
|
+
}),
|
|
63
|
+
signal
|
|
64
|
+
});
|
|
65
|
+
const task = (async () => {
|
|
66
|
+
await Promise.resolve();
|
|
67
|
+
const response = await current.graphqlFetch(request);
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
try {
|
|
70
|
+
await response.body?.cancel();
|
|
71
|
+
} catch {}
|
|
72
|
+
throw new Error(`GraphQL request failed (${response.status}).`);
|
|
73
|
+
}
|
|
74
|
+
const result = await response.json();
|
|
75
|
+
if (!isGraphqlResponse(result)) throw new Error("Invalid GraphQL response.");
|
|
76
|
+
return result;
|
|
77
|
+
})();
|
|
78
|
+
current.pending.add(task);
|
|
79
|
+
try {
|
|
80
|
+
return await task;
|
|
81
|
+
} finally {
|
|
82
|
+
current.pending.delete(task);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isGraphqlResponse(value) {
|
|
86
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
87
|
+
}
|
|
88
|
+
/** The immutable public web origin used for redirects and cookie security.
|
|
89
|
+
* @returns The configured normalized web origin.
|
|
90
|
+
*/
|
|
91
|
+
function getPublicOrigin() {
|
|
92
|
+
if (binding === void 0 || binding.abort.signal.aborted) throw new Error("Web runtime is unavailable.");
|
|
93
|
+
return binding.publicOrigin;
|
|
94
|
+
}
|
|
95
|
+
/** Apply the shared web authority, public scheme and server-function policy.
|
|
96
|
+
* @returns The prepared request or an early rejection.
|
|
97
|
+
*/
|
|
98
|
+
function prepareWebRequest(request) {
|
|
99
|
+
return prepareWebRequest$1(request, getPublicOrigin());
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { getPublicOrigin, installDevelopmentWebBinding, installWebBinding, prepareWebRequest, queryGraphql };
|
|
103
|
+
|
|
104
|
+
//# sourceMappingURL=bridge.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.mjs","names":["applyRequestPolicy"],"sources":["../src/bridge.ts"],"sourcesContent":["// DrFed: A web-based platform for developing and debugging ActivityPub apps\n// Copyright (C) 2026 DrFed team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\n\nimport type { GraphQLResponse } from \"relay-runtime\";\n\nimport { prepareWebRequest as applyRequestPolicy } from \"./request-policy.ts\";\n\n/** A GraphQL fetch handler owned by the embedding application. */\nexport type GraphqlFetch = (request: Request) => Response | Promise<Response>;\n\n/** Immutable deployment settings shared by the adapter and Nitro server. */\nexport interface WebRuntimeOptions {\n readonly publicOrigin: URL;\n readonly graphqlFetch: GraphqlFetch;\n}\n\ninterface Binding {\n readonly publicOrigin: string;\n readonly graphqlFetch: GraphqlFetch;\n readonly abort: AbortController;\n readonly pending: Set<Promise<unknown>>;\n}\n\nlet binding: Binding | undefined;\nlet initialized = false;\n\n/** Install the process's single web binding before importing Nitro.\n * @returns Runtime cancellation and shared idempotent cleanup.\n */\nexport function installWebBinding(options: WebRuntimeOptions) {\n if (initialized) {\n throw new Error(\"A web runtime was already initialized in this process.\");\n }\n const owner = createBinding(options);\n initialized = true;\n return owner;\n}\n\n/** Replace the development binding during a Vite restart.\n * Production initialization remains limited to one runtime per process.\n * @returns Cancellation and cleanup for this development server only.\n */\nexport function installDevelopmentWebBinding(options: WebRuntimeOptions) {\n if (initialized) {\n throw new Error(\"A production web runtime was already initialized.\");\n }\n return createBinding(options);\n}\n\nfunction createBinding(options: WebRuntimeOptions) {\n const origin = new URL(options.publicOrigin.href);\n if (\n ![\"http:\", \"https:\"].includes(origin.protocol) ||\n origin.username ||\n origin.password ||\n origin.pathname !== \"/\" ||\n origin.search ||\n origin.hash ||\n typeof options.graphqlFetch !== \"function\"\n ) {\n throw new TypeError(\"Invalid web runtime settings.\");\n }\n const current: Binding = {\n publicOrigin: origin.origin,\n graphqlFetch: options.graphqlFetch,\n abort: new AbortController(),\n pending: new Set(),\n };\n binding = current;\n let closing: Promise<void> | undefined;\n return {\n signal: current.abort.signal,\n // oxlint-disable-next-line typescript/promise-function-async\n close(): Promise<void> {\n closing ??= (async () => {\n // Yield before cancellation can invoke callbacks, preserving identity.\n await Promise.resolve();\n current.abort.abort();\n await Promise.allSettled(current.pending);\n if (binding === current) binding = undefined;\n })();\n return closing;\n },\n };\n}\n\n/** Run a server-side Relay request through the configured GraphQL handler.\n * @returns The parsed GraphQL response after its body has been consumed.\n */\nexport async function queryGraphql(\n query: string,\n variables: Readonly<Record<string, unknown>>,\n accessToken: string | undefined,\n sourceSignal: AbortSignal,\n): Promise<GraphQLResponse> {\n const current = binding;\n if (current === undefined || current.abort.signal.aborted) {\n throw new Error(\"Web runtime is unavailable.\");\n }\n const signal = AbortSignal.any([sourceSignal, current.abort.signal]);\n signal.throwIfAborted();\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n };\n if (accessToken !== undefined && /^[A-Za-z0-9_-]{43}$/u.test(accessToken)) {\n headers.Authorization = `Bearer ${accessToken}`;\n }\n const request = new Request(new URL(\"/graphql\", current.publicOrigin), {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query, variables }),\n signal,\n });\n const task = (async () => {\n // Register before the supplied handler can request shutdown.\n await Promise.resolve();\n const response = await current.graphqlFetch(request);\n if (!response.ok) {\n try {\n await response.body?.cancel();\n } catch {\n // Settled cancellation must not expose a handler's raw error.\n }\n throw new Error(`GraphQL request failed (${response.status}).`);\n }\n const result: unknown = await response.json();\n if (!isGraphqlResponse(result)) {\n throw new Error(\"Invalid GraphQL response.\");\n }\n return result;\n })();\n current.pending.add(task);\n try {\n return await task;\n } finally {\n current.pending.delete(task);\n }\n}\n\nfunction isGraphqlResponse(value: unknown): value is GraphQLResponse {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** The immutable public web origin used for redirects and cookie security.\n * @returns The configured normalized web origin.\n */\nexport function getPublicOrigin(): string {\n if (binding === undefined || binding.abort.signal.aborted) {\n throw new Error(\"Web runtime is unavailable.\");\n }\n return binding.publicOrigin;\n}\n\n/** Apply the shared web authority, public scheme and server-function policy.\n * @returns The prepared request or an early rejection.\n */\nexport function prepareWebRequest(request: Request): Request | Response {\n return applyRequestPolicy(request, getPublicOrigin());\n}\n"],"mappings":";;AAoCA,IAAI;AACJ,IAAI,cAAc;;;;AAKlB,SAAgB,kBAAkB,SAA4B;CAC5D,IAAI,aACF,MAAM,IAAI,MAAM,wDAAwD;CAE1E,MAAM,QAAQ,cAAc,OAAO;CACnC,cAAc;CACd,OAAO;AACT;;;;;AAMA,SAAgB,6BAA6B,SAA4B;CACvE,IAAI,aACF,MAAM,IAAI,MAAM,mDAAmD;CAErE,OAAO,cAAc,OAAO;AAC9B;AAEA,SAAS,cAAc,SAA4B;CACjD,MAAM,SAAS,IAAI,IAAI,QAAQ,aAAa,IAAI;CAChD,IACE,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,SAAS,OAAO,QAAQ,KAC7C,OAAO,YACP,OAAO,YACP,OAAO,aAAa,OACpB,OAAO,UACP,OAAO,QACP,OAAO,QAAQ,iBAAiB,YAEhC,MAAM,IAAI,UAAU,+BAA+B;CAErD,MAAM,UAAmB;EACvB,cAAc,OAAO;EACrB,cAAc,QAAQ;EACtB,OAAO,IAAI,gBAAgB;EAC3B,yBAAS,IAAI,IAAI;CACnB;CACA,UAAU;CACV,IAAI;CACJ,OAAO;EACL,QAAQ,QAAQ,MAAM;EAEtB,QAAuB;GACrB,aAAa,YAAY;IAEvB,MAAM,QAAQ,QAAQ;IACtB,QAAQ,MAAM,MAAM;IACpB,MAAM,QAAQ,WAAW,QAAQ,OAAO;IACxC,IAAI,YAAY,SAAS,UAAU,KAAA;GACrC,EAAA,CAAG;GACH,OAAO;EACT;CACF;AACF;;;;AAKA,eAAsB,aACpB,OACA,WACA,aACA,cAC0B;CAC1B,MAAM,UAAU;CAChB,IAAI,YAAY,KAAA,KAAa,QAAQ,MAAM,OAAO,SAChD,MAAM,IAAI,MAAM,6BAA6B;CAE/C,MAAM,SAAS,YAAY,IAAI,CAAC,cAAc,QAAQ,MAAM,MAAM,CAAC;CACnE,OAAO,eAAe;CACtB,MAAM,UAAkC,EACtC,gBAAgB,mBAClB;CACA,IAAI,gBAAgB,KAAA,KAAa,uBAAuB,KAAK,WAAW,GACtE,QAAQ,gBAAgB,UAAU;CAEpC,MAAM,UAAU,IAAI,QAAQ,IAAI,IAAI,YAAY,QAAQ,YAAY,GAAG;EACrE,QAAQ;EACR;EACA,MAAM,KAAK,UAAU;GAAE;GAAO;EAAU,CAAC;EACzC;CACF,CAAC;CACD,MAAM,QAAQ,YAAY;EAExB,MAAM,QAAQ,QAAQ;EACtB,MAAM,WAAW,MAAM,QAAQ,aAAa,OAAO;EACnD,IAAI,CAAC,SAAS,IAAI;GAChB,IAAI;IACF,MAAM,SAAS,MAAM,OAAO;GAC9B,QAAQ,CAER;GACA,MAAM,IAAI,MAAM,2BAA2B,SAAS,OAAO,GAAG;EAChE;EACA,MAAM,SAAkB,MAAM,SAAS,KAAK;EAC5C,IAAI,CAAC,kBAAkB,MAAM,GAC3B,MAAM,IAAI,MAAM,2BAA2B;EAE7C,OAAO;CACT,EAAA,CAAG;CACH,QAAQ,QAAQ,IAAI,IAAI;CACxB,IAAI;EACF,OAAO,MAAM;CACf,UAAU;EACR,QAAQ,QAAQ,OAAO,IAAI;CAC7B;AACF;AAEA,SAAS,kBAAkB,OAA0C;CACnE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;;AAKA,SAAgB,kBAA0B;CACxC,IAAI,YAAY,KAAA,KAAa,QAAQ,MAAM,OAAO,SAChD,MAAM,IAAI,MAAM,6BAA6B;CAE/C,OAAO,QAAQ;AACjB;;;;AAKA,SAAgB,kBAAkB,SAAsC;CACtE,OAAOA,oBAAmB,SAAS,gBAAgB,CAAC;AACtD"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/request-policy.d.ts
|
|
2
|
+
/** Validate authority and cookie-bearing server functions before body parsing.
|
|
3
|
+
* @returns A request with its public URL restored, or an early rejection.
|
|
4
|
+
*/
|
|
5
|
+
declare function prepareWebRequest(request: Request, publicOrigin: string): Request | Response;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { prepareWebRequest };
|
|
8
|
+
//# sourceMappingURL=request-policy.d.mts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//#region src/request-policy.ts
|
|
2
|
+
/** Validate authority and cookie-bearing server functions before body parsing.
|
|
3
|
+
* @returns A request with its public URL restored, or an early rejection.
|
|
4
|
+
*/
|
|
5
|
+
function prepareWebRequest(request, publicOrigin) {
|
|
6
|
+
const origin = new URL(publicOrigin);
|
|
7
|
+
let validated;
|
|
8
|
+
try {
|
|
9
|
+
validated = validateAuthority(request, origin);
|
|
10
|
+
} catch {
|
|
11
|
+
return new Response("Invalid request authority.", { status: 400 });
|
|
12
|
+
}
|
|
13
|
+
if (validated instanceof Response) return validated;
|
|
14
|
+
const incoming = validated;
|
|
15
|
+
let serverFunction;
|
|
16
|
+
try {
|
|
17
|
+
serverFunction = isServerFunctionPath(incoming.pathname, origin);
|
|
18
|
+
} catch {
|
|
19
|
+
return new Response("Invalid request path.", { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
if (serverFunction) {
|
|
22
|
+
if (request.method !== "POST") return new Response("Method not allowed.", {
|
|
23
|
+
status: 405,
|
|
24
|
+
headers: { allow: "POST" }
|
|
25
|
+
});
|
|
26
|
+
if (!sameOriginSource(request.headers, origin)) return new Response("Forbidden server function request.", { status: 403 });
|
|
27
|
+
}
|
|
28
|
+
const target = new URL(incoming.href);
|
|
29
|
+
target.protocol = origin.protocol;
|
|
30
|
+
target.host = origin.host;
|
|
31
|
+
const headers = new Headers(request.headers);
|
|
32
|
+
headers.set("host", origin.host);
|
|
33
|
+
for (const name of [
|
|
34
|
+
"forwarded",
|
|
35
|
+
"x-forwarded-host",
|
|
36
|
+
"x-forwarded-proto",
|
|
37
|
+
"x-forwarded-port",
|
|
38
|
+
"x-forwarded-for"
|
|
39
|
+
]) headers.delete(name);
|
|
40
|
+
const init = {
|
|
41
|
+
method: request.method,
|
|
42
|
+
headers,
|
|
43
|
+
signal: request.signal,
|
|
44
|
+
duplex: "half"
|
|
45
|
+
};
|
|
46
|
+
if (request.method !== "GET" && request.method !== "HEAD") init.body = request.body;
|
|
47
|
+
return new Request(target, init);
|
|
48
|
+
}
|
|
49
|
+
function validateAuthority(request, origin) {
|
|
50
|
+
const incoming = new URL(request.url);
|
|
51
|
+
if (!["http:", "https:"].includes(incoming.protocol) || incoming.username || incoming.password) throw new TypeError("Invalid request URL.");
|
|
52
|
+
const host = request.headers.get("host");
|
|
53
|
+
if (host !== null && authority(host, incoming.protocol) !== authority(incoming.host, incoming.protocol)) throw new TypeError("Inconsistent request authority.");
|
|
54
|
+
if (authority(host ?? incoming.host, origin.protocol) !== authority(origin.host, origin.protocol)) return new Response("Misdirected request.", { status: 421 });
|
|
55
|
+
return incoming;
|
|
56
|
+
}
|
|
57
|
+
function isServerFunctionPath(value, origin) {
|
|
58
|
+
let path = value;
|
|
59
|
+
let serverFunction = path.startsWith("/_server");
|
|
60
|
+
for (let depth = 0; depth < 5; depth += 1) {
|
|
61
|
+
const decoded = decodeURIComponent(path).replaceAll("\\", "/");
|
|
62
|
+
if (decoded === path) break;
|
|
63
|
+
path = decoded;
|
|
64
|
+
serverFunction ||= path.startsWith("/_server");
|
|
65
|
+
if (depth === 4) throw new TypeError("Ambiguous encoded path.");
|
|
66
|
+
}
|
|
67
|
+
return serverFunction || new URL(`/${path.replace(/^\/+/u, "")}`, origin).pathname.startsWith("/_server");
|
|
68
|
+
}
|
|
69
|
+
function sameOriginSource(headers, origin) {
|
|
70
|
+
const source = headers.get("origin");
|
|
71
|
+
try {
|
|
72
|
+
if (source === null) {
|
|
73
|
+
const referer = headers.get("referer");
|
|
74
|
+
if (referer === null) return false;
|
|
75
|
+
const candidate = new URL(referer);
|
|
76
|
+
return candidate.origin === origin.origin && !candidate.username && !candidate.password;
|
|
77
|
+
}
|
|
78
|
+
const candidate = new URL(source);
|
|
79
|
+
return candidate.origin === origin.origin && candidate.href === `${candidate.origin}/` && !candidate.username && !candidate.password;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function authority(value, protocol) {
|
|
85
|
+
if (value === "" || /[\s/\\?#@,%]/u.test(value)) throw new TypeError("Invalid authority.");
|
|
86
|
+
const url = new URL(`${protocol}//${value}/`);
|
|
87
|
+
if (url.username || url.password || url.pathname !== "/" || !url.hostname) throw new TypeError("Invalid authority.");
|
|
88
|
+
url.hostname = url.hostname.replace(/\.$/u, "");
|
|
89
|
+
return url.host;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { prepareWebRequest };
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=request-policy.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-policy.mjs","names":[],"sources":["../src/request-policy.ts"],"sourcesContent":["// DrFed: A web-based platform for developing and debugging ActivityPub apps\n// Copyright (C) 2026 DrFed team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\n\n/** Validate authority and cookie-bearing server functions before body parsing.\n * @returns A request with its public URL restored, or an early rejection.\n */\nexport function prepareWebRequest(\n request: Request,\n publicOrigin: string,\n): Request | Response {\n const origin = new URL(publicOrigin);\n let validated: URL | Response;\n try {\n validated = validateAuthority(request, origin);\n } catch {\n return new Response(\"Invalid request authority.\", { status: 400 });\n }\n if (validated instanceof Response) return validated;\n const incoming = validated;\n let serverFunction: boolean;\n try {\n serverFunction = isServerFunctionPath(incoming.pathname, origin);\n } catch {\n return new Response(\"Invalid request path.\", { status: 400 });\n }\n // SolidStart dispatches every pathname beginning with this prefix.\n if (serverFunction) {\n if (request.method !== \"POST\") {\n return new Response(\"Method not allowed.\", {\n status: 405,\n headers: { allow: \"POST\" },\n });\n }\n if (!sameOriginSource(request.headers, origin)) {\n return new Response(\"Forbidden server function request.\", {\n status: 403,\n });\n }\n }\n const target = new URL(incoming.href);\n target.protocol = origin.protocol;\n target.host = origin.host;\n const headers = new Headers(request.headers);\n headers.set(\"host\", origin.host);\n for (const name of [\n \"forwarded\",\n \"x-forwarded-host\",\n \"x-forwarded-proto\",\n \"x-forwarded-port\",\n \"x-forwarded-for\",\n ]) {\n headers.delete(name);\n }\n const init: RequestInit & { duplex: \"half\" } = {\n method: request.method,\n headers,\n signal: request.signal,\n duplex: \"half\",\n };\n if (request.method !== \"GET\" && request.method !== \"HEAD\") {\n init.body = request.body;\n }\n return new Request(target, init);\n}\n\nfunction validateAuthority(request: Request, origin: URL): URL | Response {\n const incoming = new URL(request.url);\n if (\n ![\"http:\", \"https:\"].includes(incoming.protocol) ||\n incoming.username ||\n incoming.password\n ) {\n throw new TypeError(\"Invalid request URL.\");\n }\n const host = request.headers.get(\"host\");\n if (\n host !== null &&\n authority(host, incoming.protocol) !==\n authority(incoming.host, incoming.protocol)\n ) {\n throw new TypeError(\"Inconsistent request authority.\");\n }\n if (\n authority(host ?? incoming.host, origin.protocol) !==\n authority(origin.host, origin.protocol)\n ) {\n return new Response(\"Misdirected request.\", { status: 421 });\n }\n return incoming;\n}\n\nfunction isServerFunctionPath(value: string, origin: URL): boolean {\n let path = value;\n let serverFunction = path.startsWith(\"/_server\");\n for (let depth = 0; depth < 5; depth += 1) {\n const decoded = decodeURIComponent(path).replaceAll(\"\\\\\", \"/\");\n if (decoded === path) break;\n path = decoded;\n serverFunction ||= path.startsWith(\"/_server\");\n if (depth === 4) throw new TypeError(\"Ambiguous encoded path.\");\n }\n return (\n serverFunction ||\n new URL(`/${path.replace(/^\\/+/u, \"\")}`, origin).pathname.startsWith(\n \"/_server\",\n )\n );\n}\n\nfunction sameOriginSource(headers: Headers, origin: URL): boolean {\n const source = headers.get(\"origin\");\n try {\n if (source === null) {\n const referer = headers.get(\"referer\");\n if (referer === null) return false;\n const candidate = new URL(referer);\n return (\n candidate.origin === origin.origin &&\n !candidate.username &&\n !candidate.password\n );\n }\n const candidate = new URL(source);\n return (\n candidate.origin === origin.origin &&\n candidate.href === `${candidate.origin}/` &&\n !candidate.username &&\n !candidate.password\n );\n } catch {\n return false;\n }\n}\n\nfunction authority(value: string, protocol: string): string {\n if (value === \"\" || /[\\s/\\\\?#@,%]/u.test(value)) {\n throw new TypeError(\"Invalid authority.\");\n }\n const url = new URL(`${protocol}//${value}/`);\n if (url.username || url.password || url.pathname !== \"/\" || !url.hostname) {\n throw new TypeError(\"Invalid authority.\");\n }\n url.hostname = url.hostname.replace(/\\.$/u, \"\");\n return url.host;\n}\n"],"mappings":";;;;AAmBA,SAAgB,kBACd,SACA,cACoB;CACpB,MAAM,SAAS,IAAI,IAAI,YAAY;CACnC,IAAI;CACJ,IAAI;EACF,YAAY,kBAAkB,SAAS,MAAM;CAC/C,QAAQ;EACN,OAAO,IAAI,SAAS,8BAA8B,EAAE,QAAQ,IAAI,CAAC;CACnE;CACA,IAAI,qBAAqB,UAAU,OAAO;CAC1C,MAAM,WAAW;CACjB,IAAI;CACJ,IAAI;EACF,iBAAiB,qBAAqB,SAAS,UAAU,MAAM;CACjE,QAAQ;EACN,OAAO,IAAI,SAAS,yBAAyB,EAAE,QAAQ,IAAI,CAAC;CAC9D;CAEA,IAAI,gBAAgB;EAClB,IAAI,QAAQ,WAAW,QACrB,OAAO,IAAI,SAAS,uBAAuB;GACzC,QAAQ;GACR,SAAS,EAAE,OAAO,OAAO;EAC3B,CAAC;EAEH,IAAI,CAAC,iBAAiB,QAAQ,SAAS,MAAM,GAC3C,OAAO,IAAI,SAAS,sCAAsC,EACxD,QAAQ,IACV,CAAC;CAEL;CACA,MAAM,SAAS,IAAI,IAAI,SAAS,IAAI;CACpC,OAAO,WAAW,OAAO;CACzB,OAAO,OAAO,OAAO;CACrB,MAAM,UAAU,IAAI,QAAQ,QAAQ,OAAO;CAC3C,QAAQ,IAAI,QAAQ,OAAO,IAAI;CAC/B,KAAK,MAAM,QAAQ;EACjB;EACA;EACA;EACA;EACA;CACF,GACE,QAAQ,OAAO,IAAI;CAErB,MAAM,OAAyC;EAC7C,QAAQ,QAAQ;EAChB;EACA,QAAQ,QAAQ;EAChB,QAAQ;CACV;CACA,IAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,QACjD,KAAK,OAAO,QAAQ;CAEtB,OAAO,IAAI,QAAQ,QAAQ,IAAI;AACjC;AAEA,SAAS,kBAAkB,SAAkB,QAA6B;CACxE,MAAM,WAAW,IAAI,IAAI,QAAQ,GAAG;CACpC,IACE,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,SAAS,SAAS,QAAQ,KAC/C,SAAS,YACT,SAAS,UAET,MAAM,IAAI,UAAU,sBAAsB;CAE5C,MAAM,OAAO,QAAQ,QAAQ,IAAI,MAAM;CACvC,IACE,SAAS,QACT,UAAU,MAAM,SAAS,QAAQ,MAC/B,UAAU,SAAS,MAAM,SAAS,QAAQ,GAE5C,MAAM,IAAI,UAAU,iCAAiC;CAEvD,IACE,UAAU,QAAQ,SAAS,MAAM,OAAO,QAAQ,MAChD,UAAU,OAAO,MAAM,OAAO,QAAQ,GAEtC,OAAO,IAAI,SAAS,wBAAwB,EAAE,QAAQ,IAAI,CAAC;CAE7D,OAAO;AACT;AAEA,SAAS,qBAAqB,OAAe,QAAsB;CACjE,IAAI,OAAO;CACX,IAAI,iBAAiB,KAAK,WAAW,UAAU;CAC/C,KAAK,IAAI,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG;EACzC,MAAM,UAAU,mBAAmB,IAAI,CAAC,CAAC,WAAW,MAAM,GAAG;EAC7D,IAAI,YAAY,MAAM;EACtB,OAAO;EACP,mBAAmB,KAAK,WAAW,UAAU;EAC7C,IAAI,UAAU,GAAG,MAAM,IAAI,UAAU,yBAAyB;CAChE;CACA,OACE,kBACA,IAAI,IAAI,IAAI,KAAK,QAAQ,SAAS,EAAE,KAAK,MAAM,CAAC,CAAC,SAAS,WACxD,UACF;AAEJ;AAEA,SAAS,iBAAiB,SAAkB,QAAsB;CAChE,MAAM,SAAS,QAAQ,IAAI,QAAQ;CACnC,IAAI;EACF,IAAI,WAAW,MAAM;GACnB,MAAM,UAAU,QAAQ,IAAI,SAAS;GACrC,IAAI,YAAY,MAAM,OAAO;GAC7B,MAAM,YAAY,IAAI,IAAI,OAAO;GACjC,OACE,UAAU,WAAW,OAAO,UAC5B,CAAC,UAAU,YACX,CAAC,UAAU;EAEf;EACA,MAAM,YAAY,IAAI,IAAI,MAAM;EAChC,OACE,UAAU,WAAW,OAAO,UAC5B,UAAU,SAAS,GAAG,UAAU,OAAO,MACvC,CAAC,UAAU,YACX,CAAC,UAAU;CAEf,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,UAAU,OAAe,UAA0B;CAC1D,IAAI,UAAU,MAAM,gBAAgB,KAAK,KAAK,GAC5C,MAAM,IAAI,UAAU,oBAAoB;CAE1C,MAAM,MAAM,IAAI,IAAI,GAAG,SAAS,IAAI,MAAM,EAAE;CAC5C,IAAI,IAAI,YAAY,IAAI,YAAY,IAAI,aAAa,OAAO,CAAC,IAAI,UAC/D,MAAM,IAAI,UAAU,oBAAoB;CAE1C,IAAI,WAAW,IAAI,SAAS,QAAQ,QAAQ,EAAE;CAC9C,OAAO,IAAI;AACb"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GraphqlFetch, WebRuntimeOptions } from "./bridge.mjs";
|
|
2
|
+
//#region src/runtime.d.ts
|
|
3
|
+
/** One embedded SolidStart server, without a listening socket. */
|
|
4
|
+
interface WebRuntime {
|
|
5
|
+
readonly fetch: (request: Request) => Promise<Response>;
|
|
6
|
+
readonly close: () => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/** Initialize the process's single embedded web server.
|
|
9
|
+
* @returns The shared embedded handler and idempotent cleanup.
|
|
10
|
+
*/
|
|
11
|
+
declare function createWebRuntime(options: WebRuntimeOptions): Promise<WebRuntime>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { type GraphqlFetch, WebRuntime, type WebRuntimeOptions, createWebRuntime };
|
|
14
|
+
//# sourceMappingURL=runtime.d.mts.map
|
package/dist/runtime.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { installWebBinding, prepareWebRequest } from "./bridge.mjs";
|
|
2
|
+
//#region src/runtime.ts
|
|
3
|
+
/** Initialize the process's single embedded web server.
|
|
4
|
+
* @returns The shared embedded handler and idempotent cleanup.
|
|
5
|
+
*/
|
|
6
|
+
async function createWebRuntime(options) {
|
|
7
|
+
const owner = installWebBinding(options);
|
|
8
|
+
let nitro;
|
|
9
|
+
try {
|
|
10
|
+
const loaded = await import(new URL("../.output/server/index.mjs", import.meta.url).href);
|
|
11
|
+
if (!isNitroRuntime(loaded)) throw new Error("Invalid bundled web runtime.");
|
|
12
|
+
nitro = loaded;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
await owner.close();
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
let closing;
|
|
18
|
+
return {
|
|
19
|
+
async fetch(request) {
|
|
20
|
+
if (owner.signal.aborted) return new Response("Web runtime is closed.", { status: 503 });
|
|
21
|
+
const prepared = prepareWebRequest(request);
|
|
22
|
+
if (prepared instanceof Response) return prepared;
|
|
23
|
+
return nitro.fetch(new Request(prepared, { signal: AbortSignal.any([request.signal, owner.signal]) }));
|
|
24
|
+
},
|
|
25
|
+
close() {
|
|
26
|
+
closing ??= (async () => {
|
|
27
|
+
await Promise.resolve();
|
|
28
|
+
try {
|
|
29
|
+
await owner.close();
|
|
30
|
+
} finally {
|
|
31
|
+
await nitro.close();
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
return closing;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function isNitroRuntime(value) {
|
|
39
|
+
return typeof value === "object" && value !== null && "fetch" in value && typeof value.fetch === "function" && "close" in value && typeof value.close === "function";
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { createWebRuntime };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=runtime.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.mjs","names":[],"sources":["../src/runtime.ts"],"sourcesContent":["// DrFed: A web-based platform for developing and debugging ActivityPub apps\n// Copyright (C) 2026 DrFed team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\n\nimport {\n type WebRuntimeOptions,\n installWebBinding,\n prepareWebRequest,\n} from \"./bridge.ts\";\n\nexport type { GraphqlFetch, WebRuntimeOptions } from \"./bridge.ts\";\n\n/** One embedded SolidStart server, without a listening socket. */\nexport interface WebRuntime {\n readonly fetch: (request: Request) => Promise<Response>;\n readonly close: () => Promise<void>;\n}\n\ninterface NitroRuntime {\n fetch: (request: Request) => Response | Promise<Response>;\n close: () => Promise<void>;\n}\n\n/** Initialize the process's single embedded web server.\n * @returns The shared embedded handler and idempotent cleanup.\n */\nexport async function createWebRuntime(\n options: WebRuntimeOptions,\n): Promise<WebRuntime> {\n const owner = installWebBinding(options);\n let nitro: NitroRuntime;\n try {\n const loaded: unknown = await import(\n new URL(\"../.output/server/index.mjs\", import.meta.url).href\n );\n if (!isNitroRuntime(loaded)) {\n throw new Error(\"Invalid bundled web runtime.\");\n }\n nitro = loaded;\n } catch (error) {\n await owner.close();\n throw error;\n }\n let closing: Promise<void> | undefined;\n return {\n async fetch(request) {\n if (owner.signal.aborted) {\n return new Response(\"Web runtime is closed.\", { status: 503 });\n }\n const prepared = prepareWebRequest(request);\n if (prepared instanceof Response) return prepared;\n return nitro.fetch(\n new Request(prepared, {\n signal: AbortSignal.any([request.signal, owner.signal]),\n }),\n );\n },\n // oxlint-disable-next-line typescript/promise-function-async\n close(): Promise<void> {\n closing ??= (async () => {\n await Promise.resolve();\n try {\n await owner.close();\n } finally {\n await nitro.close();\n }\n })();\n return closing;\n },\n };\n}\n\nfunction isNitroRuntime(value: unknown): value is NitroRuntime {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"fetch\" in value &&\n typeof value.fetch === \"function\" &&\n \"close\" in value &&\n typeof value.close === \"function\"\n );\n}\n"],"mappings":";;;;;AAsCA,eAAsB,iBACpB,SACqB;CACrB,MAAM,QAAQ,kBAAkB,OAAO;CACvC,IAAI;CACJ,IAAI;EACF,MAAM,SAAkB,MAAM,OAC5B,IAAI,IAAI,+BAA+B,OAAO,KAAK,GAAG,CAAC,CAAC;EAE1D,IAAI,CAAC,eAAe,MAAM,GACxB,MAAM,IAAI,MAAM,8BAA8B;EAEhD,QAAQ;CACV,SAAS,OAAO;EACd,MAAM,MAAM,MAAM;EAClB,MAAM;CACR;CACA,IAAI;CACJ,OAAO;EACL,MAAM,MAAM,SAAS;GACnB,IAAI,MAAM,OAAO,SACf,OAAO,IAAI,SAAS,0BAA0B,EAAE,QAAQ,IAAI,CAAC;GAE/D,MAAM,WAAW,kBAAkB,OAAO;GAC1C,IAAI,oBAAoB,UAAU,OAAO;GACzC,OAAO,MAAM,MACX,IAAI,QAAQ,UAAU,EACpB,QAAQ,YAAY,IAAI,CAAC,QAAQ,QAAQ,MAAM,MAAM,CAAC,EACxD,CAAC,CACH;EACF;EAEA,QAAuB;GACrB,aAAa,YAAY;IACvB,MAAM,QAAQ,QAAQ;IACtB,IAAI;KACF,MAAM,MAAM,MAAM;IACpB,UAAU;KACR,MAAM,MAAM,MAAM;IACpB;GACF,EAAA,CAAG;GACH,OAAO;EACT;CACF;AACF;AAEA,SAAS,eAAe,OAAuC;CAC7D,OACE,OAAO,UAAU,YACjB,UAAU,QACV,WAAW,SACX,OAAO,MAAM,UAAU,cACvB,WAAW,SACX,OAAO,MAAM,UAAU;AAE3B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,114 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drfed/web",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0-dev.0",
|
|
4
|
+
"description": "A SolidStart frontend for DrFed.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ActivityPub",
|
|
7
|
+
"fediverse",
|
|
8
|
+
"federation",
|
|
9
|
+
"debugger"
|
|
10
|
+
],
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "DrFed team",
|
|
13
|
+
"url": "https://drfed.org/"
|
|
14
|
+
},
|
|
15
|
+
"maintainers": [
|
|
16
|
+
{
|
|
17
|
+
"name": "ChanHaeng Lee",
|
|
18
|
+
"email": "2chanhaeng@gmail.com",
|
|
19
|
+
"url": "https://chomu.dev/"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "Hong Minhee",
|
|
23
|
+
"email": "hong@minhee.org",
|
|
24
|
+
"url": "https://hongminhee.org/"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "Hyeonseo Kim",
|
|
28
|
+
"email": "dodok8@gmail.com",
|
|
29
|
+
"url": "https://hackers.pub/@gaebalgom"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "Jiwon Kwon",
|
|
33
|
+
"email": "work@kwonjiwon.org",
|
|
34
|
+
"url": "https://kwonjiwon.org/"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"license": "AGPL-3.0-only",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=26.0.0"
|
|
40
|
+
},
|
|
41
|
+
"type": "module",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@faker-js/faker": "^10.5.0",
|
|
44
|
+
"@formisch/solid": "1.0.0",
|
|
45
|
+
"@kobalte/core": "^0.13.13",
|
|
46
|
+
"@solidjs/meta": "^0.29.4",
|
|
47
|
+
"@solidjs/router": "^1.0.0",
|
|
48
|
+
"@solidjs/start": "^2.0.0",
|
|
49
|
+
"nitro": "3.0.260610-beta",
|
|
50
|
+
"relay-runtime": "^21.0.1",
|
|
51
|
+
"seroval": "1.6.3",
|
|
52
|
+
"shiki": "4.4.1",
|
|
53
|
+
"solid-js": "^1.9.5",
|
|
54
|
+
"solid-relay": "1.0.0-beta.29",
|
|
55
|
+
"valibot": "1.4.2",
|
|
56
|
+
"vite": "^8.2.0",
|
|
57
|
+
"@types/relay-runtime": "^20.1.1"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"eslint-plugin-solid": "^0.14.5",
|
|
61
|
+
"relay-compiler": "^21.0.1",
|
|
62
|
+
"typescript": "^7.0.2",
|
|
63
|
+
"vite-plugin-cjs-interop": "^4.0.3",
|
|
64
|
+
"vite-plugin-relay-lite": "^0.12.0",
|
|
65
|
+
"tsdown": "^0.22.14",
|
|
66
|
+
"@types/node": "^26.0.0"
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"dist/",
|
|
70
|
+
".output/",
|
|
71
|
+
"README.md"
|
|
72
|
+
],
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public"
|
|
75
|
+
},
|
|
76
|
+
"main": "dist/runtime.mjs",
|
|
77
|
+
"types": "dist/runtime.d.mts",
|
|
78
|
+
"exports": {
|
|
79
|
+
".": {
|
|
80
|
+
"types": "./dist/runtime.d.mts",
|
|
81
|
+
"default": "./dist/runtime.mjs"
|
|
82
|
+
},
|
|
83
|
+
"./bridge": {
|
|
84
|
+
"types": "./dist/bridge.d.mts",
|
|
85
|
+
"default": "./dist/bridge.mjs"
|
|
86
|
+
},
|
|
87
|
+
"./request-policy": {
|
|
88
|
+
"types": "./dist/request-policy.d.mts",
|
|
89
|
+
"default": "./dist/request-policy.mjs"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"tsdown": {
|
|
93
|
+
"entry": [
|
|
94
|
+
"src/runtime.ts",
|
|
95
|
+
"src/bridge.ts",
|
|
96
|
+
"src/request-policy.ts"
|
|
97
|
+
],
|
|
98
|
+
"dts": {
|
|
99
|
+
"tsconfig": "tsconfig.runtime.json"
|
|
100
|
+
},
|
|
101
|
+
"sourcemap": true
|
|
102
|
+
},
|
|
103
|
+
"repository": {
|
|
104
|
+
"type": "git",
|
|
105
|
+
"url": "https://github.com/fedify-dev/drfed.git",
|
|
106
|
+
"directory": "packages/web"
|
|
107
|
+
},
|
|
108
|
+
"scripts": {
|
|
109
|
+
"dev": "vite dev",
|
|
110
|
+
"build": "tsdown && vite build",
|
|
111
|
+
"preview": "vite preview",
|
|
112
|
+
"test": "node --test"
|
|
113
|
+
}
|
|
6
114
|
}
|