FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
JavaScriptSolidServer/src/plugins.js at gh-pages · JavaScriptSolidServer/JavaScriptSolidServer · GitHub
JavaScriptSolidServer
JavaScriptSolidServer
Repository navigation
Code
Issues
164
(164)
Pull requests
14
(14)
Actions
Projects
Security and quality
Insights
Expand file tree
Breadcrumbs
JavaScriptSolidServer
/
src
/
plugins.js
Copy path
More file actions
More file actions
Latest commit
History
History
History
463 lines (445 loc) · 22.3 KB
Breadcrumbs
JavaScriptSolidServer
/
src
/
plugins.js
Copy path
File metadata and controls
463 lines (445 loc) · 22.3 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
/**
* Plugin loader — the #206 seam, assembled from its shipped parts.
*
* createServer({
* plugins: [
* { module: 'tideholm/jss-plugin/tideholm-jss.js', prefix: '/tideholm',
* config: { bots: 8 }
},
* { module: './my-app/plugin.js', prefix: '/myapp' },
* ],
* })
*
* Each entry's module is imported and its exported `activate(api)` called
* during server startup (before listen completes). The api wires the seams
* every plugin consumer so far has needed:
*
* api.fastify scoped Fastify instance to register routes on
* api.prefix the entry's mount prefix ('' when none)
* api.config the entry's config object, verbatim
* api.log server logger
* api.auth.getAgent(req) -> agent id string | null (#584)
* api.storage.pluginDir() -> private server-side data dir for this plugin
* api.serverInfo() -> { baseUrl, protocol, host, port, listening } (#601)
* api.reservePath(path) claim + WAC-exempt a protocol-pinned path (#602)
* api.plugins -> [{ id, prefix, module }] of every loaded entry (#610)
* api.ws.route(path, (socket, request) => {}) (#588)
*
* The entry's `prefix` is added to appPaths automatically (#582), so the
* plugin owns authentication and authorization under its mount — the same
* deal the bundled pseudo-plugins (idp, /db, /storage/…) already have.
*
* ws.route registers WebSocket endpoints through @fastify/websocket — the
* same single upgrade path the bundled realtime features (nostr relay,
* tunnel, notifications…) use — so plugins never attach their own 'upgrade'
* listener. That matters: node only auto-destroys stray upgrade attempts
* while the server has NO 'upgrade' listener, so a plugin attaching one
* would become responsible for every unclaimed socket on the host (#588).
* The handler receives the raw ws socket; a plugin with its own
* WebSocketServer({ noServer: true }) can feed it straight in:
* api.ws.route('/myapp/ws', (socket, req) => wss.emit('connection', socket, req));
*
* activate() may return { deactivate() {}
}; deactivate runs on server
* close (world saves, timer teardown). A plugin that fails to load fails
* the boot loudly — the operator wrote the config, and a server silently
* missing an app is worse than one that refuses to start.
*/
import
fs
from
'fs'
;
import
path
from
'path'
;
import
{
pathToFileURL
}
from
'url'
;
import
websocket
from
'@fastify/websocket'
;
import
{
getAgent
}
from
'../auth.js'
;
/**
* api.log speaks both dialects: pino-style (info/warn/error/debug, what
* fastify.log is) and console-style (log/error, what plain node apps
* expect) — plugins shouldn't need to know which logger the host runs.
*/
export
function
makePluginLog
(
base
)
{
const
call
=
(
level
)
=>
(
...
args
)
=>
{
const
fn
=
base
?.
[
level
]
??
base
?.
info
??
base
?.
log
;
if
(
typeof
fn
===
'function'
)
fn
.
call
(
base
,
...
args
)
;
}
;
return
{
log
:
call
(
'info'
)
,
info
:
call
(
'info'
)
,
warn
:
call
(
'warn'
)
,
error
:
call
(
'error'
)
,
debug
:
call
(
'debug'
)
}
;
}
/**
* Compile a parameterized reservation like '/:user/did.json' (#602) to a
* matcher. ':name' segments match one path segment; everything else is
* literal. Parameterized reservations match the exact path shape (plus an
* optional query string) — NOT a subtree — because they exist for pinned
* documents inside otherwise WAC-governed namespaces (did:web), where
* exempting a whole subtree would be a WAC bypass.
*/
// Only a segment that is ENTIRELY ':name' is a parameter. '/:user.json'
// is literal (its own segment mixes a param sigil with literal text) —
// treating it as a wildcard would exempt a far broader set of URLs than
// the ':name'-per-segment contract promises.
const
PARAM_SEGMENT
=
/
^
:
[
A
-
Z
a
-
z
_
]
[
A
-
Z
a
-
z
0
-
9
_
]
*
$
/
;
export
function
isParamPath
(
p
)
{
return
p
.
split
(
'/'
)
.
some
(
(
seg
)
=>
PARAM_SEGMENT
.
test
(
seg
)
)
;
}
export
function
compilePathPattern
(
p
)
{
const
pattern
=
p
.
split
(
'/'
)
// Exclude '?' as well as '/': a param must be a single PATH segment,
// so it must not swallow the query delimiter — otherwise a query
// containing '/' (e.g. /alice?x=a/b) would fail to match a shape the
// path part satisfies, making matching depend on query contents.
.
map
(
(
seg
)
=>
(
PARAM_SEGMENT
.
test
(
seg
)
?
'[^/?]+'
:
seg
.
replace
(
/
[
.
*
+
?
^
$
{
}
(
)
|
[
\]
\\
]
/
g
,
'\\$&'
)
)
)
.
join
(
'/'
)
;
return
new
RegExp
(
`^
${
pattern
}
(?:\\?.*)?$`
)
;
}
/**
* A literal reservation ('/xrpc') claims its whole subtree — the path
* itself, everything under it, and either plus a query string.
*/
export
function
compileSubtreePattern
(
p
)
{
const
esc
=
p
.
replace
(
/
[
.
*
+
?
^
$
{
}
(
)
|
[
\]
\\
]
/
g
,
'\\$&'
)
;
return
new
RegExp
(
`^
${
esc
}
(?:/.*)?(?:\\?.*)?$`
)
;
}
// Collision key: two reservations conflict when they exempt the same
// URLs, so parameter NAMES don't matter — '/:user/did.json' and
// '/:acct/did.json' are the same claim. Each segment is tagged by TYPE
// (param vs literal) so a canonicalized param can't alias a literal
// segment that happens to be the placeholder text (e.g. a literal '/:/…').
// '\x00' never occurs in a real path, so it's a safe framing prefix.
export
function
reservationKey
(
p
)
{
return
p
.
split
(
'/'
)
.
map
(
(
seg
)
=>
(
PARAM_SEGMENT
.
test
(
seg
)
?
'\x00P'
:
`\x00L
${
seg
}
`
)
)
.
join
(
'/'
)
;
}
/** Same normalization appPaths applies: no trailing slash, must be '/x…'. */
export
function
normalizePrefix
(
p
)
{
if
(
typeof
p
!==
'string'
)
return
''
;
const
trimmed
=
p
.
trim
(
)
.
replace
(
/
\/
+
$
/
,
''
)
;
return
trimmed
.
startsWith
(
'/'
)
&&
trimmed
.
length
>
1
?
trimmed
:
''
;
}
// Basenames too generic to name a plugin: the near-universal
// '<name>/plugin.js' convention means every such file would derive the same
// id and collide. For these, the id falls back to the parent directory (#596).
const
GENERIC_BASENAMES
=
new
Set
(
[
'plugin'
,
'index'
]
)
;
/**
* The distinguishing name of a plugin FILE: its basename, unless that is
* generic ('plugin.js', 'index.js'), in which case the immediate parent
* directory ('relay/plugin.js' -> 'relay'). The parent directory
* distinguishes '<name>/plugin.js' files from each other WITHOUT pinning the
* machine-specific path prefix (which would move the id — and pluginDir —
* with the deployment). Falls back to the basename when there is no usable
* parent (e.g. './plugin.js' at the cwd root). (#596)
*/
export
function
fileStem
(
module
)
{
const
withoutExt
=
path
.
basename
(
module
)
.
replace
(
/
\.
[
c
m
]
?
j
s
$
/
,
''
)
;
if
(
GENERIC_BASENAMES
.
has
(
withoutExt
.
toLowerCase
(
)
)
)
{
const
parent
=
path
.
basename
(
path
.
dirname
(
module
)
)
;
// The parent directory name is returned AS-IS — a directory legitimately
// named 'foo.js' must stay 'foo.js', not be extension-stripped to 'foo'.
if
(
parent
&&
parent
!==
'.'
&&
parent
!==
'..'
)
return
parent
;
}
return
withoutExt
;
}
/**
* Directory-safe plugin id: entry.id, or derived from the module spec.
* Bare package specifiers keep their full path ('@scope/pkg/plugin.js' ->
* 'scope-pkg-plugin') so same-named files in different packages don't
* collide; file paths use the basename — or, for a generic basename, the
* parent directory (see fileStem, #596) — because a machine-specific
* directory prefix must not name the plugin's data dir (the id — and with
* it pluginDir — would change whenever the deployment moves). The loader
* additionally rejects duplicate ids, so any residual collision fails the
* boot instead of silently sharing storage.
*/
export
function
pluginId
(
spec
)
{
const
module
=
String
(
spec
.
module
)
;
// The extension is stripped where it applies — the file basename (via
// fileStem) and a bare specifier's tail — but NOT a parent directory name
// that fileStem may return for a generic basename.
let
raw
;
if
(
typeof
spec
.
id
===
'string'
&&
spec
.
id
)
{
raw
=
spec
.
id
.
replace
(
/
\.
[
c
m
]
?
j
s
$
/
,
''
)
;
}
else
if
(
module
.
startsWith
(
'.'
)
||
path
.
isAbsolute
(
module
)
)
{
raw
=
fileStem
(
module
)
;
}
else
{
raw
=
module
.
replace
(
/
\.
[
c
m
]
?
j
s
$
/
,
''
)
;
}
const
id
=
raw
.
toLowerCase
(
)
.
replace
(
/
[
^
a
-
z
0
-
9
_
-
]
+
/
g
,
'-'
)
.
replace
(
/
^
-
+
|
-
+
$
/
g
,
''
)
;
if
(
!
id
)
throw
new
Error
(
`plugins: cannot derive an id from
${
JSON
.
stringify
(
spec
.
module
)
}
; set entry.id`
)
;
return
id
;
}
/**
* Load and activate every plugin entry. Called from createServer inside a
* fastify.register scope, so `fastify` here is that scope; routes and hooks
* plugins add land on the running server.
*
*
@param
{
object
} fastify scoped instance the plugins register on
*
@param
{
Array
} entries options.plugins, verbatim
*
@param
{
object
} ctx { appPaths, root, log }
*/
export
async
function
loadPlugins
(
fastify
,
entries
,
ctx
)
{
const
log
=
makePluginLog
(
ctx
.
log
)
;
const
seenIds
=
new
Set
(
)
;
// reservePath claims (#602), shared across all entries of this load so
// two plugins reserving the same pinned path fail the boot with both
// names — loud beats the silent-loser outcome witnessed with webfinger
// vs remotestorage.
const
reservations
=
new
Map
(
)
;
// Read-only roster of every loaded entry (#610): { id, prefix, module }
// for each. Computed up front, before any activate() runs, so a plugin
// whose job is describing the deployment (a status dashboard, an admin
// console) sees the FULL set regardless of load order — rather than being
// hand-fed a duplicate of the operator's plugins array that silently
// drifts. Frozen so one plugin can't mutate another's view; it includes
// the plugin itself (consumers filter). Lenient here (never throws) — the
// load loop below owns validation and fails the boot on a bad entry, so
// on any successful boot every id/prefix matches what the loop derives.
// A boot-time snapshot: there is no runtime add/remove yet.
const
roster
=
Object
.
freeze
(
entries
.
map
(
(
entry
)
=>
{
const
spec
=
typeof
entry
===
'string'
?
{
module
:
entry
}
:
(
entry
??
{
}
)
;
// Only surface a well-formed module — a non-string/empty one makes the
// load loop below fail the boot, so don't fabricate a coerced value
// (e.g. "[object Object]") for the transient pre-failure window. Mirrors
// the loop's own `typeof spec.module === 'string'` gate; id is derived
// only from a valid module.
const
module
=
typeof
spec
.
module
===
'string'
&&
spec
.
module
?
spec
.
module
:
null
;
let
id
=
null
;
try
{
if
(
module
)
id
=
pluginId
(
spec
)
;
}
catch
{
/* the loop reports it */
}
return
Object
.
freeze
(
{
id
,
prefix
:
normalizePrefix
(
spec
.
prefix
)
,
module
}
)
;
}
)
)
;
for
(
const
entry
of
entries
)
{
const
spec
=
typeof
entry
===
'string'
?
{
module
:
entry
}
:
entry
;
if
(
!
spec
||
typeof
spec
.
module
!==
'string'
||
!
spec
.
module
)
{
throw
new
Error
(
'plugins: each entry needs a module (import specifier or path)'
)
;
}
const
id
=
pluginId
(
spec
)
;
if
(
seenIds
.
has
(
id
)
)
{
throw
new
Error
(
`plugins: duplicate id '
${
id
}
' — set entry.id to keep the plugins' data dirs apart`
)
;
}
seenIds
.
add
(
id
)
;
// Paths resolve from the operator's cwd; bare specifiers stay package
// imports resolved from JSS's own module graph.
const
href
=
spec
.
module
.
startsWith
(
'.'
)
||
path
.
isAbsolute
(
spec
.
module
)
?
pathToFileURL
(
path
.
resolve
(
spec
.
module
)
)
.
href
:
spec
.
module
;
let
mod
;
try
{
mod
=
await
import
(
href
)
;
}
catch
(
err
)
{
throw
new
Error
(
`plugin
${
id
}
: cannot import
${
spec
.
module
}
:
${
err
.
message
}
`
)
;
}
const
activate
=
mod
.
activate
??
mod
.
default
;
if
(
typeof
activate
!==
'function'
)
{
throw
new
Error
(
`plugin
${
id
}
: module exports no activate(api) function`
)
;
}
// Any provided prefix must validate — a falsy one ('', 0) silently
// skipping the appPaths exemption would mount the app behind WAC.
// Omit the property entirely for a plugin with no mount prefix.
const
prefix
=
normalizePrefix
(
spec
.
prefix
)
;
if
(
spec
.
prefix
!==
undefined
&&
!
prefix
)
{
throw
new
Error
(
`plugin
${
id
}
: invalid prefix
${
JSON
.
stringify
(
spec
.
prefix
)
}
(must start with '/'; omit for none)`
)
;
}
if
(
prefix
)
ctx
.
appPaths
.
push
(
prefix
)
;
// WAC exemption under the mount (#582)
const
api
=
{
fastify
,
prefix
,
config
:
spec
.
config
??
{
}
,
log
,
auth
:
{
getAgent
}
,
storage
:
{
// Under the data root's dot-guard (like .idp): never served over LDP.
pluginDir
(
)
{
const
dir
=
path
.
join
(
ctx
.
root
,
'.plugins'
,
id
)
;
fs
.
mkdirSync
(
dir
,
{
recursive
:
true
}
)
;
return
dir
;
}
,
}
,
// The server's own origin (#601) — for minting absolute URLs and
// loopback calls, so plugins stop repeating baseUrl in config where
// a wrong value fails quietly. A function, not a snapshot: with
// port 0 the real port exists only once the server is listening,
// so call it lazily (per request, or in an onListen hook via
// api.fastify) rather than caching the result during activate.
// An explicit idpIssuer is the deployment's canonical public origin
// and wins over the host:port derivation, mirroring the pod-seeding
// logic in server.js.
// Reserve a path the protocol pins outside the plugin's prefix
// (#602): fixed roots like /xrpc or /_matrix (literal — exempts the
// subtree), or parameterized documents like /:user/did.json
// (exact-shape match only; see compilePathPattern). Both are
// read-only by default and method-gated ({ methods } to widen) —
// see the exemption note below. The claim is deliberate and
// cross-plugin: a second plugin reserving the same path fails the
// boot naming both claimants, instead of one silently losing.
// Registering the routes is still the plugin's job via api.fastify.
reservePath
(
p
,
opts
=
{
}
)
{
// Tolerate a null/garbage opts (treat as omitted), but a provided
// non-array `methods` is a caller mistake worth a targeted error
// rather than an opaque TypeError from .map deep in activate.
const
options
=
opts
&&
typeof
opts
===
'object'
?
opts
:
{
}
;
if
(
options
.
methods
!==
undefined
&&
!
Array
.
isArray
(
options
.
methods
)
)
{
throw
new
Error
(
`plugin
${
id
}
: reservePath { methods } must be an array, got
${
JSON
.
stringify
(
options
.
methods
)
}
`
)
;
}
// Validate AFTER normalization: '//' or '/ ' normalize to '',
// and an empty string in appPaths would match every URL in the
// WAC hook — a one-call total WAC bypass.
const
key
=
typeof
p
===
'string'
?
p
.
trim
(
)
.
replace
(
/
\/
+
$
/
,
''
)
:
''
;
if
(
!
key
.
startsWith
(
'/'
)
||
key
.
length
<
2
)
{
throw
new
Error
(
`plugin
${
id
}
: reservePath needs an absolute path, got
${
JSON
.
stringify
(
p
)
}
`
)
;
}
// A reservation is a pathname, not a URL: '?'/'#' don't belong.
// A query in the string would be escaped into the matcher (tying
// the exemption to that exact query), and fragments never reach
// request.url — both silently produce a reservation that matches
// nothing real.
if
(
key
.
includes
(
'?'
)
||
key
.
includes
(
'#'
)
)
{
throw
new
Error
(
`plugin
${
id
}
: reservePath must be a pathname without '?' or '#', got
${
JSON
.
stringify
(
p
)
}
`
)
;
}
// Track by the shape, not the raw string: '/:user/did.json' and
// '/:acct/did.json' exempt the same URLs, so they're the same
// claim and must collide loudly like two literal reservations.
const
claim
=
reservationKey
(
key
)
;
const
holder
=
reservations
.
get
(
claim
)
;
if
(
holder
===
id
)
{
return
;
// idempotent re-claim — the matcher is already installed
}
if
(
holder
)
{
throw
new
Error
(
`plugin
${
id
}
: path '
${
key
}
' is already reserved by plugin '
${
holder
}
'`
)
;
}
reservations
.
set
(
claim
,
id
)
;
// Read-only by default for BOTH lanes: a reserved path is
// WAC-exempt, and the LDP write wildcards (PUT/POST/PATCH/DELETE
// '/*') sit underneath it. Exempting a write method the plugin
// has NOT implemented would let that write fall through to the
// LDP handlers as an unauthenticated storage write — the exact
// trap core installs 405 blocks for under /.well-known/*. Widen
// with { methods } when the protocol genuinely needs writes.
const
methods
=
new
Set
(
(
options
.
methods
??
[
'GET'
,
'HEAD'
,
'OPTIONS'
]
)
.
map
(
(
m
)
=>
String
(
m
)
.
toUpperCase
(
)
)
)
;
// Parameterized shapes match exactly (they live inside a
// WAC-governed pod namespace); literals claim their subtree.
const
re
=
isParamPath
(
key
)
?
compilePathPattern
(
key
)
:
compileSubtreePattern
(
key
)
;
ctx
.
appPathPatterns
?.
push
(
{
re
,
methods
}
)
;
log
.
info
(
`plugin
${
id
}
reserved
${
key
}
[
${
[
...
methods
]
.
join
(
', '
)
}
]`
)
;
}
,
serverInfo
(
)
{
const
o
=
ctx
.
origin
??
{
}
;
const
addr
=
fastify
.
server
?.
listening
?
fastify
.
server
.
address
(
)
:
null
;
const
live
=
addr
&&
typeof
addr
===
'object'
?
addr
:
null
;
// Once listening, the live bind wins over configured values —
// listen() may be called with a different host/port than
// createServer() was given (tests do exactly this).
const
port
=
live
?.
port
??
o
.
port
??
null
;
const
rawHost
=
live
?.
address
??
o
.
host
;
// Unspecified binds aren't callable addresses; report the
// loopback name instead. IPv6 literals need brackets in URLs.
const
host
=
!
rawHost
||
rawHost
===
'0.0.0.0'
||
rawHost
===
'::'
?
'localhost'
:
rawHost
;
const
protocol
=
o
.
ssl
?
'https'
:
'http'
;
const
urlHost
=
host
.
includes
(
':'
)
?
`[
${
host
}
]`
:
host
;
const
baseUrl
=
o
.
baseUrl
||
`
${
protocol
}
://
${
urlHost
}
:
${
port
}
`
;
return
{
baseUrl
,
protocol
,
host
,
port
,
listening
:
!
!
live
}
;
}
,
// Every loaded plugin's { id, prefix, module } (#610), read-only — so
// a plugin can enumerate its co-loaded siblings instead of being
// handed a copy of the operator's plugins array. Includes this
// plugin; consumers filter themselves out. A frozen boot-time
// snapshot (see `roster` above).
plugins
:
roster
,
// Mount a node-style (req, res) handler — a wrapped HTTP app, reverse
// proxy, or framework adapter — under the plugin's prefix (#583). This
// bundles the four things every such plugin needs and otherwise
// rediscovers: the appPaths WAC exemption (already applied above), a
// scoped pass-through content parser so the wrapped app receives an
// unconsumed body stream, reply.hijack() so Fastify releases the
// response, and registration on both the bare prefix and its subtree.
// Without the scoped parser, Fastify drains the request stream before
// the handler runs and any body-reading app hangs forever.
async
mountApp
(
handler
,
opts
=
{
}
)
{
if
(
typeof
handler
!==
'function'
)
{
throw
new
Error
(
`plugin
${
id
}
: mountApp(handler) needs a (req, res) function`
)
;
}
// Any provided prefix must validate — same rule as entry.prefix: a
// falsy normalization silently falling back to the entry prefix
// would mount the app (and WAC-exempt it) somewhere unexpected.
const
secondary
=
normalizePrefix
(
opts
.
prefix
)
;
if
(
opts
.
prefix
!==
undefined
&&
!
secondary
)
{
throw
new
Error
(
`plugin
${
id
}
: mountApp invalid prefix
${
JSON
.
stringify
(
opts
.
prefix
)
}
(must start with '/'; omit to use the entry prefix)`
)
;
}
const
mountPrefix
=
secondary
||
prefix
;
if
(
!
mountPrefix
)
{
throw
new
Error
(
`plugin
${
id
}
: mountApp needs a prefix (entry.prefix or opts.prefix)`
)
;
}
if
(
mountPrefix
!==
prefix
&&
!
ctx
.
appPaths
.
includes
(
mountPrefix
)
)
{
ctx
.
appPaths
.
push
(
mountPrefix
)
;
// exempt a secondary mount too
}
await
fastify
.
register
(
async
(
scope
)
=>
{
scope
.
removeAllContentTypeParsers
(
)
;
scope
.
addContentTypeParser
(
'*'
,
(
req
,
payload
,
done
)
=>
done
(
null
,
payload
)
)
;
// After hijack() Fastify sends nothing, so a handler bug must not
// hang the client or become an unhandled rejection (same contract
// as ws.route below): log, answer 500 if nothing went out yet,
// else drop the one affected socket.
const
fail
=
(
res
,
err
)
=>
{
log
.
error
(
{
err
}
,
`plugin
${
id
}
: mounted app handler failed`
)
;
if
(
!
res
.
headersSent
&&
!
res
.
writableEnded
)
{
res
.
statusCode
=
500
;
res
.
end
(
)
;
}
else
{
res
.
destroy
(
)
;
}
}
;
const
wrapped
=
(
request
,
reply
)
=>
{
reply
.
hijack
(
)
;
try
{
Promise
.
resolve
(
handler
(
request
.
raw
,
reply
.
raw
)
)
.
catch
(
(
err
)
=>
fail
(
reply
.
raw
,
err
)
)
;
}
catch
(
err
)
{
fail
(
reply
.
raw
,
err
)
;
}
}
;
scope
.
all
(
mountPrefix
,
wrapped
)
;
scope
.
all
(
mountPrefix
+
'/*'
,
wrapped
)
;
}
)
;
}
,
ws
:
{
async
route
(
wsPath
,
handler
)
{
if
(
typeof
wsPath
!==
'string'
||
!
wsPath
.
startsWith
(
'/'
)
)
{
throw
new
Error
(
`plugin
${
id
}
: ws.route path must start with '/'`
)
;
}
if
(
!
fastify
.
websocketServer
)
{
await
fastify
.
register
(
websocket
)
;
}
fastify
.
get
(
wsPath
,
{
websocket
:
true
}
,
(
connection
,
request
)
=>
{
//
@fastify
/websocket v8 hands a SocketStream; the ws socket is
// .socket. Later majors hand the socket directly — accept both.
const
socket
=
connection
.
socket
??
connection
;
// A plugin bug here must not become an unhandled rejection that
// takes the host down: log it and close the one affected socket.
try
{
Promise
.
resolve
(
handler
(
socket
,
request
)
)
.
catch
(
(
err
)
=>
{
log
.
error
(
{
err
}
,
`plugin
${
id
}
: ws handler failed`
)
;
socket
.
terminate
?.
(
)
;
}
)
;
}
catch
(
err
)
{
log
.
error
(
{
err
}
,
`plugin
${
id
}
: ws handler failed`
)
;
socket
.
terminate
?.
(
)
;
}
}
)
;
}
,
}
,
}
;
let
result
;
try
{
result
=
await
activate
(
api
)
;
}
catch
(
err
)
{
throw
new
Error
(
`plugin
${
id
}
: activate() failed:
${
err
.
message
}
`
)
;
}
if
(
result
&&
typeof
result
.
deactivate
===
'function'
)
{
fastify
.
addHook
(
'onClose'
,
async
(
)
=>
{
try
{
await
result
.
deactivate
(
)
;
}
catch
(
err
)
{
log
.
warn
(
`plugin
${
id
}
: deactivate() failed:
${
err
.
message
}
`
)
;
}
}
)
;
}
log
.
info
(
`plugin
${
id
}
active
${
prefix
?
` at
${
prefix
}
`
:
''
}
`
)
;
}
}
Back
|
FazBrowse Home
|
New Git URL