@seedcord/http 0.10.2 → 0.11.0-next.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/README.md +55 -9
- package/dist/index.d.mts +42 -57
- package/dist/index.mjs +144 -239
- package/dist/index.mjs.map +1 -1
- package/dist/shared-vsj1uwIH.d.mts +977 -0
- package/dist/{edge.index-DCP1BD6f.mjs → username-3UsqzTSd.mjs} +1634 -1519
- package/dist/username-3UsqzTSd.mjs.map +1 -0
- package/dist/workerd.index.d.mts +65 -0
- package/dist/workerd.index.mjs +177 -0
- package/dist/workerd.index.mjs.map +1 -0
- package/package.json +19 -27
- package/dist/Manifest-D5MEsTmH.d.mts +0 -485
- package/dist/edge.index-DCP1BD6f.mjs.map +0 -1
- package/dist/edge.index-DFszgCJp.d.mts +0 -502
- package/dist/edge.index.d.mts +0 -10
- package/dist/edge.index.mjs +0 -15
- package/dist/manifest.index.d.mts +0 -10
- package/dist/manifest.index.mjs +0 -26
- package/dist/manifest.index.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,153 +1,74 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import "
|
|
3
|
-
import { HmrManager,
|
|
4
|
-
import { SeedcordError, applicationIdFromToken, validateDiscordToken } from "@seedcord/errors/internal";
|
|
5
|
-
import { Logger, LoggerChannelRegistry } from "@seedcord/logger";
|
|
6
|
-
import { Envapter } from "envapt";
|
|
7
|
-
import { REST } from "@discordjs/rest";
|
|
8
|
-
import { Bus, InteractionKind } from "@seedcord/core";
|
|
1
|
+
import { A as Emojis, C as MessageContextMenuHandler, D as InteractionHandler, E as InteractionMiddleware, M as ModalFields, N as Plugin, O as RepliableHandler, S as AutocompleteHandler, T as SlashHandler, _ as StringMenuHandler, a as HmrModuleHandler, b as ButtonHandler, c as ArraySource, d as Subscriber, f as WebhookLog, g as SelectMenuHandler, h as RoleMenuHandler, i as HttpStartupTasks, k as ReplySender, l as CursorSource, m as MentionableMenuHandler, n as InteractionsService, o as version, p as ChannelMenuHandler, s as Gated, t as usernameOf, u as Paginator, v as UserMenuHandler, w as UserContextMenuHandler, x as ComponentHandler, y as ModalHandler } from "./username-3UsqzTSd.mjs";
|
|
2
|
+
import { DRAIN_TASK_TIMEOUT_MS, DRAIN_WINDOW_MS, HealthResponder, InFlight, ShutdownPhase, StartupPhase, setEnvaptBaseDir } from "@seedcord/core/node/internal";
|
|
3
|
+
import { HmrManager, bindBotColor, busLoggerOf, connectDevChannel, getDevChannel } from "@seedcord/core/internal";
|
|
9
4
|
import { SeedcordErrorCode, paint } from "@seedcord/errors";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
5
|
+
import { SeedcordError } from "@seedcord/errors/internal";
|
|
6
|
+
import { Logger } from "@seedcord/logger";
|
|
7
|
+
import { isPortInUse } from "@seedcord/utils/internal";
|
|
13
8
|
import { once } from "node:events";
|
|
14
9
|
import { createServer } from "node:http";
|
|
15
|
-
import { CoordinatedShutdown, CoordinatedStartup,
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { HmrModuleHandler } from "@seedcord/core/hmr";
|
|
20
|
-
import { traverseDirectory } from "@seedcord/utils/node";
|
|
21
|
-
import { Plugin as Plugin$1 } from "@seedcord/core/plugin";
|
|
10
|
+
import { CoordinatedShutdown, CoordinatedStartup as CoordinatedStartup$1, ServerHost } from "@seedcord/core/node";
|
|
11
|
+
import { HostAugmentTarget, HostListenHost, HostStartup, HostVersion, SeedcordBrand } from "@seedcord/types/internal";
|
|
12
|
+
import { Envapter } from "envapt";
|
|
13
|
+
import { WinstonConsoleSink, WinstonFileSink } from "@seedcord/logger/node";
|
|
22
14
|
|
|
23
|
-
export * from "@seedcord/
|
|
15
|
+
export * from "@seedcord/errors"
|
|
24
16
|
|
|
25
17
|
export * from "@seedcord/core"
|
|
26
18
|
|
|
27
|
-
export * from "@seedcord/
|
|
28
|
-
|
|
29
|
-
export * from "@seedcord/rate-limiter"
|
|
19
|
+
export * from "@seedcord/logger"
|
|
30
20
|
|
|
31
21
|
export * from "@seedcord/event-emitter"
|
|
32
22
|
|
|
23
|
+
export * from "@seedcord/rate-limiter"
|
|
24
|
+
|
|
33
25
|
export * from "@seedcord/utils"
|
|
34
26
|
|
|
35
|
-
//#region src/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
55
|
-
if (!Envapter.isDevelopment && !Envapter.isTest) return;
|
|
56
|
-
this.hmrHandler = new HmrModuleHandler({
|
|
57
|
-
handlersDir,
|
|
58
|
-
...middlewaresDir && { middlewaresDir },
|
|
59
|
-
isHandler: this.isHandler.bind(this),
|
|
60
|
-
isMiddleware: this.isMiddleware.bind(this),
|
|
61
|
-
registerHandler: this.registerHandler.bind(this),
|
|
62
|
-
registerMiddleware: this.registerMiddleware.bind(this),
|
|
63
|
-
unregisterHandler: this.routes.unregister.bind(this.routes),
|
|
64
|
-
unregisterMiddleware: this.unregisterMiddleware.bind(this),
|
|
65
|
-
getArtifacts: this.routes.routesOf.bind(this.routes),
|
|
66
|
-
logger: this.logger
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
/** @internal */
|
|
70
|
-
async init() {
|
|
71
|
-
if (this.isInitialized) return;
|
|
72
|
-
this.isInitialized = true;
|
|
73
|
-
this.loading = true;
|
|
74
|
-
this.loadedHandlers.length = 0;
|
|
75
|
-
this.loadedMiddlewares.length = 0;
|
|
76
|
-
try {
|
|
77
|
-
if (this.middlewaresDir) await this.loadMiddlewares(this.middlewaresDir);
|
|
78
|
-
await this.loadHandlers(this.handlersDir);
|
|
79
|
-
} finally {
|
|
80
|
-
this.loading = false;
|
|
81
|
-
}
|
|
82
|
-
this.reportLoad();
|
|
83
|
-
}
|
|
84
|
-
async loadHandlers(dir) {
|
|
85
|
-
await traverseDirectory(dir, (fullPath, relativePath, imported) => {
|
|
86
|
-
for (const value of Object.values(imported)) {
|
|
87
|
-
if (!this.isHandler(value)) continue;
|
|
88
|
-
this.registerHandler(value, relativePath);
|
|
89
|
-
this.hmrHandler?.trackHandler(fullPath, value);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
async loadMiddlewares(dir) {
|
|
94
|
-
await traverseDirectory(dir, (fullPath, relativePath, imported) => {
|
|
95
|
-
for (const value of Object.values(imported)) {
|
|
96
|
-
if (!this.isMiddleware(value)) continue;
|
|
97
|
-
this.registerMiddleware(value, relativePath);
|
|
98
|
-
this.hmrHandler?.trackMiddleware(fullPath, value);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
reportLoad() {
|
|
103
|
-
const { utils } = this.logger;
|
|
104
|
-
if (this.loadedMiddlewares.length > 0) utils.block("Loaded middlewares", utils.entries(this.loadedMiddlewares), "debug");
|
|
105
|
-
utils.block("Loaded handlers", [...utils.entries(this.loadedHandlers), ...utils.counts({ routes: this.routes.size })], "debug");
|
|
106
|
-
}
|
|
107
|
-
/** @internal */
|
|
108
|
-
async onHmr(event) {
|
|
109
|
-
await this.hmrHandler?.handle(event);
|
|
27
|
+
//#region src/env-bootstrap.ts
|
|
28
|
+
setEnvaptBaseDir();
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/node/NodeEndpoint.ts
|
|
32
|
+
const SERVICE_UNAVAILABLE = 503;
|
|
33
|
+
function unavailable() {
|
|
34
|
+
return new Response(null, { status: SERVICE_UNAVAILABLE });
|
|
35
|
+
}
|
|
36
|
+
var NodeEndpoint = class {
|
|
37
|
+
service;
|
|
38
|
+
health;
|
|
39
|
+
startup;
|
|
40
|
+
inFlight;
|
|
41
|
+
started;
|
|
42
|
+
constructor(service, logger, health, startup) {
|
|
43
|
+
this.service = service;
|
|
44
|
+
this.health = health;
|
|
45
|
+
this.startup = startup;
|
|
46
|
+
this.inFlight = new InFlight(logger, "Interactions");
|
|
110
47
|
}
|
|
111
|
-
|
|
112
|
-
this.
|
|
48
|
+
startWith(starting) {
|
|
49
|
+
this.started = starting.then(() => void 0, () => this.close());
|
|
113
50
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.
|
|
118
|
-
this.warnMissing(leaves.message, message, "Message context menu", "@ContextMenuRoute");
|
|
51
|
+
async fetch(request) {
|
|
52
|
+
if (!this.started) throw new SeedcordError(SeedcordErrorCode.CoreFetchBeforeStart);
|
|
53
|
+
await this.started;
|
|
54
|
+
return await this.answer(request);
|
|
119
55
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
56
|
+
async answer(request) {
|
|
57
|
+
if (this.inFlight.closed) return unavailable();
|
|
58
|
+
const health = this.health?.answer(request, this.startup.isReady);
|
|
59
|
+
if (health) return health;
|
|
60
|
+
const answering = this.service.engine(request, this);
|
|
61
|
+
this.inFlight.track(answering);
|
|
62
|
+
return answering;
|
|
125
63
|
}
|
|
126
|
-
|
|
127
|
-
|
|
64
|
+
waitUntil(work) {
|
|
65
|
+
this.inFlight.track(work);
|
|
128
66
|
}
|
|
129
|
-
|
|
130
|
-
|
|
67
|
+
close() {
|
|
68
|
+
this.inFlight.close();
|
|
131
69
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (!metadata || !this.loading) return;
|
|
135
|
-
const scope = metadata.keys ? `${metadata.priority}, ${metadata.keys.join(", ")}` : metadata.priority;
|
|
136
|
-
this.loadedMiddlewares.push({
|
|
137
|
-
name: `${ctor.name} (${String(scope)})`,
|
|
138
|
-
from: formatFilePath(relativePath)
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
unregisterMiddleware(ctor) {
|
|
142
|
-
this.middlewares.unregister(ctor);
|
|
143
|
-
}
|
|
144
|
-
registerHandler(ctor, relativePath) {
|
|
145
|
-
const from = formatFilePath(relativePath);
|
|
146
|
-
if (!this.routes.register(ctor, from)) return;
|
|
147
|
-
if (this.loading) this.loadedHandlers.push({
|
|
148
|
-
name: ctor.name,
|
|
149
|
-
from
|
|
150
|
-
});
|
|
70
|
+
drain() {
|
|
71
|
+
return this.inFlight.drain(DRAIN_WINDOW_MS);
|
|
151
72
|
}
|
|
152
73
|
};
|
|
153
74
|
|
|
@@ -186,156 +107,153 @@ async function writeWebResponse(response, out) {
|
|
|
186
107
|
const DEFAULT_PORT = 3e3;
|
|
187
108
|
const SERVER_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
188
109
|
/**
|
|
189
|
-
* The HTTP-interactions bot
|
|
110
|
+
* The HTTP-interactions bot on node. `start()` loads handlers from `config.bot.interactions.path`
|
|
111
|
+
* and binds a server on `port`. With `port: false`, your own server passes requests to `fetch()`.
|
|
190
112
|
*
|
|
191
|
-
*
|
|
192
|
-
* `start()`, and runs coordinated shutdown with an in-flight drain. The edge deploy path calls
|
|
193
|
-
* `createSeedcord` from a generated entry.
|
|
113
|
+
* Shutdown waits up to 5s for interactions that are still running.
|
|
194
114
|
*/
|
|
195
|
-
var Seedcord = class
|
|
115
|
+
var Seedcord = class extends ServerHost {
|
|
196
116
|
/** @internal */
|
|
197
117
|
[SeedcordBrand] = true;
|
|
198
118
|
/** @internal */
|
|
199
119
|
[HostAugmentTarget] = "@seedcord/http";
|
|
200
120
|
/** @internal */
|
|
201
121
|
[HostVersion] = version;
|
|
122
|
+
/** @internal */
|
|
123
|
+
[HostListenHost];
|
|
202
124
|
/** Discord REST client built from `bot.restOptions`. `start()` sets the token. */
|
|
203
125
|
rest;
|
|
204
126
|
/** @see {@link IRateLimiter} */
|
|
205
127
|
rateLimiter;
|
|
206
128
|
/** @see {@link Bus} */
|
|
207
129
|
bus;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
server;
|
|
216
|
-
boundPort;
|
|
217
|
-
fetchedUsername;
|
|
130
|
+
#service;
|
|
131
|
+
#endpoint;
|
|
132
|
+
#hmrManager;
|
|
133
|
+
#logger = new Logger("Server", { channel: "bot" });
|
|
134
|
+
#server;
|
|
135
|
+
#boundPort;
|
|
136
|
+
#fetchedUsername;
|
|
218
137
|
config;
|
|
138
|
+
/**
|
|
139
|
+
* @throws A **SeedcordError** when another `Seedcord` exists in this process and has not shut down.
|
|
140
|
+
* @throws A **SeedcordError** when the running Node or Bun is older than the range in `engines`.
|
|
141
|
+
*/
|
|
219
142
|
constructor(config) {
|
|
220
|
-
super(new CoordinatedShutdown(config.lifecycle?.shutdownDeadline), new CoordinatedStartup());
|
|
143
|
+
super("http", new CoordinatedShutdown(config.lifecycle?.shutdownDeadline), new CoordinatedStartup$1());
|
|
221
144
|
this.config = config;
|
|
222
|
-
this.rest = new REST(config.bot.restOptions);
|
|
223
|
-
installNodeDefaults(config.logger);
|
|
224
145
|
bindBotColor(() => this.config.botColor);
|
|
225
|
-
this
|
|
226
|
-
this.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this
|
|
231
|
-
this.
|
|
232
|
-
this
|
|
233
|
-
this
|
|
146
|
+
this.#service = new InteractionsService(this, config, config.bot.restOptions);
|
|
147
|
+
this.rest = this.#service.rest;
|
|
148
|
+
this.rateLimiter = this.#service.rateLimiter;
|
|
149
|
+
this.bus = this.#service.bus;
|
|
150
|
+
this.#endpoint = new NodeEndpoint(this.#service, this.#logger, HealthResponder.fromOption(config.healthCheck), this[HostStartup]);
|
|
151
|
+
this.#hmrManager = new HmrManager();
|
|
152
|
+
this.#hmrManager.init(connectDevChannel());
|
|
153
|
+
this.#registerStartupTasks();
|
|
154
|
+
this.#registerShutdownTasks();
|
|
234
155
|
}
|
|
235
156
|
/** The bot's discord username, populated by the ready fetch. */
|
|
236
157
|
get username() {
|
|
237
|
-
return this
|
|
158
|
+
return this.#fetchedUsername;
|
|
238
159
|
}
|
|
239
|
-
/** The bound server port,
|
|
160
|
+
/** The bound server port, set once `start()` is listening. Stays `undefined` with `port: false`. */
|
|
240
161
|
get port() {
|
|
241
|
-
return this
|
|
162
|
+
return this.#boundPort;
|
|
242
163
|
}
|
|
243
164
|
/**
|
|
244
165
|
* Starts the host and runs the startup tasks.
|
|
245
166
|
*/
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
await shutdownOf(this).run(1, false);
|
|
251
|
-
Seedcord.reset(this);
|
|
252
|
-
throw caught;
|
|
253
|
-
}
|
|
254
|
-
return this;
|
|
167
|
+
start() {
|
|
168
|
+
const starting = this.init();
|
|
169
|
+
this.#endpoint.startWith(starting);
|
|
170
|
+
return starting;
|
|
255
171
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Answers one request to the interactions endpoint. Mount it in your own server when the
|
|
174
|
+
* config sets `port: false`.
|
|
175
|
+
*
|
|
176
|
+
* Rejects with `CoreFetchBeforeStart` until `start()` is called. A request that arrives while
|
|
177
|
+
* `start()` runs waits for all of startup, the command deploy included. Answers 503 once
|
|
178
|
+
* shutdown begins or `start()` fails.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* // Hono passes the untouched request through c.req.raw
|
|
183
|
+
* const seedcord = new Seedcord({ ...config, port: false });
|
|
184
|
+
* await seedcord.start();
|
|
185
|
+
*
|
|
186
|
+
* const app = new Hono();
|
|
187
|
+
* app.post('/interactions', (c) => seedcord.fetch(c.req.raw));
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
fetch(request) {
|
|
191
|
+
return this.#endpoint.fetch(request);
|
|
260
192
|
}
|
|
261
|
-
registerStartupTasks() {
|
|
262
|
-
|
|
193
|
+
#registerStartupTasks() {
|
|
194
|
+
const tasks = new HttpStartupTasks(this, this.#service);
|
|
195
|
+
if (Envapter.isDevelopment || Envapter.isTest) tasks.registerHmr(this.#hmrManager);
|
|
263
196
|
this.startup.addTask(StartupPhase.Configuration, "bus-initialization", async () => {
|
|
264
197
|
busLoggerOf(this.bus).utils.initialization("Subscribers", "start");
|
|
265
|
-
await this.subscribers.init();
|
|
198
|
+
await this.#service.subscribers.init();
|
|
266
199
|
busLoggerOf(this.bus).utils.initialization("Subscribers", "end");
|
|
267
200
|
});
|
|
268
|
-
const
|
|
201
|
+
const interactions = this.#service.interactions;
|
|
269
202
|
if (interactions) this.startup.addTask(StartupPhase.Configuration, "interactions-initialization", async () => {
|
|
270
203
|
interactions.logger.utils.initialization("Interactions", "start");
|
|
271
204
|
await interactions.init();
|
|
272
205
|
interactions.logger.utils.initialization("Interactions", "end");
|
|
273
206
|
});
|
|
274
207
|
this.startup.addTask(StartupPhase.Configuration, "authenticate", () => {
|
|
275
|
-
this.authenticate();
|
|
208
|
+
this.#service.authenticate();
|
|
276
209
|
return Promise.resolve();
|
|
277
210
|
});
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
await commandRegistry.setCommands();
|
|
283
|
-
interactions?.warnUnhandledRoutes(commandRegistry.routeLeaves());
|
|
284
|
-
interactions?.warnUnhandledContextMenuRoutes(commandRegistry.contextMenuLeaves());
|
|
211
|
+
tasks.addDiscordTasks();
|
|
212
|
+
this.startup.addTask(StartupPhase.Ready, "http-server", () => {
|
|
213
|
+
const { port = DEFAULT_PORT } = this.config;
|
|
214
|
+
return port === false ? Promise.resolve() : this.#listen(port);
|
|
285
215
|
});
|
|
286
|
-
this.startup.addTask(StartupPhase.Ready, "
|
|
287
|
-
if (!Envapter.isTest) this.startup.addTask(StartupPhase.Ready, "identity", () => this.fetchUsername());
|
|
216
|
+
if (!Envapter.isTest) this.startup.addTask(StartupPhase.Ready, "identity", () => this.#fetchUsername());
|
|
288
217
|
}
|
|
289
|
-
|
|
290
|
-
this.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
this.hmrManager.register(this.subscribers);
|
|
294
|
-
for (const plugin of this.plugins) this.hmrManager.register(plugin);
|
|
295
|
-
await Promise.resolve();
|
|
218
|
+
#registerShutdownTasks() {
|
|
219
|
+
this.shutdown.addTask(ShutdownPhase.Unbind, "close-endpoint", () => {
|
|
220
|
+
this.#endpoint.close();
|
|
221
|
+
return Promise.resolve();
|
|
296
222
|
});
|
|
297
|
-
|
|
298
|
-
authenticate() {
|
|
299
|
-
this.token = validateDiscordToken(Envapter.get("DISCORD_BOT_TOKEN"));
|
|
300
|
-
this.rest.setToken(this.token);
|
|
223
|
+
this.shutdown.addTask(ShutdownPhase.Drain, "drain-inflight", () => this.#endpoint.drain(), DRAIN_TASK_TIMEOUT_MS);
|
|
301
224
|
}
|
|
302
225
|
/** The bot's Discord application id. Throws if you read it before the Configuration phase. */
|
|
303
226
|
get applicationId() {
|
|
304
|
-
|
|
305
|
-
return applicationIdFromToken(this.token);
|
|
227
|
+
return this.#service.applicationId;
|
|
306
228
|
}
|
|
307
|
-
async listen() {
|
|
308
|
-
const maps = this.interactions?.maps ?? emptyRouteMaps();
|
|
309
|
-
const middlewares = this.interactions?.middlewares ?? new MiddlewareRegistry(interactionMiddleware);
|
|
310
|
-
const { handle, inFlight } = buildEngine(this, maps, middlewares);
|
|
229
|
+
async #listen(port) {
|
|
311
230
|
const server = createServer((incoming, outgoing) => {
|
|
312
231
|
(async () => {
|
|
313
|
-
const response = await
|
|
232
|
+
const response = await this.#endpoint.answer(await toWebRequest(incoming));
|
|
233
|
+
if (!server.listening) outgoing.setHeader("connection", "close");
|
|
314
234
|
await writeWebResponse(response, outgoing);
|
|
315
235
|
})().catch((error) => {
|
|
316
236
|
outgoing.destroy(Error.isError(error) ? error : new Error(String(error)));
|
|
317
237
|
});
|
|
318
238
|
});
|
|
319
|
-
this
|
|
320
|
-
server.listen(
|
|
321
|
-
await once(server, "listening");
|
|
322
|
-
this.boundPort = server.address().port;
|
|
323
|
-
this.logger.info(`Interactions server listening on port ${paint.sky.bold(String(this.boundPort))}`);
|
|
324
|
-
getDevChannel()?.send("seedcord:server-listening", { port: this.boundPort });
|
|
325
|
-
this.shutdown.addTask(ShutdownPhase.Unbind, "stop-http-server", () => this.stopServer(), SERVER_SHUTDOWN_TIMEOUT_MS);
|
|
326
|
-
this.shutdown.addTask(ShutdownPhase.Drain, "drain-inflight", () => drainInFlight(inFlight, DRAIN_WINDOW_MS, this.logger, "Interactions"), DRAIN_TASK_TIMEOUT_MS);
|
|
327
|
-
}
|
|
328
|
-
async fetchUsername() {
|
|
239
|
+
this.#server = server;
|
|
240
|
+
server.listen(port, this[HostListenHost]);
|
|
329
241
|
try {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
this.logger.warn("could not fetch the bot identity", caught);
|
|
242
|
+
await once(server, "listening");
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (!isPortInUse(error)) throw error;
|
|
245
|
+
throw new SeedcordError(SeedcordErrorCode.PortInUse, [port, "port"], { cause: error });
|
|
335
246
|
}
|
|
247
|
+
this.#boundPort = server.address().port;
|
|
248
|
+
this.#logger.info(`Interactions server listening on port ${paint.sky.bold(String(this.#boundPort))}`);
|
|
249
|
+
getDevChannel()?.send("seedcord:server-listening", { port: this.#boundPort });
|
|
250
|
+
this.shutdown.addTask(ShutdownPhase.Unbind, "stop-http-server", () => this.#stopServer(), SERVER_SHUTDOWN_TIMEOUT_MS);
|
|
251
|
+
}
|
|
252
|
+
async #fetchUsername() {
|
|
253
|
+
this.#fetchedUsername = await usernameOf(this.rest, this.#logger);
|
|
336
254
|
}
|
|
337
|
-
stopServer() {
|
|
338
|
-
const server = this
|
|
255
|
+
#stopServer() {
|
|
256
|
+
const server = this.#server;
|
|
339
257
|
if (!server?.listening) return Promise.resolve();
|
|
340
258
|
return new Promise((resolveClose, rejectClose) => {
|
|
341
259
|
server.close((err) => {
|
|
@@ -343,26 +261,13 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
343
261
|
rejectClose(err);
|
|
344
262
|
return;
|
|
345
263
|
}
|
|
346
|
-
this
|
|
264
|
+
this.#logger.info(paint.coral.bold("Interactions server stopped"));
|
|
347
265
|
resolveClose();
|
|
348
266
|
});
|
|
349
|
-
server.closeIdleConnections();
|
|
350
267
|
});
|
|
351
268
|
}
|
|
352
269
|
};
|
|
353
270
|
|
|
354
271
|
//#endregion
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Base class for an HTTP plugin, binding `this.core` to the HTTP {@link Core}.
|
|
358
|
-
*
|
|
359
|
-
* A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose
|
|
360
|
-
* `this.core` is typed to the shared members only.
|
|
361
|
-
*
|
|
362
|
-
* @typeParam Opts - The transport and runtime the plugin declares.
|
|
363
|
-
*/
|
|
364
|
-
var Plugin = class extends Plugin$1 {};
|
|
365
|
-
|
|
366
|
-
//#endregion
|
|
367
|
-
export { ArraySource, AutocompleteHandler, ButtonHandler, ChannelMenuHandler, ComponentHandler, CursorSource, Emojis, Gated, InteractionHandler, InteractionMiddleware, MentionableMenuHandler, MessageContextMenuHandler, ModalFields, ModalHandler, Paginator, Plugin, RepliableHandler, ReplySender, RoleMenuHandler, Seedcord, SelectMenuHandler, SlashHandler, StringMenuHandler, Subscriber, UserContextMenuHandler, UserMenuHandler, WebhookLog, WinstonConsoleSink, WinstonFileSink, createSeedcord, version };
|
|
272
|
+
export { ArraySource, AutocompleteHandler, ButtonHandler, ChannelMenuHandler, ComponentHandler, CursorSource, Emojis, Gated, HmrModuleHandler, InteractionHandler, InteractionMiddleware, MentionableMenuHandler, MessageContextMenuHandler, ModalFields, ModalHandler, Paginator, Plugin, RepliableHandler, ReplySender, RoleMenuHandler, Seedcord, SelectMenuHandler, SlashHandler, StringMenuHandler, Subscriber, UserContextMenuHandler, UserMenuHandler, WebhookLog, WinstonConsoleSink, WinstonFileSink, version };
|
|
368
273
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["packageVersion","CorePlugin"],"sources":["../src/node/InteractionDispatcher.ts","../src/node/webBridge.ts","../src/node/Seedcord.ts","../src/Plugin.ts"],"sourcesContent":["import { InteractionKind } from '@seedcord/core';\nimport { HmrModuleHandler } from '@seedcord/core/hmr';\nimport {\n interactionMiddleware,\n InteractionMetadataKey,\n InteractionMiddlewareMetadataKey,\n MiddlewareRegistry\n} from '@seedcord/core/internal';\nimport { paint } from '@seedcord/errors';\nimport { Logger } from '@seedcord/logger';\nimport { formatFilePath } from '@seedcord/utils';\nimport { traverseDirectory } from '@seedcord/utils/node';\nimport { Envapter } from 'envapt';\n\nimport { RouteRegistry } from '#src/dispatch/RouteRegistry';\nimport { isHandlerClass, isMiddlewareClass } from '#src/manifest/entries';\n\nimport type { HandlerConstructor, InteractionMiddlewareConstructor } from '#handlers/constructors';\nimport type { RouteMap, RouteMaps } from '#src/dispatch/resolve';\nimport type { Initializeable, ContextMenuLeaves } from '@seedcord/core/internal';\nimport type { HmrAware, HmrUpdateEvent } from '@seedcord/types';\n\n// hmr swaps entries in these maps while resolve() reads them per request\nexport class InteractionDispatcher implements Initializeable, HmrAware {\n /** @internal */\n public readonly middlewares = new MiddlewareRegistry<InteractionMiddlewareConstructor>(interactionMiddleware);\n\n /** @internal */\n public readonly logger = new Logger('Interactions', { channel: 'interactions' });\n\n private readonly routes = new RouteRegistry();\n\n public get maps(): RouteMaps {\n return this.routes.maps;\n }\n\n private isInitialized = false;\n private readonly hmrHandler?: HmrModuleHandler<HandlerConstructor, InteractionMiddlewareConstructor, string[]>;\n\n private loading = false;\n private readonly loadedHandlers: { name: string; from: string }[] = [];\n private readonly loadedMiddlewares: { name: string; from: string }[] = [];\n\n constructor(\n private readonly handlersDir: string,\n private readonly middlewaresDir?: string\n ) {\n if (!Envapter.isDevelopment && !Envapter.isTest) return;\n\n this.hmrHandler = new HmrModuleHandler({\n handlersDir,\n ...(middlewaresDir && { middlewaresDir }),\n isHandler: this.isHandler.bind(this),\n isMiddleware: this.isMiddleware.bind(this),\n registerHandler: this.registerHandler.bind(this),\n registerMiddleware: this.registerMiddleware.bind(this),\n unregisterHandler: this.routes.unregister.bind(this.routes),\n unregisterMiddleware: this.unregisterMiddleware.bind(this),\n getArtifacts: this.routes.routesOf.bind(this.routes),\n logger: this.logger\n });\n }\n\n /** @internal */\n public async init(): Promise<void> {\n if (this.isInitialized) return;\n this.isInitialized = true;\n\n this.loading = true;\n this.loadedHandlers.length = 0;\n this.loadedMiddlewares.length = 0;\n try {\n if (this.middlewaresDir) await this.loadMiddlewares(this.middlewaresDir);\n await this.loadHandlers(this.handlersDir);\n } finally {\n this.loading = false;\n }\n\n this.reportLoad();\n }\n\n private async loadHandlers(dir: string): Promise<void> {\n await traverseDirectory(dir, (fullPath, relativePath, imported) => {\n for (const value of Object.values(imported)) {\n if (!this.isHandler(value)) continue;\n this.registerHandler(value, relativePath);\n this.hmrHandler?.trackHandler(fullPath, value);\n }\n });\n }\n\n private async loadMiddlewares(dir: string): Promise<void> {\n await traverseDirectory(dir, (fullPath, relativePath, imported) => {\n for (const value of Object.values(imported)) {\n if (!this.isMiddleware(value)) continue;\n this.registerMiddleware(value, relativePath);\n this.hmrHandler?.trackMiddleware(fullPath, value);\n }\n });\n }\n\n private reportLoad(): void {\n const { utils } = this.logger;\n\n if (this.loadedMiddlewares.length > 0) {\n utils.block('Loaded middlewares', utils.entries(this.loadedMiddlewares), 'debug');\n }\n\n utils.block(\n 'Loaded handlers',\n [...utils.entries(this.loadedHandlers), ...utils.counts({ routes: this.routes.size })],\n 'debug'\n );\n }\n\n /** @internal */\n public async onHmr(event: HmrUpdateEvent): Promise<void> {\n await this.hmrHandler?.handle(event);\n }\n\n public warnUnhandledRoutes(commandLeaves: Iterable<string>): void {\n this.warnMissing(commandLeaves, this.maps[InteractionKind.Slash], 'Slash route', '@SlashRoute');\n }\n\n public warnUnhandledContextMenuRoutes(leaves: ContextMenuLeaves): void {\n const user = this.maps[InteractionKind.UserContextMenu];\n const message = this.maps[InteractionKind.MessageContextMenu];\n this.warnMissing(leaves.user, user, 'User context menu', '@ContextMenuRoute');\n this.warnMissing(leaves.message, message, 'Message context menu', '@ContextMenuRoute');\n }\n\n private warnMissing(names: Iterable<string>, map: RouteMap, label: string, decorator: string): void {\n for (const name of names) {\n if (map.has(name)) continue;\n this.logger.warn(`${label} ${paint.sky.bold(name)} has no registered ${paint.bold(decorator)} handler.`);\n }\n }\n\n private isHandler(value: unknown): value is HandlerConstructor {\n return isHandlerClass(value) && Reflect.hasMetadata(InteractionMetadataKey, value);\n }\n\n private isMiddleware(value: unknown): value is InteractionMiddlewareConstructor {\n return isMiddlewareClass(value) && Reflect.hasMetadata(InteractionMiddlewareMetadataKey, value);\n }\n\n private registerMiddleware(ctor: InteractionMiddlewareConstructor, relativePath: string): void {\n const metadata = this.middlewares.register(ctor);\n if (!metadata || !this.loading) return;\n\n // the kinds are the only place a dev sees that a middleware is scoped\n const scope = metadata.keys ? `${metadata.priority}, ${metadata.keys.join(', ')}` : metadata.priority;\n this.loadedMiddlewares.push({\n name: `${ctor.name} (${String(scope)})`,\n from: formatFilePath(relativePath)\n });\n }\n\n private unregisterMiddleware(ctor: InteractionMiddlewareConstructor): void {\n this.middlewares.unregister(ctor);\n }\n\n private registerHandler(ctor: HandlerConstructor, relativePath: string): void {\n const from = formatFilePath(relativePath);\n if (!this.routes.register(ctor, from)) return;\n if (this.loading) this.loadedHandlers.push({ name: ctor.name, from });\n }\n}\n","import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'node:http';\n\nfunction toHeaders(raw: IncomingHttpHeaders): Headers {\n const headers = new Headers();\n for (const [name, value] of Object.entries(raw)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const item of value) headers.append(name, item);\n } else {\n headers.set(name, value);\n }\n }\n return headers;\n}\n\n// buffers the raw body before any parse so ed25519 verifies the exact signed bytes\nexport async function toWebRequest(request: IncomingMessage): Promise<Request> {\n const chunks: Buffer[] = [];\n // justified: the async iterator is typed any and IncomingMessage yields Buffer chunks\n for await (const chunk of request) chunks.push(chunk as Buffer);\n\n const method = request.method ?? 'GET';\n const body = method === 'GET' || method === 'HEAD' ? undefined : new Uint8Array(Buffer.concat(chunks));\n\n return new Request(`http://${request.headers.host ?? 'localhost'}${request.url ?? '/'}`, {\n method,\n headers: toHeaders(request.headers),\n ...(body !== undefined && { body })\n });\n}\n\nexport async function writeWebResponse(response: Response, out: ServerResponse): Promise<void> {\n const headers: Record<string, string | string[]> = Object.fromEntries(response.headers);\n // iterating Headers comma-joins same-name values, which corrupts Set-Cookie (a per-cookie header)\n const cookies = response.headers.getSetCookie();\n if (cookies.length > 0) headers['set-cookie'] = cookies;\n\n out.writeHead(response.status, headers);\n out.end(response.body === null ? undefined : Buffer.from(await response.arrayBuffer()));\n}\n","import { once } from 'node:events';\nimport { createServer } from 'node:http';\n\nimport { REST } from '@discordjs/rest';\nimport { Bus } from '@seedcord/core';\nimport {\n bindBotColor,\n busLoggerOf,\n getDevChannel,\n HmrManager,\n interactionMiddleware,\n MiddlewareRegistry\n} from '@seedcord/core/internal';\nimport { CoordinatedShutdown, CoordinatedStartup, Pluggable } from '@seedcord/core/node';\nimport {\n CommandRegistry,\n DRAIN_TASK_TIMEOUT_MS,\n DRAIN_WINDOW_MS,\n drainInFlight,\n ShutdownPhase,\n shutdownOf,\n StartupPhase,\n SubscriberLoader\n} from '@seedcord/core/node/internal';\nimport { SeedcordErrorCode, paint } from '@seedcord/errors';\nimport { applicationIdFromToken, SeedcordError, validateDiscordToken } from '@seedcord/errors/internal';\nimport { Logger, LoggerChannelRegistry } from '@seedcord/logger';\nimport { installNodeDefaults } from '@seedcord/logger/node';\nimport { MemoryRateLimiter } from '@seedcord/rate-limiter';\nimport { HostAugmentTarget, HostVersion, SeedcordBrand } from '@seedcord/types/internal';\nimport { Routes } from 'discord-api-types/v10';\nimport { Envapter } from 'envapt';\n\nimport { emptyRouteMaps } from '#src/dispatch/resolve';\nimport { EmojiInjector } from '#src/emojis/EmojiInjector';\nimport { buildEngine } from '#src/engine';\nimport { version as packageVersion } from '#src/version';\n\nimport { InteractionDispatcher } from './InteractionDispatcher';\nimport { toWebRequest, writeWebResponse } from './webBridge';\n\nimport type { InteractionMiddlewareConstructor } from '#handlers/constructors';\nimport type { HttpConfig } from '#interfaces/Config';\nimport type { IRateLimiter } from '@seedcord/types';\nimport type { SeedcordInstance } from '@seedcord/types/internal';\nimport type { Server } from 'node:http';\nimport type { AddressInfo } from 'node:net';\n\nconst DEFAULT_PORT = 3000;\nconst SERVER_SHUTDOWN_TIMEOUT_MS = 5000;\n\ntype RuntimeOfConfig<Cfg extends HttpConfig> = Cfg extends { runtime: 'edge' } ? 'edge' : 'server';\n\n/**\n * The HTTP-interactions bot host, a long-running node server around the engine.\n *\n * Discovers handlers from `config.bot.interactions.path`, verifies and dispatches interactions on\n * `start()`, and runs coordinated shutdown with an in-flight drain. The edge deploy path calls\n * `createSeedcord` from a generated entry.\n */\n// tests/node/seedcord-core.types-test.ts checks this class against Core in place of an implements clause\nexport class Seedcord<Cfg extends HttpConfig = HttpConfig>\n extends Pluggable<'http', RuntimeOfConfig<Cfg>>\n implements SeedcordInstance\n{\n // the CLI reads these to detect and augment the instance\n /** @internal */\n public readonly [SeedcordBrand] = true;\n /** @internal */\n public readonly [HostAugmentTarget] = '@seedcord/http';\n /** @internal */\n public readonly [HostVersion]: string = packageVersion;\n\n /** Discord REST client built from `bot.restOptions`. `start()` sets the token. */\n public readonly rest: REST;\n\n /** @see {@link IRateLimiter} */\n public readonly rateLimiter: IRateLimiter;\n\n /** @see {@link Bus} */\n public readonly bus: Bus;\n\n private readonly subscribers: SubscriberLoader;\n\n private readonly interactions?: InteractionDispatcher;\n private readonly commandRegistry?: CommandRegistry;\n private readonly emojiInjector = new EmojiInjector(this);\n private readonly hmrManager: HmrManager;\n private readonly logger = new Logger('Server', { channel: 'bot' });\n\n private token?: string;\n private server?: Server;\n private boundPort?: number;\n private fetchedUsername?: string | undefined;\n\n public readonly config: HttpConfig;\n\n constructor(config: Cfg) {\n super(new CoordinatedShutdown(config.lifecycle?.shutdownDeadline), new CoordinatedStartup());\n this.config = config;\n this.rest = new REST(config.bot.restOptions);\n\n installNodeDefaults(config.logger);\n bindBotColor(() => this.config.botColor);\n\n this.hmrManager = new HmrManager();\n this.hmrManager.init();\n\n const interactions = this.config.bot.interactions;\n if (interactions.path) {\n this.interactions = new InteractionDispatcher(interactions.path, interactions.middlewares);\n }\n\n if (this.config.bot.commands.path) this.commandRegistry = new CommandRegistry(this);\n\n this.rateLimiter = config.store ?? new MemoryRateLimiter();\n this.bus = new Bus(this);\n this.subscribers = new SubscriberLoader(this.bus, config.subscribers.path);\n\n this.registerStartupTasks();\n }\n\n /** The bot's discord username, populated by the ready fetch. */\n public get username(): string | undefined {\n return this.fetchedUsername;\n }\n\n /** The bound server port, populated once `start()` is listening. */\n public get port(): number | undefined {\n return this.boundPort;\n }\n\n /**\n * Starts the host and runs the startup tasks.\n */\n public async start(): Promise<this> {\n try {\n await super.init();\n } catch (caught) {\n await shutdownOf(this).run(1, false);\n Seedcord.reset(this);\n throw caught;\n }\n return this;\n }\n\n protected static override reset(host?: object): boolean {\n if (!super.reset(host)) return false;\n // super.reset() drops the dev TUI's log sink\n LoggerChannelRegistry.instance.configure({});\n return true;\n }\n\n private registerStartupTasks(): void {\n if (Envapter.isDevelopment || Envapter.isTest) this.registerHmrAwareModules();\n\n this.startup.addTask(StartupPhase.Configuration, 'bus-initialization', async () => {\n busLoggerOf(this.bus).utils.initialization('Subscribers', 'start');\n await this.subscribers.init();\n busLoggerOf(this.bus).utils.initialization('Subscribers', 'end');\n });\n\n const { interactions } = this;\n if (interactions) {\n this.startup.addTask(StartupPhase.Configuration, 'interactions-initialization', async () => {\n interactions.logger.utils.initialization('Interactions', 'start');\n await interactions.init();\n interactions.logger.utils.initialization('Interactions', 'end');\n });\n }\n\n this.startup.addTask(StartupPhase.Configuration, 'authenticate', () => {\n this.authenticate();\n return Promise.resolve();\n });\n\n // needs the token from Configuration, and must finish before Ready opens the server to interactions\n this.startup.addTask(StartupPhase.Login, 'emoji-injection', () => this.emojiInjector.init());\n\n const { commandRegistry } = this;\n if (commandRegistry) {\n // one task because tasks in a phase run concurrently and the deploy reads the id\n this.startup.addTask(StartupPhase.Login, 'command-deploy', async () => {\n await commandRegistry.init();\n await commandRegistry.setCommands();\n interactions?.warnUnhandledRoutes(commandRegistry.routeLeaves());\n interactions?.warnUnhandledContextMenuRoutes(commandRegistry.contextMenuLeaves());\n });\n }\n\n this.startup.addTask(StartupPhase.Ready, 'http-server', () => this.listen());\n\n if (!Envapter.isTest) {\n this.startup.addTask(StartupPhase.Ready, 'identity', () => this.fetchUsername());\n }\n }\n\n private registerHmrAwareModules(): void {\n this.startup.addTask(StartupPhase.Configuration, 'hmr-registration', async () => {\n if (this.interactions) this.hmrManager.register(this.interactions);\n if (this.commandRegistry) this.hmrManager.register(this.commandRegistry);\n this.hmrManager.register(this.subscribers);\n for (const plugin of this.plugins) {\n this.hmrManager.register(plugin);\n }\n await Promise.resolve();\n });\n }\n\n private authenticate(): void {\n this.token = validateDiscordToken(Envapter.get('DISCORD_BOT_TOKEN'));\n this.rest.setToken(this.token);\n }\n\n /** The bot's Discord application id. Throws if you read it before the Configuration phase. */\n public get applicationId(): string {\n if (!this.token) throw new SeedcordError(SeedcordErrorCode.CoreApplicationUnavailable);\n return applicationIdFromToken(this.token);\n }\n\n private async listen(): Promise<void> {\n const maps = this.interactions?.maps ?? emptyRouteMaps();\n const middlewares =\n this.interactions?.middlewares ??\n new MiddlewareRegistry<InteractionMiddlewareConstructor>(interactionMiddleware);\n const { handle, inFlight } = buildEngine(this, maps, middlewares);\n\n const server = createServer((incoming, outgoing) => {\n void (async () => {\n const response = await handle(await toWebRequest(incoming));\n await writeWebResponse(response, outgoing);\n })().catch((error: unknown) => {\n // a swallowed throw would hang the client with no cause\n outgoing.destroy(Error.isError(error) ? error : new Error(String(error)));\n });\n });\n this.server = server;\n\n server.listen(this.config.port ?? DEFAULT_PORT);\n await once(server, 'listening');\n // justified: address() is AddressInfo once a TCP server is listening\n this.boundPort = (server.address() as AddressInfo).port;\n this.logger.info(`Interactions server listening on port ${paint.sky.bold(String(this.boundPort))}`);\n getDevChannel()?.send('seedcord:server-listening', { port: this.boundPort });\n\n this.shutdown.addTask(\n ShutdownPhase.Unbind,\n 'stop-http-server',\n () => this.stopServer(),\n SERVER_SHUTDOWN_TIMEOUT_MS\n );\n // Unbind already ran, so every accepted request is in the in-flight set here\n this.shutdown.addTask(\n ShutdownPhase.Drain,\n 'drain-inflight',\n () => drainInFlight(inFlight, DRAIN_WINDOW_MS, this.logger, 'Interactions'),\n DRAIN_TASK_TIMEOUT_MS\n );\n }\n\n private async fetchUsername(): Promise<void> {\n try {\n // justified: the @me payload carries username per the discord api contract\n const me = (await this.rest.get(Routes.user('@me'))) as { username?: string };\n this.fetchedUsername = me.username;\n if (me.username) this.logger.info(`Running as ${paint.sky.bold(me.username)}`);\n } catch (caught) {\n // a bad token errors on the first real send anyway\n this.logger.warn('could not fetch the bot identity', caught);\n }\n }\n\n private stopServer(): Promise<void> {\n const server = this.server;\n if (!server?.listening) return Promise.resolve();\n\n return new Promise((resolveClose, rejectClose) => {\n server.close((err) => {\n if (err) {\n rejectClose(err);\n return;\n }\n this.logger.info(paint.coral.bold('Interactions server stopped'));\n resolveClose();\n });\n // node's close() leaves idle keep-alive sockets open\n server.closeIdleConnections();\n });\n }\n}\n","import { Plugin as CorePlugin } from '@seedcord/core/plugin';\n\nimport type { Core } from '#interfaces/Core';\nimport type { PluginOptions } from '@seedcord/core/plugin';\nimport type { TypedOmit } from '@seedcord/types';\n\n/** The options an HTTP plugin declares, narrowed to the transports this base serves. */\nexport type HttpPluginOptions = TypedOmit<PluginOptions, 'transport'> & { transport?: 'http' };\n\n/**\n * Base class for an HTTP plugin, binding `this.core` to the HTTP {@link Core}.\n *\n * A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose\n * `this.core` is typed to the shared members only.\n *\n * @typeParam Opts - The transport and runtime the plugin declares.\n */\nexport abstract class Plugin<Opts extends HttpPluginOptions = { transport: 'http' }> extends CorePlugin<Opts, Core> {}\n\nexport type { PluginLifecycleSpec, PluginOptions } from '@seedcord/core/plugin';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["packageVersion","CoordinatedStartup"],"sources":["../src/env-bootstrap.ts","../src/node/NodeEndpoint.ts","../src/node/webBridge.ts","../src/node/Seedcord.ts"],"sourcesContent":["import { setEnvaptBaseDir } from '@seedcord/core/node/internal';\n\nsetEnvaptBaseDir();\n","import { DRAIN_WINDOW_MS, InFlight } from '@seedcord/core/node/internal';\nimport { SeedcordErrorCode } from '@seedcord/errors';\nimport { SeedcordError } from '@seedcord/errors/internal';\n\nimport type { EngineContext } from '#src/engine';\nimport type { InteractionsService } from '#src/InteractionsService';\nimport type { CoordinatedStartup } from '@seedcord/core/node';\nimport type { HealthResponder } from '@seedcord/core/node/internal';\nimport type { Logger } from '@seedcord/logger';\n\nconst SERVICE_UNAVAILABLE = 503;\n\nfunction unavailable(): Response {\n return new Response(null, { status: SERVICE_UNAVAILABLE });\n}\n\nexport class NodeEndpoint implements EngineContext {\n private readonly inFlight: InFlight;\n private started?: Promise<void>;\n\n constructor(\n private readonly service: InteractionsService,\n logger: Logger,\n private readonly health: HealthResponder | undefined,\n private readonly startup: Pick<CoordinatedStartup, 'isReady'>\n ) {\n this.inFlight = new InFlight(logger, 'Interactions');\n }\n\n public startWith(starting: Promise<unknown>): void {\n this.started = starting.then(\n () => undefined,\n // the caller of start() gets the error\n () => this.close()\n );\n }\n\n public async fetch(request: Request): Promise<Response> {\n if (!this.started) throw new SeedcordError(SeedcordErrorCode.CoreFetchBeforeStart);\n await this.started;\n return await this.answer(request);\n }\n\n // the built-in server calls this directly because it binds in Ready, before start() resolves\n public async answer(request: Request): Promise<Response> {\n if (this.inFlight.closed) return unavailable();\n const health = this.health?.answer(request, this.startup.isReady);\n if (health) return health;\n const answering = this.service.engine(request, this);\n this.inFlight.track(answering);\n return answering;\n }\n\n public waitUntil(work: Promise<unknown>): void {\n this.inFlight.track(work);\n }\n\n public close(): void {\n this.inFlight.close();\n }\n\n public drain(): Promise<void> {\n return this.inFlight.drain(DRAIN_WINDOW_MS);\n }\n}\n","import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'node:http';\n\nfunction toHeaders(raw: IncomingHttpHeaders): Headers {\n const headers = new Headers();\n for (const [name, value] of Object.entries(raw)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const item of value) headers.append(name, item);\n } else {\n headers.set(name, value);\n }\n }\n return headers;\n}\n\n// buffers the raw body before any parse so ed25519 verifies the exact signed bytes\nexport async function toWebRequest(request: IncomingMessage): Promise<Request> {\n const chunks: Buffer[] = [];\n // justified: the async iterator is typed any and IncomingMessage yields Buffer chunks\n for await (const chunk of request) chunks.push(chunk as Buffer);\n\n const method = request.method ?? 'GET';\n const body = method === 'GET' || method === 'HEAD' ? undefined : new Uint8Array(Buffer.concat(chunks));\n\n return new Request(`http://${request.headers.host ?? 'localhost'}${request.url ?? '/'}`, {\n method,\n headers: toHeaders(request.headers),\n ...(body !== undefined && { body })\n });\n}\n\nexport async function writeWebResponse(response: Response, out: ServerResponse): Promise<void> {\n const headers: Record<string, string | string[]> = Object.fromEntries(response.headers);\n // iterating Headers comma-joins same-name values, which corrupts Set-Cookie (a per-cookie header)\n const cookies = response.headers.getSetCookie();\n if (cookies.length > 0) headers['set-cookie'] = cookies;\n\n out.writeHead(response.status, headers);\n out.end(response.body === null ? undefined : Buffer.from(await response.arrayBuffer()));\n}\n","import { once } from 'node:events';\nimport { createServer } from 'node:http';\n\nimport { bindBotColor, busLoggerOf, connectDevChannel, getDevChannel, HmrManager } from '@seedcord/core/internal';\nimport { CoordinatedShutdown, CoordinatedStartup, ServerHost } from '@seedcord/core/node';\nimport { DRAIN_TASK_TIMEOUT_MS, HealthResponder, ShutdownPhase, StartupPhase } from '@seedcord/core/node/internal';\nimport { paint, SeedcordErrorCode } from '@seedcord/errors';\nimport { SeedcordError } from '@seedcord/errors/internal';\nimport { Logger } from '@seedcord/logger';\nimport { HostAugmentTarget, HostListenHost, HostStartup, HostVersion, SeedcordBrand } from '@seedcord/types/internal';\nimport { isPortInUse } from '@seedcord/utils/internal';\nimport { Envapter } from 'envapt';\n\nimport { HttpStartupTasks } from '#src/HttpStartupTasks';\nimport { InteractionsService } from '#src/InteractionsService';\nimport { usernameOf } from '#src/username';\nimport { version as packageVersion } from '#src/version';\n\nimport { NodeEndpoint } from './NodeEndpoint';\nimport { toWebRequest, writeWebResponse } from './webBridge';\n\nimport type { HttpServerConfig } from '#interfaces/Config';\nimport type { REST } from '@discordjs/rest';\nimport type { Bus } from '@seedcord/core';\nimport type { IRateLimiter } from '@seedcord/types';\nimport type { SeedcordInstance } from '@seedcord/types/internal';\nimport type { Server } from 'node:http';\nimport type { AddressInfo } from 'node:net';\n\nconst DEFAULT_PORT = 3000;\nconst SERVER_SHUTDOWN_TIMEOUT_MS = 5000;\n\n/**\n * The HTTP-interactions bot on node. `start()` loads handlers from `config.bot.interactions.path`\n * and binds a server on `port`. With `port: false`, your own server passes requests to `fetch()`.\n *\n * Shutdown waits up to 5s for interactions that are still running.\n */\n// tests/node/seedcord-core.types-test.ts checks this class against Core in place of an implements clause\nexport class Seedcord extends ServerHost<'http'> implements SeedcordInstance {\n // the CLI reads these to detect and augment the instance\n /** @internal */\n public readonly [SeedcordBrand] = true;\n /** @internal */\n public readonly [HostAugmentTarget] = '@seedcord/http';\n /** @internal */\n public readonly [HostVersion]: string = packageVersion;\n /** @internal */\n public [HostListenHost]?: string | undefined;\n\n /** Discord REST client built from `bot.restOptions`. `start()` sets the token. */\n public readonly rest: REST;\n\n /** @see {@link IRateLimiter} */\n public readonly rateLimiter: IRateLimiter;\n\n /** @see {@link Bus} */\n public readonly bus: Bus;\n\n readonly #service: InteractionsService;\n readonly #endpoint: NodeEndpoint;\n readonly #hmrManager: HmrManager;\n readonly #logger = new Logger('Server', { channel: 'bot' });\n\n #server?: Server;\n #boundPort?: number;\n #fetchedUsername?: string | undefined;\n\n public readonly config: HttpServerConfig;\n\n /**\n * @throws A **SeedcordError** when another `Seedcord` exists in this process and has not shut down.\n * @throws A **SeedcordError** when the running Node or Bun is older than the range in `engines`.\n */\n constructor(config: HttpServerConfig) {\n super('http', new CoordinatedShutdown(config.lifecycle?.shutdownDeadline), new CoordinatedStartup());\n this.config = config;\n\n bindBotColor(() => this.config.botColor);\n\n this.#service = new InteractionsService(this, config, config.bot.restOptions);\n this.rest = this.#service.rest;\n this.rateLimiter = this.#service.rateLimiter;\n this.bus = this.#service.bus;\n this.#endpoint = new NodeEndpoint(\n this.#service,\n this.#logger,\n HealthResponder.fromOption(config.healthCheck),\n this[HostStartup]\n );\n\n this.#hmrManager = new HmrManager();\n this.#hmrManager.init(connectDevChannel());\n\n this.#registerStartupTasks();\n this.#registerShutdownTasks();\n }\n\n /** The bot's discord username, populated by the ready fetch. */\n public get username(): string | undefined {\n return this.#fetchedUsername;\n }\n\n /** The bound server port, set once `start()` is listening. Stays `undefined` with `port: false`. */\n public get port(): number | undefined {\n return this.#boundPort;\n }\n\n /**\n * Starts the host and runs the startup tasks.\n */\n public start(): Promise<this> {\n const starting = this.init();\n this.#endpoint.startWith(starting);\n return starting;\n }\n\n /**\n * Answers one request to the interactions endpoint. Mount it in your own server when the\n * config sets `port: false`.\n *\n * Rejects with `CoreFetchBeforeStart` until `start()` is called. A request that arrives while\n * `start()` runs waits for all of startup, the command deploy included. Answers 503 once\n * shutdown begins or `start()` fails.\n *\n * @example\n * ```ts\n * // Hono passes the untouched request through c.req.raw\n * const seedcord = new Seedcord({ ...config, port: false });\n * await seedcord.start();\n *\n * const app = new Hono();\n * app.post('/interactions', (c) => seedcord.fetch(c.req.raw));\n * ```\n */\n public fetch(request: Request): Promise<Response> {\n return this.#endpoint.fetch(request);\n }\n\n #registerStartupTasks(): void {\n const tasks = new HttpStartupTasks(this, this.#service);\n if (Envapter.isDevelopment || Envapter.isTest) tasks.registerHmr(this.#hmrManager);\n\n this.startup.addTask(StartupPhase.Configuration, 'bus-initialization', async () => {\n busLoggerOf(this.bus).utils.initialization('Subscribers', 'start');\n await this.#service.subscribers.init();\n busLoggerOf(this.bus).utils.initialization('Subscribers', 'end');\n });\n\n const interactions = this.#service.interactions;\n if (interactions) {\n this.startup.addTask(StartupPhase.Configuration, 'interactions-initialization', async () => {\n interactions.logger.utils.initialization('Interactions', 'start');\n await interactions.init();\n interactions.logger.utils.initialization('Interactions', 'end');\n });\n }\n\n this.startup.addTask(StartupPhase.Configuration, 'authenticate', () => {\n this.#service.authenticate();\n return Promise.resolve();\n });\n\n // the Login tasks read the token authenticate sets\n tasks.addDiscordTasks();\n\n this.startup.addTask(StartupPhase.Ready, 'http-server', () => {\n const { port = DEFAULT_PORT } = this.config;\n return port === false ? Promise.resolve() : this.#listen(port);\n });\n\n if (!Envapter.isTest) {\n this.startup.addTask(StartupPhase.Ready, 'identity', () => this.#fetchUsername());\n }\n }\n\n #registerShutdownTasks(): void {\n this.shutdown.addTask(ShutdownPhase.Unbind, 'close-endpoint', () => {\n this.#endpoint.close();\n return Promise.resolve();\n });\n this.shutdown.addTask(\n ShutdownPhase.Drain,\n 'drain-inflight',\n () => this.#endpoint.drain(),\n DRAIN_TASK_TIMEOUT_MS\n );\n }\n\n /** The bot's Discord application id. Throws if you read it before the Configuration phase. */\n public get applicationId(): string {\n return this.#service.applicationId;\n }\n\n async #listen(port: number): Promise<void> {\n const server = createServer((incoming, outgoing) => {\n void (async () => {\n const response = await this.#endpoint.answer(await toWebRequest(incoming));\n // node's close() leaves a busy connection open once it goes idle\n if (!server.listening) outgoing.setHeader('connection', 'close');\n await writeWebResponse(response, outgoing);\n })().catch((error: unknown) => {\n // a swallowed throw would hang the client with no cause\n outgoing.destroy(Error.isError(error) ? error : new Error(String(error)));\n });\n });\n this.#server = server;\n\n server.listen(port, this[HostListenHost]);\n try {\n await once(server, 'listening');\n } catch (error: unknown) {\n if (!isPortInUse(error)) throw error;\n throw new SeedcordError(SeedcordErrorCode.PortInUse, [port, 'port'], { cause: error });\n }\n // justified: address() is AddressInfo once a TCP server is listening\n this.#boundPort = (server.address() as AddressInfo).port;\n this.#logger.info(`Interactions server listening on port ${paint.sky.bold(String(this.#boundPort))}`);\n getDevChannel()?.send('seedcord:server-listening', { port: this.#boundPort });\n\n this.shutdown.addTask(\n ShutdownPhase.Unbind,\n 'stop-http-server',\n () => this.#stopServer(),\n SERVER_SHUTDOWN_TIMEOUT_MS\n );\n }\n\n async #fetchUsername(): Promise<void> {\n this.#fetchedUsername = await usernameOf(this.rest, this.#logger);\n }\n\n #stopServer(): Promise<void> {\n const server = this.#server;\n if (!server?.listening) return Promise.resolve();\n\n return new Promise((resolveClose, rejectClose) => {\n server.close((err) => {\n if (err) {\n rejectClose(err);\n return;\n }\n this.#logger.info(paint.coral.bold('Interactions server stopped'));\n resolveClose();\n });\n });\n }\n}\n"],"mappings":"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"}
|