serverless-ircd 0.2.0 → 0.4.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/.github/workflows/ci.yml +2 -2
- package/.github/workflows/deploy-aws.yml +1 -3
- package/.github/workflows/deploy-cf-tcp.yml +87 -0
- package/.github/workflows/deploy-cf.yml +1 -1
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +258 -18
- package/README.md +72 -30
- package/apps/aws-stack/README.md +2 -2
- package/apps/aws-stack/bin/aws.ts +7 -0
- package/apps/aws-stack/package.json +4 -4
- package/apps/aws-stack/src/aws-stack.ts +118 -6
- package/apps/aws-stack/tests/stack.test.ts +98 -3
- package/apps/cf-tcp-container/Dockerfile +69 -0
- package/apps/cf-tcp-container/package.json +34 -0
- package/apps/cf-tcp-container/src/config-loader.ts +145 -0
- package/apps/cf-tcp-container/src/container-do.ts +38 -0
- package/apps/cf-tcp-container/src/container-server.ts +363 -0
- package/apps/cf-tcp-container/src/main.ts +77 -0
- package/apps/cf-tcp-container/src/persistence.ts +144 -0
- package/apps/cf-tcp-container/src/worker.ts +41 -0
- package/apps/cf-tcp-container/terraform/provider.tf +24 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
- package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
- package/apps/cf-tcp-container/tsconfig.build.json +17 -0
- package/apps/cf-tcp-container/tsconfig.test.json +15 -0
- package/apps/cf-tcp-container/vitest.config.ts +26 -0
- package/apps/cf-tcp-container/wrangler.toml +63 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/scripts/smoke.mjs +0 -0
- package/apps/cf-worker/src/worker.ts +8 -2
- package/apps/cf-worker/tests/smoke.test.ts +1 -0
- package/apps/cf-worker/wrangler.test.toml +11 -1
- package/apps/cf-worker/wrangler.toml +69 -19
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +11 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +46 -3
- package/apps/local-cli/tests/e2e.test.ts +52 -0
- package/package.json +19 -16
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +124 -34
- package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
- package/packages/aws-adapter/src/config-loader.ts +62 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +95 -0
- package/packages/aws-adapter/src/handlers/connect.ts +64 -8
- package/packages/aws-adapter/src/handlers/default.ts +43 -2
- package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
- package/packages/aws-adapter/src/handlers/index.ts +120 -9
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +13 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +182 -0
- package/packages/aws-adapter/tests/account-store.test.ts +279 -0
- package/packages/aws-adapter/tests/admission.test.ts +70 -0
- package/packages/aws-adapter/tests/aws-harness.ts +13 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +75 -0
- package/packages/aws-adapter/tests/connect.test.ts +78 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +343 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +194 -47
- package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
- package/packages/aws-adapter/tests/transactions.test.ts +25 -20
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/cf-runtime.ts +40 -22
- package/packages/cf-adapter/src/channel-do.ts +40 -1
- package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
- package/packages/cf-adapter/src/config-loader.ts +62 -0
- package/packages/cf-adapter/src/connection-do.ts +181 -31
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +58 -8
- package/packages/cf-adapter/src/index.ts +11 -9
- package/packages/cf-adapter/tests/cf-harness.ts +11 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +40 -0
- package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
- package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
- package/packages/cf-adapter/tests/worker/main.ts +9 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- package/packages/cf-adapter/wrangler.test.toml +18 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +14 -28
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
- package/packages/irc-core/package.json +8 -2
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/caps/capabilities.ts +23 -2
- package/packages/irc-core/src/case-fold.ts +64 -0
- package/packages/irc-core/src/cloak.ts +1 -1
- package/packages/irc-core/src/commands/cap.ts +8 -1
- package/packages/irc-core/src/commands/index.ts +11 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +4 -3
- package/packages/irc-core/src/commands/kick.ts +4 -11
- package/packages/irc-core/src/commands/list.ts +5 -4
- package/packages/irc-core/src/commands/mode.ts +5 -9
- package/packages/irc-core/src/commands/motd-lines.ts +127 -0
- package/packages/irc-core/src/commands/motd.ts +6 -110
- package/packages/irc-core/src/commands/oper.ts +151 -0
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +26 -19
- package/packages/irc-core/src/commands/sasl.ts +72 -9
- package/packages/irc-core/src/commands/server-info.ts +129 -0
- package/packages/irc-core/src/commands/tagmsg.ts +205 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +84 -3
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +6 -29
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +240 -7
- package/packages/irc-core/src/protocol/numerics.ts +14 -8
- package/packages/irc-core/src/types.ts +60 -1
- package/packages/irc-core/tests/account-store.test.ts +131 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- package/packages/irc-core/tests/commands/cap.test.ts +33 -1
- package/packages/irc-core/tests/commands/ison.test.ts +166 -0
- package/packages/irc-core/tests/commands/kick.test.ts +15 -0
- package/packages/irc-core/tests/commands/oper.test.ts +257 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +256 -19
- package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
- package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +662 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/who.test.ts +22 -4
- package/packages/irc-core/tests/commands/whois.test.ts +8 -1
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +170 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/effects.test.ts +0 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +12 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- package/packages/irc-core/tsconfig.build.json +1 -1
- package/packages/irc-core/tsconfig.test.json +1 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +182 -14
- package/packages/irc-server/src/dispatch.ts +0 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +21 -0
- package/packages/irc-server/src/transport.ts +101 -0
- package/packages/irc-server/tests/actor.test.ts +617 -1
- package/packages/irc-server/tests/dispatch.test.ts +0 -17
- package/packages/irc-server/tests/routing.test.ts +7 -0
- package/packages/irc-server/tests/transport.test.ts +230 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/harness.ts +44 -9
- package/packages/irc-test-support/src/in-memory-harness.ts +78 -13
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +132 -2
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
- package/pnpm-workspace.yaml +9 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +4 -0
- package/tools/seed-aws-accounts.ts +79 -0
- package/tools/seed-cf-accounts.ts +104 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/AGENTS.md +0 -5
- package/MOTD.txt +0 -3
- package/PLAN-FIXES.md +0 -358
- package/PLAN.md +0 -420
- package/dashboards/cloudwatch-irc.json +0 -139
- package/docs/AWS-Adapter-Architecture.md +0 -494
- package/docs/AWS-Deployment.md +0 -1107
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -1
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -440
- package/progress.md +0 -107
- package/tickets.md +0 -2485
- package/webircgateway/LICENSE +0 -201
- package/webircgateway/Makefile +0 -44
- package/webircgateway/README.md +0 -134
- package/webircgateway/config.conf.example +0 -135
- package/webircgateway/go.mod +0 -16
- package/webircgateway/go.sum +0 -89
- package/webircgateway/main.go +0 -118
- package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
- package/webircgateway/pkg/identd/identd.go +0 -86
- package/webircgateway/pkg/identd/rpcclient.go +0 -59
- package/webircgateway/pkg/irc/isupport.go +0 -56
- package/webircgateway/pkg/irc/message.go +0 -217
- package/webircgateway/pkg/irc/state.go +0 -79
- package/webircgateway/pkg/proxy/proxy.go +0 -129
- package/webircgateway/pkg/proxy/server.go +0 -237
- package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
- package/webircgateway/pkg/webircgateway/client.go +0 -741
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
- package/webircgateway/pkg/webircgateway/config.go +0 -385
- package/webircgateway/pkg/webircgateway/gateway.go +0 -278
- package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
- package/webircgateway/pkg/webircgateway/hooks.go +0 -152
- package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
- package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
- package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
- package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
- package/webircgateway/pkg/webircgateway/utils.go +0 -147
- package/webircgateway/plugins/example/plugin.go +0 -11
- package/webircgateway/plugins/stats/plugin.go +0 -52
- package/webircgateway/staticcheck.conf +0 -1
- package/webircgateway/webircgateway.svg +0 -3
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CF TCP container origin — the stateful IRC server behind Cloudflare
|
|
3
|
+
* Spectrum.
|
|
4
|
+
*
|
|
5
|
+
* Spectrum terminates TLS at the edge and forwards plaintext TCP to this
|
|
6
|
+
* origin. The container runs the shared IRC core (`ConnectionActor` +
|
|
7
|
+
* `InMemoryRuntime`) over the `TcpByteStreamTransport` seam,
|
|
8
|
+
* which reassembles `\r\n`-terminated IRC lines across arbitrary TCP
|
|
9
|
+
* chunks. The actor/reducer/dispatch pipeline is identical to the wss
|
|
10
|
+
* path — only the line-framing transport differs.
|
|
11
|
+
*
|
|
12
|
+
* When `persistencePath` is supplied, channel state (topics, modes, bans)
|
|
13
|
+
* is restored from a snapshot file on boot and auto-saved on an interval
|
|
14
|
+
* plus at graceful shutdown, so a restart doesn't lose channel topology.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { type Server, type Socket, createServer } from 'node:net';
|
|
19
|
+
import { InMemoryRuntime } from '@serverless-ircd/in-memory-runtime';
|
|
20
|
+
import {
|
|
21
|
+
type AccountStore,
|
|
22
|
+
type AdmissionConfig,
|
|
23
|
+
type ChanName,
|
|
24
|
+
type Clock,
|
|
25
|
+
type ConnectionState,
|
|
26
|
+
ConsoleLogger,
|
|
27
|
+
InMemoryAccountStore,
|
|
28
|
+
InMemoryMessageStore,
|
|
29
|
+
InMemoryNickHistoryStore,
|
|
30
|
+
LogLevel,
|
|
31
|
+
type Logger,
|
|
32
|
+
type MessageStore,
|
|
33
|
+
type NickHistoryStore,
|
|
34
|
+
type RawLine,
|
|
35
|
+
createConnection,
|
|
36
|
+
} from '@serverless-ircd/irc-core';
|
|
37
|
+
import { ConnectionActor, TcpByteStreamTransport } from '@serverless-ircd/irc-server';
|
|
38
|
+
import { loadSnapshot, restoreRuntime, saveSnapshot, snapshotRuntime } from './persistence.js';
|
|
39
|
+
|
|
40
|
+
/** Options accepted by {@link startContainerServer}. */
|
|
41
|
+
export interface StartContainerOptions {
|
|
42
|
+
/** Plaintext TCP port. Use 0 for an ephemeral port. */
|
|
43
|
+
readonly port: number;
|
|
44
|
+
/** Bind address. Defaults to `0.0.0.0` (Spectrum forwards from the edge). */
|
|
45
|
+
readonly host?: string;
|
|
46
|
+
readonly serverName: string;
|
|
47
|
+
readonly networkName: string;
|
|
48
|
+
readonly motdLines?: string[];
|
|
49
|
+
readonly maxChannelsPerUser?: number;
|
|
50
|
+
readonly maxTargetsPerCommand?: number;
|
|
51
|
+
readonly maxListEntries?: number;
|
|
52
|
+
readonly nickLen?: number;
|
|
53
|
+
readonly channelLen?: number;
|
|
54
|
+
readonly topicLen?: number;
|
|
55
|
+
readonly quitMessage?: string;
|
|
56
|
+
readonly serverPassword?: string;
|
|
57
|
+
readonly cloaking?: { enabled: boolean; secret: string; cloakedSuffix?: string };
|
|
58
|
+
readonly maxConnectionsPerIp?: number;
|
|
59
|
+
readonly maxConnectionsPerUser?: number;
|
|
60
|
+
readonly perIpConnectionRate?: { max: number; windowMs: number };
|
|
61
|
+
readonly historyMaxPerChannel?: number;
|
|
62
|
+
readonly saslAccounts?: Array<{ username: string; password: string }>;
|
|
63
|
+
/**
|
|
64
|
+
* Path to the persistence snapshot file. When set, channel state is
|
|
65
|
+
* restored on boot and auto-saved every {@link snapshotIntervalMs} plus
|
|
66
|
+
* at shutdown. Omit to disable persistence.
|
|
67
|
+
*/
|
|
68
|
+
readonly persistencePath?: string;
|
|
69
|
+
/** Auto-snapshot interval in milliseconds. Defaults to 60 000. */
|
|
70
|
+
readonly snapshotIntervalMs?: number;
|
|
71
|
+
/** Injected clock (testing). Defaults to wall-clock `Date.now()`. */
|
|
72
|
+
readonly clock?: Clock;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A running container origin. Call {@link close} to shut down. */
|
|
76
|
+
export interface ContainerServer {
|
|
77
|
+
readonly port: number;
|
|
78
|
+
readonly host: string;
|
|
79
|
+
readonly runtime: InMemoryRuntime;
|
|
80
|
+
readonly messages: MessageStore;
|
|
81
|
+
readonly history: NickHistoryStore;
|
|
82
|
+
/**
|
|
83
|
+
* Test-only seam: live server-side TCP sockets. Production code MUST
|
|
84
|
+
* NOT use.
|
|
85
|
+
*/
|
|
86
|
+
readonly testSockets: ReadonlySet<Socket>;
|
|
87
|
+
close(): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface ConnectionBindings {
|
|
91
|
+
state: ConnectionState;
|
|
92
|
+
actor: ConnectionActor;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const DEFAULT_ID_FACTORY = {
|
|
96
|
+
batchId: () => randomUUID(),
|
|
97
|
+
nonce: () => randomUUID(),
|
|
98
|
+
sessionId: () => randomUUID(),
|
|
99
|
+
traceId: () => randomUUID(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const DEFAULT_MAX_CONNECTIONS_PER_IP = 10;
|
|
103
|
+
const DEFAULT_MAX_CONNECTIONS_PER_USER = 5;
|
|
104
|
+
const DEFAULT_PER_IP_CONNECTION_RATE = { max: 5, windowMs: 60_000 };
|
|
105
|
+
const DEFAULT_SNAPSHOT_INTERVAL_MS = 60_000;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Starts the TCP container origin. Resolves once the TCP listener is
|
|
109
|
+
* ready. When `persistencePath` is set and a snapshot file exists, channel
|
|
110
|
+
* state is restored before any connection is accepted.
|
|
111
|
+
*/
|
|
112
|
+
export async function startContainerServer(opts: StartContainerOptions): Promise<ContainerServer> {
|
|
113
|
+
const host = opts.host ?? '0.0.0.0';
|
|
114
|
+
const clock: Clock = opts.clock ?? { now: () => Date.now() };
|
|
115
|
+
|
|
116
|
+
const admission: AdmissionConfig | undefined =
|
|
117
|
+
opts.maxConnectionsPerIp !== undefined ||
|
|
118
|
+
opts.maxConnectionsPerUser !== undefined ||
|
|
119
|
+
opts.perIpConnectionRate !== undefined
|
|
120
|
+
? {
|
|
121
|
+
maxConnectionsPerIp: opts.maxConnectionsPerIp ?? DEFAULT_MAX_CONNECTIONS_PER_IP,
|
|
122
|
+
maxConnectionsPerUser: opts.maxConnectionsPerUser ?? DEFAULT_MAX_CONNECTIONS_PER_USER,
|
|
123
|
+
perIpConnectionRate: opts.perIpConnectionRate ?? DEFAULT_PER_IP_CONNECTION_RATE,
|
|
124
|
+
}
|
|
125
|
+
: undefined;
|
|
126
|
+
|
|
127
|
+
const runtime = new InMemoryRuntime({
|
|
128
|
+
clock,
|
|
129
|
+
...(admission !== undefined ? { admission } : {}),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const messages = new InMemoryMessageStore(opts.historyMaxPerChannel);
|
|
133
|
+
const history = new InMemoryNickHistoryStore(clock);
|
|
134
|
+
const accounts: AccountStore | undefined =
|
|
135
|
+
opts.saslAccounts !== undefined && opts.saslAccounts.length > 0
|
|
136
|
+
? new InMemoryAccountStore(opts.saslAccounts)
|
|
137
|
+
: undefined;
|
|
138
|
+
|
|
139
|
+
// Restore persisted channel state before accepting connections.
|
|
140
|
+
if (opts.persistencePath !== undefined) {
|
|
141
|
+
try {
|
|
142
|
+
const snap = await loadSnapshot(opts.persistencePath);
|
|
143
|
+
if (snap !== null) {
|
|
144
|
+
restoreRuntime(runtime, snap);
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
logError('failed to load persistence snapshot; starting fresh', err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const connections = new Map<Socket, ConnectionBindings>();
|
|
152
|
+
const sockets = new Set<Socket>();
|
|
153
|
+
|
|
154
|
+
const tcpServer = createServer((socket) => {
|
|
155
|
+
sockets.add(socket);
|
|
156
|
+
|
|
157
|
+
const ip = socket.remoteAddress ?? 'unknown';
|
|
158
|
+
const decision = runtime.admitConnection(ip, undefined);
|
|
159
|
+
if (!decision.ok) {
|
|
160
|
+
if (socket.writable) {
|
|
161
|
+
socket.write(`ERROR :Closing link: (connection limit reached: ${decision.reason})\r\n`);
|
|
162
|
+
}
|
|
163
|
+
socket.destroy();
|
|
164
|
+
sockets.delete(socket);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const admissionRecordId = decision.recordId;
|
|
168
|
+
runtime.commitAdmission(ip, undefined, admissionRecordId);
|
|
169
|
+
|
|
170
|
+
const { state, actor } = attachConnection(runtime, opts, messages, accounts, history, clock, {
|
|
171
|
+
sendText: (text) => {
|
|
172
|
+
if (socket.writable) socket.write(text);
|
|
173
|
+
},
|
|
174
|
+
closeTransport: () => socket.destroy(),
|
|
175
|
+
admissionRecordId,
|
|
176
|
+
sourceHost: ip,
|
|
177
|
+
});
|
|
178
|
+
connections.set(socket, { state, actor });
|
|
179
|
+
|
|
180
|
+
socket.on('data', (chunk: Buffer) => {
|
|
181
|
+
actor.receive(chunk.toString('utf8')).catch((err) => {
|
|
182
|
+
logError('actor failure', err);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const cleanup = (): void => {
|
|
187
|
+
connections.delete(socket);
|
|
188
|
+
sockets.delete(socket);
|
|
189
|
+
runtime.unregisterConnection(state.id);
|
|
190
|
+
};
|
|
191
|
+
socket.on('close', cleanup);
|
|
192
|
+
socket.on('error', (err) => {
|
|
193
|
+
logError('tcp socket error', err);
|
|
194
|
+
cleanup();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Auto-snapshot timer.
|
|
199
|
+
const snapshotIntervalMs = opts.snapshotIntervalMs ?? DEFAULT_SNAPSHOT_INTERVAL_MS;
|
|
200
|
+
const timer: ReturnType<typeof setInterval> | undefined =
|
|
201
|
+
opts.persistencePath !== undefined
|
|
202
|
+
? setInterval(() => {
|
|
203
|
+
void snapshotNow(runtime, opts.persistencePath as string);
|
|
204
|
+
}, snapshotIntervalMs)
|
|
205
|
+
: undefined;
|
|
206
|
+
if (timer !== undefined) timer.unref?.();
|
|
207
|
+
|
|
208
|
+
return new Promise<ContainerServer>((resolve, reject) => {
|
|
209
|
+
const onError = (err: Error): void => {
|
|
210
|
+
tcpServer.removeListener('listening', onListening);
|
|
211
|
+
reject(err);
|
|
212
|
+
};
|
|
213
|
+
const onListening = (): void => {
|
|
214
|
+
tcpServer.removeListener('error', onError);
|
|
215
|
+
const addr = tcpServer.address();
|
|
216
|
+
// c8 ignore next 1
|
|
217
|
+
const port = typeof addr === 'object' && addr !== null ? addr.port : opts.port;
|
|
218
|
+
resolve({
|
|
219
|
+
port,
|
|
220
|
+
host,
|
|
221
|
+
runtime,
|
|
222
|
+
messages,
|
|
223
|
+
history,
|
|
224
|
+
testSockets: sockets,
|
|
225
|
+
close: () =>
|
|
226
|
+
closeServer({
|
|
227
|
+
tcpServer,
|
|
228
|
+
connections,
|
|
229
|
+
sockets,
|
|
230
|
+
timer,
|
|
231
|
+
runtime,
|
|
232
|
+
persistencePath: opts.persistencePath,
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
tcpServer.once('listening', onListening);
|
|
237
|
+
tcpServer.once('error', onError);
|
|
238
|
+
tcpServer.listen(opts.port, host);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function attachConnection(
|
|
243
|
+
runtime: InMemoryRuntime,
|
|
244
|
+
opts: StartContainerOptions,
|
|
245
|
+
messages: MessageStore,
|
|
246
|
+
accounts: AccountStore | undefined,
|
|
247
|
+
history: NickHistoryStore,
|
|
248
|
+
clock: Clock,
|
|
249
|
+
transport: {
|
|
250
|
+
sendText: (text: string) => void;
|
|
251
|
+
closeTransport: () => void;
|
|
252
|
+
admissionRecordId?: string;
|
|
253
|
+
sourceHost?: string;
|
|
254
|
+
},
|
|
255
|
+
): ConnectionBindings {
|
|
256
|
+
const id = randomUUID();
|
|
257
|
+
const state = createConnection({ id, connectedSince: clock.now() });
|
|
258
|
+
if (transport.sourceHost !== undefined) {
|
|
259
|
+
state.host = transport.sourceHost;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const logger: Logger = new ConsoleLogger({ connectionId: id }, undefined, LogLevel.Info);
|
|
263
|
+
|
|
264
|
+
const actor = new ConnectionActor({
|
|
265
|
+
state,
|
|
266
|
+
runtime,
|
|
267
|
+
channels: runtime,
|
|
268
|
+
serverConfig: {
|
|
269
|
+
serverName: opts.serverName,
|
|
270
|
+
networkName: opts.networkName,
|
|
271
|
+
maxChannelsPerUser: opts.maxChannelsPerUser ?? 30,
|
|
272
|
+
maxTargetsPerCommand: opts.maxTargetsPerCommand ?? 10,
|
|
273
|
+
maxListEntries: opts.maxListEntries ?? 50,
|
|
274
|
+
nickLen: opts.nickLen ?? 30,
|
|
275
|
+
channelLen: opts.channelLen ?? 50,
|
|
276
|
+
topicLen: opts.topicLen ?? 390,
|
|
277
|
+
quitMessage: opts.quitMessage ?? 'Client Quit',
|
|
278
|
+
...(opts.serverPassword !== undefined ? { serverPassword: opts.serverPassword } : {}),
|
|
279
|
+
...(opts.cloaking !== undefined ? { cloaking: opts.cloaking } : {}),
|
|
280
|
+
},
|
|
281
|
+
clock,
|
|
282
|
+
ids: DEFAULT_ID_FACTORY,
|
|
283
|
+
motd: { lines: () => opts.motdLines ?? [] },
|
|
284
|
+
messages,
|
|
285
|
+
...(accounts !== undefined ? { accounts } : {}),
|
|
286
|
+
history,
|
|
287
|
+
logger,
|
|
288
|
+
transport: new TcpByteStreamTransport(),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
runtime.registerConnection(
|
|
292
|
+
state,
|
|
293
|
+
{
|
|
294
|
+
send: (lines: RawLine[]) => {
|
|
295
|
+
const text = `${lines.map((l) => l.text).join('\r\n')}\r\n`;
|
|
296
|
+
transport.sendText(text);
|
|
297
|
+
},
|
|
298
|
+
disconnect: (reason?: string) => {
|
|
299
|
+
if (reason !== undefined) {
|
|
300
|
+
transport.sendText(`ERROR :Closing link: (${reason})\r\n`);
|
|
301
|
+
}
|
|
302
|
+
transport.closeTransport();
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
transport.admissionRecordId,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
return { state, actor };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function closeServer(args: {
|
|
312
|
+
tcpServer: Server;
|
|
313
|
+
connections: Map<Socket, ConnectionBindings>;
|
|
314
|
+
sockets: Set<Socket>;
|
|
315
|
+
timer: ReturnType<typeof setInterval> | undefined;
|
|
316
|
+
runtime: InMemoryRuntime;
|
|
317
|
+
persistencePath: string | undefined;
|
|
318
|
+
}): Promise<void> {
|
|
319
|
+
const { tcpServer, connections, sockets, timer, runtime, persistencePath } = args;
|
|
320
|
+
|
|
321
|
+
if (timer !== undefined) clearInterval(timer);
|
|
322
|
+
|
|
323
|
+
// Destroy all live sockets so clients see the disconnect.
|
|
324
|
+
for (const socket of sockets) {
|
|
325
|
+
if (!socket.destroyed) socket.destroy();
|
|
326
|
+
}
|
|
327
|
+
sockets.clear();
|
|
328
|
+
connections.clear();
|
|
329
|
+
|
|
330
|
+
if (persistencePath !== undefined) {
|
|
331
|
+
try {
|
|
332
|
+
await snapshotNow(runtime, persistencePath);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
logError('failed to save final snapshot', err);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Stop accepting new connections. We do NOT await the callback because
|
|
339
|
+
// `server.close()` waits for every connection's 'close' event, which can
|
|
340
|
+
// hang if a destroyed socket's TCP cleanup is delayed by the OS. All
|
|
341
|
+
// sockets are already destroyed above; the server is safe to abandon.
|
|
342
|
+
tcpServer.close();
|
|
343
|
+
(tcpServer as Server & { closeAllConnections?: () => void }).closeAllConnections?.();
|
|
344
|
+
tcpServer.unref();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function snapshotNow(runtime: InMemoryRuntime, path: string): Promise<void> {
|
|
348
|
+
const state = await snapshotRuntime(runtime);
|
|
349
|
+
await saveSnapshot(path, state);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function logError(msg: string, err: unknown): void {
|
|
353
|
+
const payload = {
|
|
354
|
+
ts: new Date().toISOString(),
|
|
355
|
+
level: 'error',
|
|
356
|
+
msg,
|
|
357
|
+
err: err instanceof Error ? { name: err.name, message: err.message } : err,
|
|
358
|
+
};
|
|
359
|
+
// eslint-disable-next-line no-console
|
|
360
|
+
console.error(JSON.stringify(payload));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export type { ChanName };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CF TCP container origin entry point.
|
|
4
|
+
*
|
|
5
|
+
* Boots the IRC core as a plaintext TCP server behind Cloudflare Spectrum.
|
|
6
|
+
* Spectrum terminates TLS at the edge and forwards plaintext TCP to this
|
|
7
|
+
* process. Configuration comes from environment variables (set by the
|
|
8
|
+
* Cloudflare Container runtime, Docker, or Kubernetes).
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* TCP_PORT=6667 SERVER_NAME=irc.example.com node dist/main.js
|
|
12
|
+
*
|
|
13
|
+
* The process runs until SIGINT/SIGTERM. On shutdown it writes a final
|
|
14
|
+
* persistence snapshot (when PERSISTENCE_PATH is set) so channel state
|
|
15
|
+
* survives the next boot.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { loadContainerConfig } from './config-loader.js';
|
|
19
|
+
import { startContainerServer } from './container-server.js';
|
|
20
|
+
|
|
21
|
+
async function main(): Promise<void> {
|
|
22
|
+
const cfg = loadContainerConfig(process.env as Record<string, string | undefined>);
|
|
23
|
+
|
|
24
|
+
const server = await startContainerServer({
|
|
25
|
+
port: cfg.tcpPort,
|
|
26
|
+
host: cfg.tcpHost,
|
|
27
|
+
serverName: cfg.server.serverName,
|
|
28
|
+
networkName: cfg.server.networkName,
|
|
29
|
+
motdLines: cfg.server.motdLines,
|
|
30
|
+
maxChannelsPerUser: cfg.server.maxChannelsPerUser,
|
|
31
|
+
maxTargetsPerCommand: cfg.server.maxTargetsPerCommand,
|
|
32
|
+
maxListEntries: cfg.server.maxListEntries,
|
|
33
|
+
nickLen: cfg.server.nickLen,
|
|
34
|
+
channelLen: cfg.server.channelLen,
|
|
35
|
+
topicLen: cfg.server.topicLen,
|
|
36
|
+
quitMessage: cfg.server.quitMessage,
|
|
37
|
+
snapshotIntervalMs: cfg.snapshotIntervalMs,
|
|
38
|
+
...(cfg.persistencePath !== undefined ? { persistencePath: cfg.persistencePath } : {}),
|
|
39
|
+
...(cfg.server.saslAccounts !== undefined && cfg.server.saslAccounts.length > 0
|
|
40
|
+
? { saslAccounts: cfg.server.saslAccounts }
|
|
41
|
+
: {}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
process.stdout.write(
|
|
45
|
+
`${JSON.stringify({
|
|
46
|
+
ts: new Date().toISOString(),
|
|
47
|
+
level: 'info',
|
|
48
|
+
msg: 'cf-tcp-container listening',
|
|
49
|
+
host: server.host,
|
|
50
|
+
port: server.port,
|
|
51
|
+
persistence: cfg.persistencePath ?? null,
|
|
52
|
+
serverName: cfg.server.serverName,
|
|
53
|
+
})}\n`,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const shutdown = (signal: string): void => {
|
|
57
|
+
process.stdout.write(
|
|
58
|
+
`${JSON.stringify({ ts: new Date().toISOString(), level: 'info', msg: 'shutdown', signal })}\n`,
|
|
59
|
+
);
|
|
60
|
+
server
|
|
61
|
+
.close()
|
|
62
|
+
.then(() => process.exit(0))
|
|
63
|
+
.catch((err) => {
|
|
64
|
+
process.stderr.write(`shutdown error: ${String(err)}\n`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
69
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
main().catch((err) => {
|
|
73
|
+
process.stderr.write(
|
|
74
|
+
`fatal: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`,
|
|
75
|
+
);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime persistence for the CF TCP container origin.
|
|
3
|
+
*
|
|
4
|
+
* The container is a stateful process fronted by Cloudflare Spectrum (TLS
|
|
5
|
+
* termination). When the container restarts (crash, deploy, scale event)
|
|
6
|
+
* every live TCP connection drops — clients must reconnect. But the
|
|
7
|
+
* channel topology they built (topics, modes, ban masks) must survive so
|
|
8
|
+
* a reconnecting client doesn't find a bare server.
|
|
9
|
+
*
|
|
10
|
+
* This module snapshots the non-connection channel state (everything
|
|
11
|
+
* EXCEPT the member roster — those ConnIds are stale after a restart) to
|
|
12
|
+
* a JSON file on the container's persistent volume and restores it on
|
|
13
|
+
* boot. The shape is deliberately minimal and forward-compatible
|
|
14
|
+
* (`version` field) so future schema changes can migrate.
|
|
15
|
+
*
|
|
16
|
+
* Connection state survives origin reconnect via the runtime's
|
|
17
|
+
* persistence layer.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
21
|
+
|
|
22
|
+
import type { InMemoryRuntime } from '@serverless-ircd/in-memory-runtime';
|
|
23
|
+
import type { ChanSnapshot, ChannelModes, ChannelTopic } from '@serverless-ircd/irc-core';
|
|
24
|
+
|
|
25
|
+
/** Schema version for the persisted snapshot file. */
|
|
26
|
+
const SCHEMA_VERSION = 1;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One channel's persistent state. Mirrors {@link ChannelState} minus the
|
|
30
|
+
* member roster and pending invites — both are per-connection and cannot
|
|
31
|
+
* outlive the connections that created them.
|
|
32
|
+
*/
|
|
33
|
+
export interface PersistedChannel {
|
|
34
|
+
name: string;
|
|
35
|
+
nameLower: string;
|
|
36
|
+
modes: ChannelModes;
|
|
37
|
+
banMasks: string[];
|
|
38
|
+
topic?: ChannelTopic;
|
|
39
|
+
createdAt: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Top-level persisted state envelope. */
|
|
43
|
+
export interface PersistedState {
|
|
44
|
+
version: typeof SCHEMA_VERSION;
|
|
45
|
+
savedAt: number;
|
|
46
|
+
channels: PersistedChannel[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extracts every channel's durable state from `runtime`. Members and
|
|
51
|
+
* pending invites are omitted — they reference ConnIds that will be stale
|
|
52
|
+
* after a restart.
|
|
53
|
+
*/
|
|
54
|
+
export async function snapshotRuntime(runtime: InMemoryRuntime): Promise<PersistedState> {
|
|
55
|
+
return snapshotFromList(await runtime.listChannels());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Pure extraction from pre-fetched channel snapshots. */
|
|
59
|
+
export function snapshotFromList(channels: ReadonlyArray<ChanSnapshot>): PersistedState {
|
|
60
|
+
return {
|
|
61
|
+
version: SCHEMA_VERSION,
|
|
62
|
+
savedAt: Date.now(),
|
|
63
|
+
channels: channels.map((c) => {
|
|
64
|
+
const entry: PersistedChannel = {
|
|
65
|
+
name: c.name,
|
|
66
|
+
nameLower: c.nameLower,
|
|
67
|
+
modes: { ...c.modes },
|
|
68
|
+
banMasks: [...c.banMasks],
|
|
69
|
+
createdAt: c.createdAt,
|
|
70
|
+
};
|
|
71
|
+
if (c.topic !== undefined) entry.topic = c.topic;
|
|
72
|
+
return entry;
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Serializes a {@link PersistedState} to a JSON string. */
|
|
78
|
+
export function serializeState(state: PersistedState): string {
|
|
79
|
+
return JSON.stringify(state);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parses and validates a JSON string into a {@link PersistedState}. Throws
|
|
84
|
+
* on malformed JSON or an incompatible schema version.
|
|
85
|
+
*/
|
|
86
|
+
export function deserializeState(json: string): PersistedState {
|
|
87
|
+
const parsed = JSON.parse(json) as unknown;
|
|
88
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
89
|
+
throw new Error('invalid persisted state: expected an object');
|
|
90
|
+
}
|
|
91
|
+
const obj = parsed as Record<string, unknown>;
|
|
92
|
+
if (obj.version !== SCHEMA_VERSION) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`incompatible persisted state version: got ${String(obj.version)}, expected ${SCHEMA_VERSION}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (!Array.isArray(obj.channels)) {
|
|
98
|
+
throw new Error('invalid persisted state: channels is not an array');
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
version: SCHEMA_VERSION,
|
|
102
|
+
savedAt: typeof obj.savedAt === 'number' ? obj.savedAt : 0,
|
|
103
|
+
channels: obj.channels as PersistedChannel[],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Writes a snapshot to `filePath`. */
|
|
108
|
+
export async function saveSnapshot(filePath: string, state: PersistedState): Promise<void> {
|
|
109
|
+
await writeFile(filePath, serializeState(state), 'utf8');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Reads and parses a snapshot from `filePath`. Returns `null` when the
|
|
114
|
+
* file does not exist (first boot). Throws on corrupt content so the
|
|
115
|
+
* caller can log and continue with a fresh state.
|
|
116
|
+
*/
|
|
117
|
+
export async function loadSnapshot(filePath: string): Promise<PersistedState | null> {
|
|
118
|
+
let raw: string;
|
|
119
|
+
try {
|
|
120
|
+
raw = await readFile(filePath, 'utf8');
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
return deserializeState(raw);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Restores channels from a persisted snapshot into `runtime`. Each channel
|
|
130
|
+
* is created via `getOrCreateChannel` (so the runtime owns it) and then its
|
|
131
|
+
* modes, ban masks, topic, and original `createdAt` are overwritten. The
|
|
132
|
+
* member roster stays empty — clients reconnect and rejoin after a restart.
|
|
133
|
+
*/
|
|
134
|
+
export function restoreRuntime(runtime: InMemoryRuntime, state: PersistedState): void {
|
|
135
|
+
for (const pc of state.channels) {
|
|
136
|
+
const chan = runtime.getOrCreateChannel(pc.name);
|
|
137
|
+
chan.modes = { ...pc.modes };
|
|
138
|
+
chan.banMasks = new Set(pc.banMasks);
|
|
139
|
+
if (pc.topic !== undefined) {
|
|
140
|
+
chan.topic = pc.topic;
|
|
141
|
+
}
|
|
142
|
+
chan.createdAt = pc.createdAt;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker entry for the CF TCP container origin.
|
|
3
|
+
*
|
|
4
|
+
* This Worker is a thin management wrapper around the Cloudflare Container.
|
|
5
|
+
* It is NOT in the data path for IRC traffic — Spectrum terminates TLS on
|
|
6
|
+
* :6697 and forwards plaintext TCP directly to the container's port 6667.
|
|
7
|
+
* The Worker exists for:
|
|
8
|
+
* - Health checks (`GET /health` → container status).
|
|
9
|
+
* - Container lifecycle management (start/stop via the DO binding).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { IrcTcpOrigin } from './container-do.js';
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
16
|
+
const url = new URL(request.url);
|
|
17
|
+
|
|
18
|
+
if (url.pathname === '/health') {
|
|
19
|
+
const container = env.IRC_TCP_ORIGIN.getByName('singleton');
|
|
20
|
+
try {
|
|
21
|
+
await container.startAndWaitForPorts();
|
|
22
|
+
return new Response(JSON.stringify({ ok: true, status: 'running' }), {
|
|
23
|
+
headers: { 'content-type': 'application/json' },
|
|
24
|
+
});
|
|
25
|
+
} catch {
|
|
26
|
+
return new Response(JSON.stringify({ ok: false, status: 'error' }), {
|
|
27
|
+
status: 503,
|
|
28
|
+
headers: { 'content-type': 'application/json' },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return new Response('ServerlessIRCd TCP origin management endpoint\n', {
|
|
34
|
+
headers: { 'content-type': 'text/plain' },
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface Env {
|
|
40
|
+
IRC_TCP_ORIGIN: DurableObjectNamespace;
|
|
41
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# Terraform provider config for Cloudflare Spectrum.
|
|
3
|
+
#
|
|
4
|
+
# Configure via environment variables:
|
|
5
|
+
# export CLOUDFLARE_API_TOKEN=cf-secret-token
|
|
6
|
+
# export CLOUDFLARE_ACCOUNT_ID=your-account-id
|
|
7
|
+
#
|
|
8
|
+
# Or via a *.tfvars file passed with -var-file.
|
|
9
|
+
###############################################################################
|
|
10
|
+
|
|
11
|
+
terraform {
|
|
12
|
+
required_version = ">= 1.5"
|
|
13
|
+
required_providers {
|
|
14
|
+
cloudflare = {
|
|
15
|
+
source = "cloudflare/cloudflare"
|
|
16
|
+
version = "~> 4.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
provider "cloudflare" {
|
|
22
|
+
# api_token and account_id are read from CLOUDFLARE_API_TOKEN and
|
|
23
|
+
# CLOUDFLARE_ACCOUNT_ID environment variables.
|
|
24
|
+
}
|