keryx 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/actions/status.ts +25 -0
  3. package/actions/swagger.ts +170 -0
  4. package/api.ts +45 -0
  5. package/classes/API.ts +168 -0
  6. package/classes/Action.ts +128 -0
  7. package/classes/Channel.ts +81 -0
  8. package/classes/Connection.ts +282 -0
  9. package/classes/ExitCode.ts +4 -0
  10. package/classes/Initializer.ts +45 -0
  11. package/classes/Logger.ts +132 -0
  12. package/classes/Server.ts +16 -0
  13. package/classes/TypedError.ts +91 -0
  14. package/config/channels.ts +9 -0
  15. package/config/database.ts +6 -0
  16. package/config/index.ts +23 -0
  17. package/config/logger.ts +8 -0
  18. package/config/process.ts +9 -0
  19. package/config/rateLimit.ts +22 -0
  20. package/config/redis.ts +8 -0
  21. package/config/server/cli.ts +9 -0
  22. package/config/server/mcp.ts +11 -0
  23. package/config/server/web.ts +68 -0
  24. package/config/session.ts +18 -0
  25. package/config/tasks.ts +26 -0
  26. package/index.ts +29 -0
  27. package/initializers/actionts.ts +669 -0
  28. package/initializers/channels.ts +284 -0
  29. package/initializers/connections.ts +37 -0
  30. package/initializers/db.ts +158 -0
  31. package/initializers/mcp.ts +477 -0
  32. package/initializers/oauth.ts +610 -0
  33. package/initializers/process.ts +25 -0
  34. package/initializers/pubsub.ts +86 -0
  35. package/initializers/redis.ts +77 -0
  36. package/initializers/resque.ts +354 -0
  37. package/initializers/servers.ts +66 -0
  38. package/initializers/session.ts +84 -0
  39. package/initializers/signals.ts +60 -0
  40. package/initializers/swagger.ts +317 -0
  41. package/keryx.ts +61 -0
  42. package/lua/add-presence.lua +13 -0
  43. package/lua/refresh-presence.lua +8 -0
  44. package/lua/remove-presence.lua +16 -0
  45. package/middleware/rateLimit.ts +92 -0
  46. package/migrations.ts +5 -0
  47. package/package.json +97 -0
  48. package/servers/web.ts +721 -0
  49. package/templates/lion.svg +102 -0
  50. package/templates/oauth-authorize.html +75 -0
  51. package/templates/oauth-common.css +140 -0
  52. package/templates/oauth-success.html +38 -0
  53. package/tsconfig.json +24 -0
  54. package/util/cli.ts +135 -0
  55. package/util/config.ts +24 -0
  56. package/util/connectionString.ts +5 -0
  57. package/util/glob.ts +41 -0
  58. package/util/http.ts +86 -0
  59. package/util/oauth.ts +69 -0
  60. package/util/zodMixins.ts +88 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Evan Tahler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import { Action, api } from "../api";
