@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/dist/index.mjs CHANGED
@@ -1,153 +1,74 @@
1
- import { A as InteractionHandler, C as createSeedcord, D as isHandlerClass, E as emptyRouteMaps, M as RepliableHandler, N as ReplySender, O as isMiddlewareClass, S as ModalFields, T as RouteRegistry, _ as MessageContextMenuHandler, a as Paginator, b as EmojiInjector, c as ChannelMenuHandler, d as SelectMenuHandler, f as StringMenuHandler, g as ComponentHandler, h as ButtonHandler, i as CursorSource, j as AutocompleteHandler, k as InteractionMiddleware, l as MentionableMenuHandler, m as ModalHandler, n as Gated, o as Subscriber, p as UserMenuHandler, r as ArraySource, s as WebhookLog, t as version, u as RoleMenuHandler, v as UserContextMenuHandler, w as buildEngine, x as Emojis, y as SlashHandler } from "./edge.index-DCP1BD6f.mjs";
2
- import "reflect-metadata";
3
- import { HmrManager, InteractionMetadataKey, InteractionMiddlewareMetadataKey, MiddlewareRegistry, bindBotColor, busLoggerOf, getDevChannel, interactionMiddleware } from "@seedcord/core/internal";
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 { MemoryRateLimiter } from "@seedcord/rate-limiter";
11
- import { Routes } from "discord-api-types/v10";
12
- import { formatFilePath } from "@seedcord/utils";
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, Pluggable } from "@seedcord/core/node";
16
- import { CommandRegistry, DRAIN_TASK_TIMEOUT_MS, DRAIN_WINDOW_MS, ShutdownPhase, StartupPhase, SubscriberLoader, drainInFlight, shutdownOf } from "@seedcord/core/node/internal";
17
- import { WinstonConsoleSink, WinstonFileSink, installNodeDefaults } from "@seedcord/logger/node";
18
- import { HostAugmentTarget, HostVersion, SeedcordBrand } from "@seedcord/types/internal";
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/logger"
15
+ export * from "@seedcord/errors"
24
16
 
25
17
  export * from "@seedcord/core"
26
18
 
27
- export * from "@seedcord/errors"
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/node/InteractionDispatcher.ts
36
- var InteractionDispatcher = class {
37
- handlersDir;
38
- middlewaresDir;
39
- /** @internal */
40
- middlewares = new MiddlewareRegistry(interactionMiddleware);
41
- /** @internal */
42
- logger = new Logger("Interactions", { channel: "interactions" });
43
- routes = new RouteRegistry();
44
- get maps() {
45
- return this.routes.maps;
46
- }
47
- isInitialized = false;
48
- hmrHandler;
49
- loading = false;
50
- loadedHandlers = [];
51
- loadedMiddlewares = [];
52
- constructor(handlersDir, middlewaresDir) {
53
- this.handlersDir = handlersDir;
54
- this.middlewaresDir = middlewaresDir;
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
- warnUnhandledRoutes(commandLeaves) {
112
- this.warnMissing(commandLeaves, this.maps[InteractionKind.Slash], "Slash route", "@SlashRoute");
48
+ startWith(starting) {
49
+ this.started = starting.then(() => void 0, () => this.close());
113
50
  }
114
- warnUnhandledContextMenuRoutes(leaves) {
115
- const user = this.maps[InteractionKind.UserContextMenu];
116
- const message = this.maps[InteractionKind.MessageContextMenu];
117
- this.warnMissing(leaves.user, user, "User context menu", "@ContextMenuRoute");
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
- warnMissing(names, map, label, decorator) {
121
- for (const name of names) {
122
- if (map.has(name)) continue;
123
- this.logger.warn(`${label} ${paint.sky.bold(name)} has no registered ${paint.bold(decorator)} handler.`);
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
- isHandler(value) {
127
- return isHandlerClass(value) && Reflect.hasMetadata(InteractionMetadataKey, value);
64
+ waitUntil(work) {
65
+ this.inFlight.track(work);
128
66
  }
129
- isMiddleware(value) {
130
- return isMiddlewareClass(value) && Reflect.hasMetadata(InteractionMiddlewareMetadataKey, value);
67
+ close() {
68
+ this.inFlight.close();
131
69
  }
132
- registerMiddleware(ctor, relativePath) {
133
- const metadata = this.middlewares.register(ctor);
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 host, a long-running node server around the engine.
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
- * Discovers handlers from `config.bot.interactions.path`, verifies and dispatches interactions on
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 Seedcord extends Pluggable {
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
- subscribers;
209
- interactions;
210
- commandRegistry;
211
- emojiInjector = new EmojiInjector(this);
212
- hmrManager;
213
- logger = new Logger("Server", { channel: "bot" });
214
- token;
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.hmrManager = new HmrManager();
226
- this.hmrManager.init();
227
- const interactions = this.config.bot.interactions;
228
- if (interactions.path) this.interactions = new InteractionDispatcher(interactions.path, interactions.middlewares);
229
- if (this.config.bot.commands.path) this.commandRegistry = new CommandRegistry(this);
230
- this.rateLimiter = config.store ?? new MemoryRateLimiter();
231
- this.bus = new Bus(this);
232
- this.subscribers = new SubscriberLoader(this.bus, config.subscribers.path);
233
- this.registerStartupTasks();
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.fetchedUsername;
158
+ return this.#fetchedUsername;
238
159
  }
239
- /** The bound server port, populated once `start()` is listening. */
160
+ /** The bound server port, set once `start()` is listening. Stays `undefined` with `port: false`. */
240
161
  get port() {
241
- return this.boundPort;
162
+ return this.#boundPort;
242
163
  }
243
164
  /**
244
165
  * Starts the host and runs the startup tasks.
245
166
  */
246
- async start() {
247
- try {
248
- await super.init();
249
- } catch (caught) {
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
- static reset(host) {
257
- if (!super.reset(host)) return false;
258
- LoggerChannelRegistry.instance.configure({});
259
- return true;
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
- if (Envapter.isDevelopment || Envapter.isTest) this.registerHmrAwareModules();
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 { interactions } = this;
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
- this.startup.addTask(StartupPhase.Login, "emoji-injection", () => this.emojiInjector.init());
279
- const { commandRegistry } = this;
280
- if (commandRegistry) this.startup.addTask(StartupPhase.Login, "command-deploy", async () => {
281
- await commandRegistry.init();
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, "http-server", () => this.listen());
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
- registerHmrAwareModules() {
290
- this.startup.addTask(StartupPhase.Configuration, "hmr-registration", async () => {
291
- if (this.interactions) this.hmrManager.register(this.interactions);
292
- if (this.commandRegistry) this.hmrManager.register(this.commandRegistry);
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
- if (!this.token) throw new SeedcordError(SeedcordErrorCode.CoreApplicationUnavailable);
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 handle(await toWebRequest(incoming));
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.server = server;
320
- server.listen(this.config.port ?? DEFAULT_PORT);
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
- const me = await this.rest.get(Routes.user("@me"));
331
- this.fetchedUsername = me.username;
332
- if (me.username) this.logger.info(`Running as ${paint.sky.bold(me.username)}`);
333
- } catch (caught) {
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.server;
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.logger.info(paint.coral.bold("Interactions server stopped"));
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
- //#region src/Plugin.ts
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
@@ -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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"}