3
+ import { HTTP_METHOD } from "../classes/Action";
4
+ import packageJSON from "../package.json";
5
+
6
+ export class Status implements Action {
7
+ name = "status";
8
+ description =
9
+ "Returns server health and runtime information including the server name, process ID, package version, uptime in milliseconds, and memory consumption in MB. Does not require authentication.";
10
+ inputs = z.object({});
11
+ web = { route: "/status", method: HTTP_METHOD.GET };
12
+
13
+ async run() {
14
+ const consumedMemoryMB =
15
+ Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100;
16
+
17
+ return {
18
+ name: api.process.name,
19
+ pid: api.process.pid,
20
+ version: packageJSON.version,
21
+ uptime: new Date().getTime() - api.bootTime,
22
+ consumedMemoryMB,
23
+ };
24
+ }
25
+ }
@@ -0,0 +1,170 @@
1
+ import { z } from "zod";
2
+ import { Action, api, config } from "../api";
3
+ import { HTTP_METHOD } from "../classes/Action";
4
+ import packageJSON from "../package.json";
5
+
6
+ const SWAGGER_VERSION = "3.0.0";
7
+
8
+ const errorResponseSchema = {
9
+ type: "object",
10
+ properties: {
11
+ error: { type: "string" },
12
+ },
13
+ };
14
+
15
+ const swaggerResponses = {
16
+ "200": {
17
+ description: "successful operation",
18
+ content: {
19
+ "application/json": {
20
+ schema: {},
21
+ },
22
+ },
23
+ },
24
+ "400": {
25
+ description: "Invalid input",
26
+ content: {
27
+ "application/json": {
28
+ schema: errorResponseSchema,
29
+ },
30
+ },
31
+ },
32
+ "404": {
33
+ description: "Not Found",
34
+ content: {
35
+ "application/json": {
36
+ schema: errorResponseSchema,
37
+ },
38
+ },
39
+ },
40
+ "422": {
41
+ description: "Missing or invalid params",
42
+ content: {
43
+ "application/json": {
44
+ schema: errorResponseSchema,
45
+ },
46
+ },
47
+ },
48
+ "500": {
49
+ description: "Server error",
50
+ content: {
51
+ "application/json": {
52
+ schema: errorResponseSchema,
53
+ },
54
+ },
55
+ },
56
+ };
57
+
58
+ export class Swagger implements Action {
59
+ name = "swagger";
60
+ description =
61
+ "Returns the full API documentation as an OpenAPI 3.0.0 JSON document. Includes all available endpoints with their routes, methods, request schemas, response schemas, and parameter descriptions. Does not require authentication.";
62
+ web = { route: "/swagger", method: HTTP_METHOD.GET };
63
+
64
+ async run() {
65
+ const paths: Record<string, any> = {};
66
+ const components: { schemas: Record<string, any> } = { schemas: {} };
67
+
68
+ for (const action of api.actions.actions) {
69
+ if (!action.web?.route || !action.web?.method) continue;
70
+ // Skip RegExp routes for swagger documentation
71
+ if (typeof action.web.route !== "string") continue;
72
+ // Convert :param format to OpenAPI {param} format
73
+ const path = action.web.route.replace(
74
+ /:\w+/g,
75
+ (match: string) => `{${match.slice(1)}}`,
76
+ );
77
+ const method = action.web.method.toLowerCase();
78
+ const tag = action.name.split(":")[0];
79
+ const summary = action.description || action.name;
80
+
81
+ // Extract path parameters from the original route
82
+ const pathParams: any[] = [];
83
+ const pathParamMatches = action.web.route.match(/:\w+/g) || [];
84
+ for (const paramMatch of pathParamMatches) {
85
+ const paramName = paramMatch.slice(1); // Remove the colon
86
+ pathParams.push({
87
+ name: paramName,
88
+ in: "path",
89
+ required: true,
90
+ schema: { type: "string" },
91
+ description: `The ${paramName} parameter`,
92
+ });
93
+ }
94
+
95
+ // Build requestBody if Zod inputs exist and method supports body
96
+ let requestBody: any = undefined;
97
+ if (
98
+ action.inputs &&
99
+ typeof action.inputs.parse === "function" &&
100
+ method !== "get" &&
101
+ method !== "head"
102
+ ) {
103
+ const zodSchema = action.inputs;
104
+ const schemaName = `${action.name.replace(/:/g, "_")}_Request`;
105
+ // Use io: "input" to get the input schema (before transforms)
106
+ // Use unrepresentable: "any" to handle refinements, async transforms, etc.
107
+ const jsonSchema = z.toJSONSchema(zodSchema, {
108
+ io: "input",
109
+ unrepresentable: "any",
110
+ });
111
+ // Remove $schema from component schemas (not needed in OpenAPI)
112
+ const { $schema, ...schemaWithout$schema } = jsonSchema as any;
113
+ components.schemas[schemaName] = schemaWithout$schema;
114
+ requestBody = {
115
+ required: true,
116
+ content: {
117
+ "application/json": {
118
+ schema: { $ref: `#/components/schemas/${schemaName}` },
119
+ },
120
+ },
121
+ };
122
+ }
123
+
124
+ // Build responses - use generated schema if available
125
+ const responses = JSON.parse(JSON.stringify(swaggerResponses));
126
+ const responseSchema = api.swagger?.responseSchemas[action.name];
127
+ if (responseSchema) {
128
+ const schemaName = `${action.name.replace(/:/g, "_")}_Response`;
129
+ components.schemas[schemaName] = responseSchema;
130
+ responses["200"] = {
131
+ description: "successful operation",
132
+ content: {
133
+ "application/json": {
134
+ schema: { $ref: `#/components/schemas/${schemaName}` },
135
+ },
136
+ },
137
+ };
138
+ }
139
+
140
+ // Add path/method
141
+ if (!paths[path]) paths[path] = {};
142
+ paths[path][method] = {
143
+ summary,
144
+ ...(pathParams.length > 0 ? { parameters: pathParams } : {}),
145
+ ...(requestBody ? { requestBody } : {}),
146
+ responses,
147
+ tags: [tag],
148
+ };
149
+ }
150
+
151
+ const document = {
152
+ openapi: SWAGGER_VERSION,
153
+ info: {
154
+ version: packageJSON.version,
155
+ title: packageJSON.name,
156
+ license: { name: packageJSON.license },
157
+ description: packageJSON.description,
158
+ },
159
+ servers: [
160
+ {
161
+ url: config.server.web.applicationUrl + config.server.web.apiRoute,
162
+ description: packageJSON.description,
163
+ },
164
+ ],
165
+ paths,
166
+ components,
167
+ };
168
+ return document;
169
+ }
170
+ }
package/api.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { API } from "./classes/API";
2
+ import type { Logger } from "./classes/Logger";
3
+ import { config as Config } from "./config";
4
+
5
+ export {
6
+ Action,
7
+ type ActionConstructorInputs,
8
+ type ActionParams,
9
+ type ActionResponse,
10
+ type McpActionConfig,
11
+ type OAuthActionResponse,
12
+ } from "./classes/Action";
13
+ export { API, RUN_MODE } from "./classes/API";
14
+ export {
15
+ Channel,
16
+ type ChannelConstructorInputs,
17
+ type ChannelMiddleware,
18
+ } from "./classes/Channel";
19
+ export { Connection } from "./classes/Connection";
20
+ export { Initializer } from "./classes/Initializer";
21
+ export { Logger } from "./classes/Logger";
22
+ export { Server } from "./classes/Server";
23
+ export type {
24
+ FanOutJob,
25
+ FanOutOptions,
26
+ FanOutResult,
27
+ FanOutStatus,
28
+ } from "./initializers/actionts";
29
+
30
+ declare namespace globalThis {
31
+ let api: API;
32
+ let logger: Logger;
33
+ let config: typeof Config;
34
+ }
35
+
36
+ if (!globalThis.api) {
37
+ // @ts-ignore — augmented API properties (db, redis, etc.) are set later by initializers at runtime
38
+ globalThis.api = new API();
39
+ globalThis.logger = globalThis.api.logger;
40
+ globalThis.config = Config;
41
+ }
42
+
43
+ export const api = globalThis.api;
44
+ export const logger = globalThis.logger;
45
+ export const config = globalThis.config;
package/classes/API.ts ADDED
@@ -0,0 +1,168 @@
1
+ import path from "path";
2
+ import { config } from "../config";
3
+ import { globLoader } from "../util/glob";
4
+ import type { Initializer, InitializerSortKeys } from "./Initializer";
5
+ import { Logger } from "./Logger";
6
+ import { ErrorType, TypedError } from "./TypedError";
7
+
8
+ export enum RUN_MODE {
9
+ CLI = "cli",
10
+ SERVER = "server",
11
+ }
12
+
13
+ let flapPreventer = false;
14
+
15
+ export class API {
16
+ rootDir: string;
17
+ packageDir: string;
18
+ initialized: boolean;
19
+ started: boolean;
20
+ stopped: boolean;
21
+ bootTime: number;
22
+ logger: Logger;
23
+ runMode!: RUN_MODE;
24
+ initializers: Initializer[];
25
+
26
+ // allow arbitrary properties to be set on the API, to be added and typed later
27
+ [key: string]: any;
28
+
29
+ constructor() {
30
+ this.bootTime = new Date().getTime();
31
+ this.packageDir = path.join(import.meta.path, "..", "..");
32
+ this.rootDir = this.packageDir;
33
+ this.logger = new Logger(config.logger);
34
+
35
+ this.initialized = false;
36
+ this.started = false;
37
+ this.stopped = false;
38
+
39
+ this.initializers = [];
40
+ }
41
+
42
+ async initialize() {
43
+ this.logger.warn("--- 🔄 Initializing process ---");
44
+ this.initialized = false;
45
+
46
+ await this.findInitializers();
47
+ this.sortInitializers("loadPriority");
48
+
49
+ for (const initializer of this.initializers) {
50
+ try {
51
+ this.logger.debug(`Initializing initializer ${initializer.name}`);
52
+ const response = await initializer.initialize?.();
53
+ if (response) this[initializer.name] = response;
54
+ this.logger.debug(`Initialized initializer ${initializer.name}`);
55
+ } catch (e) {
56
+ throw new TypedError({
57
+ message: `${e}`,
58
+ type: ErrorType.SERVER_INITIALIZATION,
59
+ originalError: e,
60
+ });
61
+ }
62
+ }
63
+
64
+ this.initialized = true;
65
+ this.logger.warn("--- 🔄 Initializing complete ---");
66
+ }
67
+
68
+ async start(runMode: RUN_MODE = RUN_MODE.SERVER) {
69
+ this.stopped = false;
70
+ this.started = false;
71
+ this.runMode = runMode;
72
+ if (!this.initialized) await this.initialize();
73
+
74
+ this.logger.warn("--- 🔼 Starting process ---");
75
+
76
+ this.sortInitializers("startPriority");
77
+
78
+ for (const initializer of this.initializers) {
79
+ if (!initializer.runModes.includes(runMode)) {
80
+ this.logger.debug(
81
+ `Not starting initializer ${initializer.name} in ${runMode} mode`,
82
+ );
83
+ continue;
84
+ }
85
+
86
+ try {
87
+ this.logger.debug(`Starting initializer ${initializer.name}`);
88
+ await initializer.start?.();
89
+ this.logger.debug(`Started initializer ${initializer.name}`);
90
+ } catch (e) {
91
+ throw new TypedError({
92
+ message: `${e}`,
93
+ type: ErrorType.SERVER_START,
94
+ originalError: e,
95
+ });
96
+ }
97
+ }
98
+
99
+ this.started = true;
100
+ this.logger.warn("--- 🔼 Starting complete ---");
101
+ }
102
+
103
+ async stop() {
104
+ if (this.stopped) {
105
+ this.logger.warn("API is already stopped");
106
+ return;
107
+ }
108
+
109
+ this.logger.warn("--- 🔽 Stopping process ---");
110
+
111
+ this.sortInitializers("stopPriority");
112
+
113
+ for (const initializer of this.initializers) {
114
+ try {
115
+ this.logger.debug(`Stopping initializer ${initializer.name}`);
116
+ await initializer.stop?.();
117
+ this.logger.debug(`Stopped initializer ${initializer.name}`);
118
+ } catch (e) {
119
+ throw new TypedError({
120
+ message: `${e}`,
121
+ type: ErrorType.SERVER_STOP,
122
+ originalError: e,
123
+ });
124
+ }
125
+ }
126
+
127
+ this.stopped = true;
128
+ this.started = false;
129
+ this.logger.warn("--- 🔽 Stopping complete ---");
130
+ }
131
+
132
+ async restart() {
133
+ if (flapPreventer) return;
134
+
135
+ flapPreventer = true;
136
+ await this.stop();
137
+ await this.start();
138
+ flapPreventer = false;
139
+ }
140
+
141
+ private async findInitializers() {
142
+ // Load framework initializers from the package directory
143
+ const frameworkInitializers = await globLoader<Initializer>(
144
+ path.join(this.packageDir, "initializers"),
145
+ );
146
+ for (const i of frameworkInitializers) {
147
+ this.initializers.push(i);
148
+ }
149
+
150
+ // Load user project initializers (if rootDir differs from packageDir)
151
+ if (this.rootDir !== this.packageDir) {
152
+ try {
153
+ const userInitializers = await globLoader<Initializer>(
154
+ path.join(this.rootDir, "initializers"),
155
+ );
156
+ for (const i of userInitializers) {
157
+ this.initializers.push(i);
158
+ }
159
+ } catch {
160
+ // user project may not have initializers, that's fine
161
+ }
162
+ }
163
+ }
164
+
165
+ private sortInitializers(key: InitializerSortKeys) {
166
+ this.initializers.sort((a, b) => a[key] - b[key]);
167
+ }
168
+ }
@@ -0,0 +1,128 @@
1
+ import { z } from "zod";
2
+ import type { Connection } from "./Connection";
3
+ import type { TypedError } from "./TypedError";
4
+
5
+ export enum HTTP_METHOD {
6
+ "GET" = "GET",
7
+ "POST" = "POST",
8
+ "PUT" = "PUT",
9
+ "DELETE" = "DELETE",
10
+ "PATCH" = "PATCH",
11
+ "OPTIONS" = "OPTIONS",
12
+ }
13
+
14
+ export const DEFAULT_QUEUE = "default";
15
+
16
+ export type OAuthActionResponse = {
17
+ user: { id: number };
18
+ };
19
+
20
+ export type McpActionConfig = {
21
+ /** Expose this action as an MCP tool (default true) */
22
+ enabled?: boolean;
23
+ /** Tag as the OAuth login action */
24
+ isLoginAction?: boolean;
25
+ /** Tag as the OAuth signup action */
26
+ isSignupAction?: boolean;
27
+ };
28
+
29
+ export type ActionConstructorInputs = {
30
+ /** Unique action name (also used for default routes, etc.) */
31
+ name: string;
32
+
33
+ /** Human-friendly description (defaults to `An Action: ${name}`) */
34
+ description?: string;
35
+
36
+ /** Zod schema used to validate/coerce inputs (and for type inference) */
37
+ inputs?: z.ZodType<any>;
38
+
39
+ /** Middleware hooks to run before/after `run()` */
40
+ middleware?: ActionMiddleware[];
41
+
42
+ /** Expose this action via the MCP server (defaults to `{ enabled: true }`) */
43
+ mcp?: McpActionConfig;
44
+
45
+ /** Expose this action via HTTP (defaults: route `/${name}`, method `GET`) */
46
+ web?: {
47
+ /** HTTP route pattern (string with `:params` or a `RegExp`) */
48
+ route?: RegExp | string;
49
+ /** HTTP method to bind the route to */
50
+ method?: HTTP_METHOD;
51
+ };
52
+
53
+ /** Configure this action as a background task/job */
54
+ task?: {
55
+ /** Optional recurring frequency in milliseconds */
56
+ frequency?: number;
57
+ /** Queue name to enqueue jobs onto (defaults to `"default"`) */
58
+ queue: string;
59
+ };
60
+ };
61
+
62
+ export type ActionMiddlewareResponse = {
63
+ updatedParams?: ActionParams<Action>;
64
+ updatedResponse?: any;
65
+ };
66
+
67
+ export type ActionMiddleware = {
68
+ runBefore?: (
69
+ params: ActionParams<Action>,
70
+ connection: Connection,
71
+ ) => Promise<ActionMiddlewareResponse | void>;
72
+ runAfter?: (
73
+ params: ActionParams<Action>,
74
+ connection: Connection,
75
+ ) => Promise<ActionMiddlewareResponse | void>;
76
+ };
77
+
78
+ export abstract class Action {
79
+ name: string;
80
+ description?: string;
81
+ inputs?: z.ZodType<any>;
82
+ middleware?: ActionMiddleware[];
83
+ mcp?: McpActionConfig;
84
+ web?: {
85
+ route: RegExp | string;
86
+ method: HTTP_METHOD;
87
+ };
88
+ task?: {
89
+ frequency?: number;
90
+ queue: string;
91
+ };
92
+
93
+ constructor(args: ActionConstructorInputs) {
94
+ this.name = args.name;
95
+ this.description = args.description ?? `An Action: ${this.name}`;
96
+ this.inputs = args.inputs;
97
+ this.middleware = args.middleware ?? [];
98
+ this.mcp = { enabled: true, ...args.mcp };
99
+ this.web = {
100
+ route: args.web?.route ?? `/${this.name}`,
101
+ method: args.web?.method ?? HTTP_METHOD.GET,
102
+ };
103
+ this.task = {
104
+ frequency: args.task?.frequency,
105
+ queue: args.task?.queue ?? DEFAULT_QUEUE,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * The main "do something" method for this action.
111
+ * It can be `async`.
112
+ * Usually the goal of this run method is to return the data that you want to be sent to API consumers.
113
+ * If error is thrown in this method, it will be logged, caught, and returned to the client as `error`
114
+ * @throws {TypedError} All errors thrown should be TypedError instances
115
+ */
116
+ abstract run(
117
+ params: ActionParams<Action>,
118
+ connection?: Connection,
119
+ ): Promise<any>;
120
+ }
121
+
122
+ export type ActionParams<A extends Action> =
123
+ A["inputs"] extends z.ZodType<any>
124
+ ? z.infer<A["inputs"]>
125
+ : Record<string, unknown>;
126
+
127
+ export type ActionResponse<A extends Action> = Awaited<ReturnType<A["run"]>> &
128
+ Partial<{ error?: TypedError }>;
@@ -0,0 +1,81 @@
1
+ import type { Connection } from "./Connection";
2
+
3
+ export const CHANNEL_NAME_PATTERN = /^[a-zA-Z0-9:._-]{1,200}$/;
4
+
5
+ export type ChannelMiddlewareResponse = void;
6
+
7
+ export type ChannelMiddleware = {
8
+ /**
9
+ * Runs before a connection is allowed to subscribe to a channel.
10
+ * Throw a TypedError to deny subscription.
11
+ */
12
+ runBefore?: (
13
+ channel: string,
14
+ connection: Connection,
15
+ ) => Promise<ChannelMiddlewareResponse>;
16
+
17
+ /**
18
+ * Runs after a connection unsubscribes from a channel.
19
+ * Useful for cleanup or presence tracking.
20
+ */
21
+ runAfter?: (
22
+ channel: string,
23
+ connection: Connection,
24
+ ) => Promise<ChannelMiddlewareResponse>;
25
+ };
26
+
27
+ export type ChannelConstructorInputs = {
28
+ /**
29
+ * The name or pattern of the channel.
30
+ * Can be a string for exact match or a RegExp for pattern matching.
31
+ * Examples:
32
+ * - "messages" - matches only "messages"
33
+ * - /^room:.*$/ - matches "room:123", "room:abc", etc.
34
+ */
35
+ name: string | RegExp;
36
+ description?: string;
37
+ middleware?: ChannelMiddleware[];
38
+ };
39
+
40
+ export abstract class Channel {
41
+ name: string | RegExp;
42
+ description?: string;
43
+ middleware: ChannelMiddleware[];
44
+
45
+ constructor(args: ChannelConstructorInputs) {
46
+ this.name = args.name;
47
+ this.description = args.description ?? `A Channel: ${this.name}`;
48
+ this.middleware = args.middleware ?? [];
49
+ }
50
+
51
+ /**
52
+ * Check if this channel definition matches the requested channel name.
53
+ */
54
+ matches(channelName: string): boolean {
55
+ if (typeof this.name === "string") {
56
+ return this.name === channelName;
57
+ }
58
+ return this.name.test(channelName);
59
+ }
60
+
61
+ /**
62
+ * Optional authorization method that can be overridden for custom logic.
63
+ * Called after middleware runs, provides access to the parsed channel name.
64
+ * Throw a TypedError to deny subscription.
65
+ */
66
+ async authorize(
67
+ _channelName: string,
68
+ _connection: Connection,
69
+ ): Promise<void> {
70
+ // Default implementation allows all subscriptions
71
+ }
72
+
73
+ /**
74
+ * Returns the presence identifier for a connection in this channel.
75
+ * Override to use a custom key (e.g. user ID from session).
76
+ * Defaults to `connection.id`.
77
+ */
78
+ async presenceKey(connection: Connection): Promise<string> {
79
+ return connection.id;
80
+ }
81
+ }