serverless-ircd 0.1.0 → 0.3.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 +96 -2
- package/.github/workflows/deploy-aws.yml +129 -0
- package/.github/workflows/deploy-cf.yml +0 -2
- package/.gitmodules +3 -0
- package/CHANGELOG.md +436 -0
- package/README.md +127 -84
- package/apps/aws-stack/README.md +73 -0
- package/apps/aws-stack/bin/aws.ts +49 -0
- package/apps/aws-stack/cdk.json +10 -0
- package/apps/aws-stack/package.json +41 -0
- package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
- package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
- package/apps/aws-stack/scripts/smoke.mjs +142 -0
- package/apps/aws-stack/src/aws-stack.ts +263 -0
- package/apps/aws-stack/src/tables.ts +10 -0
- package/apps/aws-stack/tests/localstack.test.ts +46 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
- package/apps/aws-stack/tests/stack.test.ts +464 -0
- package/apps/aws-stack/tsconfig.build.json +11 -0
- package/apps/aws-stack/tsconfig.test.json +8 -0
- package/apps/aws-stack/vitest.config.ts +20 -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 +5 -1
- package/apps/cf-worker/wrangler.toml +66 -17
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +57 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +267 -32
- package/apps/local-cli/tests/config-loader.test.ts +107 -0
- package/apps/local-cli/tests/e2e.test.ts +126 -0
- package/apps/local-cli/tests/security-e2e.test.ts +239 -0
- package/biome.json +3 -1
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +93 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +86 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1186 -0
- package/docs/Cloudflare-Deployment-Guide.md +660 -0
- package/docs/Home.md +11 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanIRCv3Websocket.md +489 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/package.json +19 -14
- package/packages/aws-adapter/README.md +35 -0
- package/packages/aws-adapter/package.json +52 -0
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/aws-runtime.ts +871 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +73 -0
- package/packages/aws-adapter/src/config-loader.ts +126 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +156 -0
- package/packages/aws-adapter/src/dynamo.ts +61 -0
- package/packages/aws-adapter/src/handlers/connect.ts +44 -0
- package/packages/aws-adapter/src/handlers/default.ts +330 -0
- package/packages/aws-adapter/src/handlers/disconnect.ts +48 -0
- package/packages/aws-adapter/src/handlers/index.ts +293 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
- package/packages/aws-adapter/src/handlers/state.ts +13 -0
- package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
- package/packages/aws-adapter/src/index.ts +74 -0
- package/packages/aws-adapter/src/message-store.ts +34 -0
- package/packages/aws-adapter/src/serialize.ts +283 -0
- package/packages/aws-adapter/src/tables.ts +53 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +171 -0
- package/packages/aws-adapter/tests/account-store.test.ts +280 -0
- package/packages/aws-adapter/tests/aws-harness.ts +408 -0
- package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
- package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +213 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +336 -0
- package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
- package/packages/aws-adapter/tests/global-setup.ts +301 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
- package/packages/aws-adapter/tests/handlers.test.ts +473 -0
- package/packages/aws-adapter/tests/message-store.test.ts +55 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
- package/packages/aws-adapter/tests/serialize.test.ts +249 -0
- package/packages/aws-adapter/tests/smoke.test.ts +48 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
- package/packages/aws-adapter/tests/tables.test.ts +74 -0
- package/packages/aws-adapter/tests/transactions.test.ts +446 -0
- package/packages/aws-adapter/tsconfig.build.json +16 -0
- package/packages/aws-adapter/tsconfig.test.json +14 -0
- package/packages/aws-adapter/vitest.config.ts +23 -0
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/cf-runtime.ts +42 -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 +135 -0
- package/packages/cf-adapter/src/connection-do.ts +119 -0
- package/packages/cf-adapter/src/env.ts +27 -1
- package/packages/cf-adapter/src/index.ts +2 -1
- package/packages/cf-adapter/tests/cf-harness.ts +2 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
- package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +82 -0
- package/packages/cf-adapter/tests/worker/main.ts +2 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- package/packages/cf-adapter/wrangler.test.toml +10 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +107 -16
- package/packages/in-memory-runtime/src/index.ts +1 -1
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +134 -0
- package/packages/irc-core/package.json +13 -2
- package/packages/irc-core/src/admission.ts +216 -0
- package/packages/irc-core/src/caps/capabilities.ts +1 -0
- package/packages/irc-core/src/case-fold.ts +64 -0
- package/packages/irc-core/src/cloak.ts +81 -0
- package/packages/irc-core/src/commands/cap.ts +1 -2
- package/packages/irc-core/src/commands/chathistory.ts +305 -0
- package/packages/irc-core/src/commands/index.ts +9 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +63 -10
- 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/privmsg.ts +24 -0
- package/packages/irc-core/src/commands/registration.ts +79 -21
- package/packages/irc-core/src/commands/sasl.ts +251 -0
- package/packages/irc-core/src/commands/tagmsg.ts +205 -0
- package/packages/irc-core/src/config.ts +270 -0
- package/packages/irc-core/src/flood-control.ts +175 -0
- package/packages/irc-core/src/index.ts +7 -0
- package/packages/irc-core/src/ports.ts +719 -0
- package/packages/irc-core/src/protocol/base64.ts +16 -0
- package/packages/irc-core/src/protocol/index.ts +2 -1
- package/packages/irc-core/src/protocol/numerics.ts +11 -0
- package/packages/irc-core/src/protocol/parser.ts +27 -2
- package/packages/irc-core/src/state/connection.ts +41 -0
- package/packages/irc-core/src/types.ts +88 -2
- package/packages/irc-core/stryker.commands.conf.json +41 -0
- package/packages/irc-core/stryker.protocol.conf.json +26 -0
- package/packages/irc-core/tests/account-store.test.ts +88 -0
- package/packages/irc-core/tests/admission.test.ts +229 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- package/packages/irc-core/tests/cloak.test.ts +78 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
- package/packages/irc-core/tests/commands/join.test.ts +246 -2
- 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/privmsg.test.ts +146 -2
- package/packages/irc-core/tests/commands/registration.test.ts +380 -20
- package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +688 -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/config.test.ts +721 -0
- package/packages/irc-core/tests/flood-control.test.ts +394 -0
- package/packages/irc-core/tests/message-store.test.ts +530 -0
- package/packages/irc-core/tests/parser.test.ts +44 -1
- package/packages/irc-core/tests/ports.test.ts +263 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +186 -44
- package/packages/irc-server/src/dispatch.ts +89 -5
- package/packages/irc-server/src/index.ts +2 -0
- package/packages/irc-server/src/routing.ts +160 -0
- package/packages/irc-server/tests/actor.test.ts +690 -15
- package/packages/irc-server/tests/dispatch.test.ts +84 -0
- package/packages/irc-server/tests/routing.test.ts +204 -0
- package/packages/irc-test-support/README.md +32 -0
- package/packages/irc-test-support/package.json +37 -0
- package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +6 -5
- package/packages/irc-test-support/src/index.ts +24 -0
- package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
- package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
- package/packages/irc-test-support/tests/smoke.test.ts +11 -0
- package/packages/irc-test-support/tsconfig.build.json +16 -0
- package/packages/irc-test-support/tsconfig.test.json +14 -0
- package/packages/irc-test-support/vitest.config.ts +20 -0
- package/pnpm-workspace.yaml +1 -0
- package/tools/ci-hardening/package.json +30 -0
- package/tools/ci-hardening/src/index.ts +6 -0
- package/tools/ci-hardening/src/validate.ts +103 -0
- package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
- package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
- package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
- package/tools/ci-hardening/tests/validate.test.ts +177 -0
- package/tools/ci-hardening/tsconfig.build.json +12 -0
- package/tools/ci-hardening/tsconfig.test.json +10 -0
- package/tools/ci-hardening/vitest.config.ts +21 -0
- package/tools/seed-aws-accounts.ts +80 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
- package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
- package/tools/tcp-ws-forwarder/src/main.ts +33 -14
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
- package/packages/cf-adapter/tests/integration/index.ts +0 -17
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
MUTATION_SCORE_THRESHOLD,
|
|
6
|
+
readCoverageThresholds,
|
|
7
|
+
readStrykerConfig,
|
|
8
|
+
readWorkflowJob,
|
|
9
|
+
} from '../src/validate.js';
|
|
10
|
+
|
|
11
|
+
const REPO_ROOT = resolve(import.meta.dirname, '..', '..', '..');
|
|
12
|
+
|
|
13
|
+
function repoPath(rel: string): string {
|
|
14
|
+
return resolve(REPO_ROOT, rel);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readText(rel: string): string {
|
|
18
|
+
return readFileSync(repoPath(rel), 'utf8');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('coverage gate', () => {
|
|
22
|
+
it('enforces 100% line/function/branch/statement coverage on irc-core', () => {
|
|
23
|
+
const thresholds = readCoverageThresholds(repoPath('packages/irc-core/vitest.config.ts'));
|
|
24
|
+
expect(thresholds).toEqual({
|
|
25
|
+
lines: 100,
|
|
26
|
+
functions: 100,
|
|
27
|
+
branches: 100,
|
|
28
|
+
statements: 100,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('enforces ≥90% coverage on cf-adapter', () => {
|
|
33
|
+
const thresholds = readCoverageThresholds(repoPath('packages/cf-adapter/vitest.config.ts'));
|
|
34
|
+
expect(thresholds.lines).toBeGreaterThanOrEqual(90);
|
|
35
|
+
expect(thresholds.functions).toBeGreaterThanOrEqual(90);
|
|
36
|
+
expect(thresholds.branches).toBeGreaterThanOrEqual(90);
|
|
37
|
+
expect(thresholds.statements).toBeGreaterThanOrEqual(90);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('enforces ≥90% coverage on irc-server', () => {
|
|
41
|
+
const thresholds = readCoverageThresholds(repoPath('packages/irc-server/vitest.config.ts'));
|
|
42
|
+
expect(thresholds.lines).toBeGreaterThanOrEqual(90);
|
|
43
|
+
expect(thresholds.functions).toBeGreaterThanOrEqual(90);
|
|
44
|
+
expect(thresholds.branches).toBeGreaterThanOrEqual(90);
|
|
45
|
+
expect(thresholds.statements).toBeGreaterThanOrEqual(90);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('enforces ≥90% coverage on in-memory-runtime', () => {
|
|
49
|
+
const thresholds = readCoverageThresholds(
|
|
50
|
+
repoPath('packages/in-memory-runtime/vitest.config.ts'),
|
|
51
|
+
);
|
|
52
|
+
expect(thresholds.lines).toBeGreaterThanOrEqual(90);
|
|
53
|
+
expect(thresholds.functions).toBeGreaterThanOrEqual(90);
|
|
54
|
+
expect(thresholds.branches).toBeGreaterThanOrEqual(90);
|
|
55
|
+
expect(thresholds.statements).toBeGreaterThanOrEqual(90);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('contract tests on every PR', () => {
|
|
60
|
+
it('runs the integration contract suite on PRs via ci.yml', () => {
|
|
61
|
+
const ci = readText('.github/workflows/ci.yml');
|
|
62
|
+
// Pull request trigger must be present so contract suite runs on PRs.
|
|
63
|
+
expect(ci).toMatch(/pull_request:/);
|
|
64
|
+
// The integration contract suite must be invoked. It ships as
|
|
65
|
+
// packages/cf-adapter/tests/integration (the parametrized harness
|
|
66
|
+
// that runs against every runtime) and is exercised by the
|
|
67
|
+
// workspace-wide coverage task. The gate step must explicitly
|
|
68
|
+
// reference the integration suite so it cannot be silently dropped.
|
|
69
|
+
expect(ci).toMatch(/integration/);
|
|
70
|
+
// Coverage must run on PRs (not only on main pushes).
|
|
71
|
+
const job = readWorkflowJob(ci, 'verify');
|
|
72
|
+
const runSteps = job.steps
|
|
73
|
+
.filter((s: { run?: unknown }) => typeof s.run === 'string')
|
|
74
|
+
.map((s: { run?: string }) => s.run ?? '');
|
|
75
|
+
const runsCoverage = runSteps.some((r) => r.includes('pnpm coverage'));
|
|
76
|
+
expect(runsCoverage).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('has an explicit coverage-gate step that fails the build on regressions', () => {
|
|
80
|
+
const ci = readText('.github/workflows/ci.yml');
|
|
81
|
+
// The gate step name must make the failure mode obvious in the
|
|
82
|
+
// GitHub Actions UI.
|
|
83
|
+
expect(ci).toMatch(/coverage gate/i);
|
|
84
|
+
// The gate step must surface an explicit failure marker rather than
|
|
85
|
+
// relying on vitest's implicit threshold exit code alone.
|
|
86
|
+
expect(ci).toMatch(/coverage-thresholds/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('mutation testing spot-check', () => {
|
|
91
|
+
it('exports a documented mutation-score threshold of ≥80%', () => {
|
|
92
|
+
expect(MUTATION_SCORE_THRESHOLD).toBeGreaterThanOrEqual(80);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('ships a Stryker config that targets irc-core/protocol', () => {
|
|
96
|
+
const config = readStrykerConfig(repoPath('packages/irc-core/stryker.protocol.conf.json'));
|
|
97
|
+
expect(config.mutate).toEqual(
|
|
98
|
+
expect.arrayContaining([expect.stringMatching(/protocol\/[^/]+\.ts$/)]),
|
|
99
|
+
);
|
|
100
|
+
expect(config.thresholds?.high).toBeGreaterThanOrEqual(MUTATION_SCORE_THRESHOLD);
|
|
101
|
+
expect(config.thresholds?.break).toBe(MUTATION_SCORE_THRESHOLD);
|
|
102
|
+
// The config must disable Stryker's own coverage runner and reuse
|
|
103
|
+
// vitest's so the per-package coverage gate stays the source of
|
|
104
|
+
// truth for line/function coverage.
|
|
105
|
+
expect(config.coverageAnalysis).toBe('off');
|
|
106
|
+
expect(config.testRunner).toBe('vitest');
|
|
107
|
+
// Under pnpm's symlinked node_modules the Stryker child process
|
|
108
|
+
// cannot auto-discover the vitest plugin, so it must be listed
|
|
109
|
+
// explicitly.
|
|
110
|
+
expect(config.plugins).toEqual(['@stryker-mutator/vitest-runner']);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('ships a Stryker config that targets irc-core/commands', () => {
|
|
114
|
+
const config = readStrykerConfig(repoPath('packages/irc-core/stryker.commands.conf.json'));
|
|
115
|
+
expect(config.mutate).toEqual(
|
|
116
|
+
expect.arrayContaining([expect.stringMatching(/commands\/[^/]+\.ts$/)]),
|
|
117
|
+
);
|
|
118
|
+
expect(config.thresholds?.high).toBeGreaterThanOrEqual(MUTATION_SCORE_THRESHOLD);
|
|
119
|
+
expect(config.thresholds?.break).toBe(MUTATION_SCORE_THRESHOLD);
|
|
120
|
+
expect(config.coverageAnalysis).toBe('off');
|
|
121
|
+
expect(config.testRunner).toBe('vitest');
|
|
122
|
+
expect(config.plugins).toEqual(['@stryker-mutator/vitest-runner']);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('exposes a workspace-level mutation script that runs both spot-checks', () => {
|
|
126
|
+
const root = JSON.parse(readText('package.json')) as { scripts: Record<string, string> };
|
|
127
|
+
// Root scripts delegate to the irc-core package's stryker scripts.
|
|
128
|
+
expect(root.scripts['mutation:protocol']).toMatch(
|
|
129
|
+
/--filter @serverless-ircd\/irc-core mutation:protocol/,
|
|
130
|
+
);
|
|
131
|
+
expect(root.scripts['mutation:commands']).toMatch(
|
|
132
|
+
/--filter @serverless-ircd\/irc-core mutation:commands/,
|
|
133
|
+
);
|
|
134
|
+
expect(root.scripts.mutation).toMatch(/mutation:protocol/);
|
|
135
|
+
expect(root.scripts.mutation).toMatch(/mutation:commands/);
|
|
136
|
+
|
|
137
|
+
// The irc-core package must actually invoke stryker.
|
|
138
|
+
const ircCore = JSON.parse(readText('packages/irc-core/package.json')) as {
|
|
139
|
+
scripts: Record<string, string>;
|
|
140
|
+
};
|
|
141
|
+
expect(ircCore.scripts['mutation:protocol']).toMatch(
|
|
142
|
+
/stryker run stryker\.protocol\.conf\.json/,
|
|
143
|
+
);
|
|
144
|
+
expect(ircCore.scripts['mutation:commands']).toMatch(
|
|
145
|
+
/stryker run stryker\.commands\.conf\.json/,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('runs mutation spot-checks in CI on every PR', () => {
|
|
150
|
+
const ci = readText('.github/workflows/ci.yml');
|
|
151
|
+
expect(ci).toMatch(/mutation/i);
|
|
152
|
+
expect(ci).toMatch(/stryker/);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('validate.ts error paths', () => {
|
|
157
|
+
it('readCoverageThresholds throws when no thresholds block exists', () => {
|
|
158
|
+
const tmpPath = repoPath('tools/ci-hardening/tests/__no_thresholds__.txt');
|
|
159
|
+
expect(() => readCoverageThresholds(tmpPath)).toThrow(/no thresholds block|No coverage/i);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('readCoverageThresholds throws when a threshold key is missing', () => {
|
|
163
|
+
const tmpPath = repoPath('tools/ci-hardening/tests/__partial_thresholds__.txt');
|
|
164
|
+
expect(() => readCoverageThresholds(tmpPath)).toThrow(/No lines threshold/i);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('readStrykerConfig throws when mutate is missing', () => {
|
|
168
|
+
const tmpPath = repoPath('tools/ci-hardening/tests/__stryker_bad__.json');
|
|
169
|
+
expect(() => readStrykerConfig(tmpPath)).toThrow(/missing mutate/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('readWorkflowJob throws when the job does not exist', () => {
|
|
173
|
+
expect(() => readWorkflowJob('jobs:\n other: {}\n', 'verify')).toThrow(
|
|
174
|
+
/no job named "verify"/,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"tsBuildInfoFile": "./.tsbuildinfo",
|
|
8
|
+
"types": ["node"]
|
|
9
|
+
},
|
|
10
|
+
"include": ["src/**/*.ts"],
|
|
11
|
+
"exclude": ["dist", "tests", "**/*.test.ts"]
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
environment: 'node',
|
|
6
|
+
include: ['tests/**/*.test.ts'],
|
|
7
|
+
coverage: {
|
|
8
|
+
provider: 'v8',
|
|
9
|
+
all: false,
|
|
10
|
+
include: ['src/**/*.ts'],
|
|
11
|
+
exclude: ['src/index.ts'],
|
|
12
|
+
reporter: ['text', 'html', 'json-summary'],
|
|
13
|
+
thresholds: {
|
|
14
|
+
lines: 100,
|
|
15
|
+
functions: 100,
|
|
16
|
+
branches: 100,
|
|
17
|
+
statements: 100,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Seed SASL PLAIN accounts into the DynamoDB `Accounts` table.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node --import tsx tools/seed-aws-accounts.ts \
|
|
7
|
+
* --table StagingAccounts \
|
|
8
|
+
* --endpoint http://localhost:8000 \
|
|
9
|
+
* --accounts alice:s3cret bob:password2
|
|
10
|
+
*
|
|
11
|
+
* Or read from a file (newline-delimited `username:password`):
|
|
12
|
+
* node --import tsx tools/seed-aws-accounts.ts \
|
|
13
|
+
* --table StagingAccounts \
|
|
14
|
+
* --file accounts.txt
|
|
15
|
+
*
|
|
16
|
+
* The script hashes each password with scrypt (never writes plaintext).
|
|
17
|
+
* Re-running with the same username overwrites the prior row.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { parseArgs } from 'node:util';
|
|
21
|
+
import { readFileSync } from 'node:fs';
|
|
22
|
+
import {
|
|
23
|
+
createDynamoDocumentClient,
|
|
24
|
+
putAccountCredential,
|
|
25
|
+
} from '@serverless-ircd/aws-adapter';
|
|
26
|
+
|
|
27
|
+
const { values } = parseArgs({
|
|
28
|
+
options: {
|
|
29
|
+
table: { type: 'string' },
|
|
30
|
+
endpoint: { type: 'string', default: undefined },
|
|
31
|
+
region: { type: 'string', default: undefined },
|
|
32
|
+
accounts: { type: 'string', multiple: true, default: [] },
|
|
33
|
+
file: { type: 'string', default: undefined },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (values.table === undefined) {
|
|
38
|
+
console.error('Usage: seed-aws-accounts --table <TableName> [--endpoint <url>] [--accounts user:pass ...] [--file <path>]');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const clientOptions: Record<string, string> = {};
|
|
43
|
+
if (values.endpoint !== undefined) clientOptions.endpoint = values.endpoint;
|
|
44
|
+
if (values.region !== undefined) clientOptions.region = values.region;
|
|
45
|
+
|
|
46
|
+
const docClient = createDynamoDocumentClient(clientOptions);
|
|
47
|
+
|
|
48
|
+
const pairs: Array<{ username: string; password: string }> = [];
|
|
49
|
+
|
|
50
|
+
for (const pair of values.accounts) {
|
|
51
|
+
const sep = pair.indexOf(':');
|
|
52
|
+
if (sep <= 0) {
|
|
53
|
+
console.error(`Skipping malformed entry (no colon): ${pair}`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
pairs.push({ username: pair.slice(0, sep), password: pair.slice(sep + 1) });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (values.file !== undefined) {
|
|
60
|
+
const content = readFileSync(values.file, 'utf-8');
|
|
61
|
+
for (const line of content.split('\n')) {
|
|
62
|
+
const trimmed = line.trim();
|
|
63
|
+
if (trimmed.length === 0) continue;
|
|
64
|
+
const sep = trimmed.indexOf(':');
|
|
65
|
+
if (sep <= 0) continue;
|
|
66
|
+
pairs.push({ username: trimmed.slice(0, sep), password: trimmed.slice(sep + 1) });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (pairs.length === 0) {
|
|
71
|
+
console.error('No accounts to seed. Use --accounts or --file.');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const { username, password } of pairs) {
|
|
76
|
+
const entry = await putAccountCredential(docClient, values.table, username, password);
|
|
77
|
+
console.log(`Seeded ${username} (algorithm=${entry.algorithm})`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(`Done: ${pairs.length} account(s) written to ${values.table}.`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverless-ircd/tcp-ws-forwarder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Local TCP to ws/wss forwarder so standard IRC clients can reach the WebSocket-only ServerlessIRCd transport",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import { type Socket, createServer } from 'node:net';
|
|
30
30
|
import { type RawData, WebSocket } from 'ws';
|
|
31
31
|
import { LineScanner } from './line-scanner.js';
|
|
32
|
+
import { ConsoleLogger, LogLevel, type Logger } from './logger.js';
|
|
32
33
|
|
|
33
34
|
/** Options accepted by {@link startForwarder}. */
|
|
34
35
|
export interface StartForwarderOptions {
|
|
@@ -41,6 +42,12 @@ export interface StartForwarderOptions {
|
|
|
41
42
|
* `ws:` or `wss:` scheme; otherwise `startForwarder` throws synchronously.
|
|
42
43
|
*/
|
|
43
44
|
targetUrl: string;
|
|
45
|
+
/**
|
|
46
|
+
* Minimum log severity to emit. `debug` traces every relayed line/frame;
|
|
47
|
+
* `info` (default) logs connection lifecycle; `warn`/`error` quiet the
|
|
48
|
+
* bridge to faults only. Mirrors the irc-core `LogLevel` ordering.
|
|
49
|
+
*/
|
|
50
|
+
logLevel?: LogLevel;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
/** A running forwarder. Call {@link Forwarder.close} to shut it down. */
|
|
@@ -87,6 +94,8 @@ export function startForwarder(opts: StartForwarderOptions): Promise<Forwarder>
|
|
|
87
94
|
const target = parseTargetUrl(opts.targetUrl);
|
|
88
95
|
const listenHost = opts.listenHost ?? '127.0.0.1';
|
|
89
96
|
const targetHref = target.href;
|
|
97
|
+
const logger: Logger = new ConsoleLogger(undefined, opts.logLevel ?? LogLevel.Info);
|
|
98
|
+
let nextConnId = 1;
|
|
90
99
|
|
|
91
100
|
const bridges = new Map<Socket, () => void>();
|
|
92
101
|
const sockets = new Set<Socket>();
|
|
@@ -94,7 +103,7 @@ export function startForwarder(opts: StartForwarderOptions): Promise<Forwarder>
|
|
|
94
103
|
|
|
95
104
|
const server = createServer((tcp) => {
|
|
96
105
|
sockets.add(tcp);
|
|
97
|
-
bridges.set(tcp, bridgeConnection(tcp, targetHref, bridges, sockets));
|
|
106
|
+
bridges.set(tcp, bridgeConnection(tcp, targetHref, bridges, sockets, logger, nextConnId++));
|
|
98
107
|
});
|
|
99
108
|
|
|
100
109
|
const close = (): Promise<void> => {
|
|
@@ -132,6 +141,9 @@ export function startForwarder(opts: StartForwarderOptions): Promise<Forwarder>
|
|
|
132
141
|
});
|
|
133
142
|
}
|
|
134
143
|
|
|
144
|
+
/** Why a bridge tore down. Surfaces in the `bridge.closed` log record. */
|
|
145
|
+
type CloseReason = 'shutdown' | 'tcp-close' | 'tcp-error' | 'ws-close' | 'ws-error';
|
|
146
|
+
|
|
135
147
|
/**
|
|
136
148
|
* Wires one TCP socket to one fresh WebSocket. Returns a teardown closure
|
|
137
149
|
* that closes both ends idempotently; the caller registers it in `bridges`
|
|
@@ -142,12 +154,17 @@ function bridgeConnection(
|
|
|
142
154
|
targetHref: string,
|
|
143
155
|
bridges: Map<Socket, () => void>,
|
|
144
156
|
sockets: Set<Socket>,
|
|
157
|
+
logger: Logger,
|
|
158
|
+
connId: number,
|
|
145
159
|
): () => void {
|
|
146
160
|
const ws = new WebSocket(targetHref);
|
|
147
161
|
const outgoing = new LineScanner();
|
|
148
162
|
const pending: string[] = [];
|
|
149
163
|
let wsReady = false;
|
|
150
164
|
let bridgeClosed = false;
|
|
165
|
+
const remote = `${tcp.remoteAddress ?? '?'}:${tcp.remotePort ?? '?'}`;
|
|
166
|
+
|
|
167
|
+
logger.info('bridge.accepted', { conn: connId, remote });
|
|
151
168
|
|
|
152
169
|
const sendLine = (line: string): void => {
|
|
153
170
|
if (wsReady) ws.send(line);
|
|
@@ -156,28 +173,35 @@ function bridgeConnection(
|
|
|
156
173
|
|
|
157
174
|
tcp.on('data', (chunk: Buffer) => {
|
|
158
175
|
for (const line of outgoing.push(chunk)) {
|
|
159
|
-
if (line.length > 0)
|
|
176
|
+
if (line.length > 0) {
|
|
177
|
+
logger.debug('tcp.line', { conn: connId, line });
|
|
178
|
+
sendLine(line);
|
|
179
|
+
}
|
|
160
180
|
}
|
|
161
181
|
});
|
|
162
182
|
|
|
163
183
|
ws.on('open', () => {
|
|
164
184
|
wsReady = true;
|
|
185
|
+
const flushed = pending.length;
|
|
165
186
|
for (const line of pending) ws.send(line);
|
|
166
187
|
pending.length = 0;
|
|
188
|
+
logger.debug('ws.open', { conn: connId, flushed });
|
|
167
189
|
});
|
|
168
190
|
|
|
169
191
|
ws.on('message', (data: RawData) => {
|
|
170
192
|
const text = toBuffer(data).toString('utf8');
|
|
193
|
+
logger.debug('ws.frame', { conn: connId, frame: text });
|
|
171
194
|
for (const line of splitFrameLines(text)) {
|
|
172
195
|
if (line.length > 0) tcp.write(`${line}\r\n`);
|
|
173
196
|
}
|
|
174
197
|
});
|
|
175
198
|
|
|
176
|
-
const teardown = (): void => {
|
|
199
|
+
const teardown = (reason: CloseReason): void => {
|
|
177
200
|
if (bridgeClosed) return;
|
|
178
201
|
bridgeClosed = true;
|
|
179
202
|
bridges.delete(tcp);
|
|
180
203
|
sockets.delete(tcp);
|
|
204
|
+
logger.info('bridge.closed', { conn: connId, reason });
|
|
181
205
|
// ws.close() does not throw: on a CONNECTING socket it aborts the
|
|
182
206
|
// handshake and emits an 'error' event, which our handler suppresses
|
|
183
207
|
// via the `bridgeClosed` guard set above.
|
|
@@ -185,25 +209,25 @@ function bridgeConnection(
|
|
|
185
209
|
tcp.destroy();
|
|
186
210
|
};
|
|
187
211
|
|
|
188
|
-
tcp.on('close', teardown);
|
|
212
|
+
tcp.on('close', () => teardown('tcp-close'));
|
|
189
213
|
tcp.on('error', (err) => {
|
|
190
214
|
// Errors that arrive after we already tore this bridge down (e.g. an
|
|
191
215
|
// RST echoing our own `destroy()`) are expected — don't log them.
|
|
192
216
|
if (bridgeClosed) return;
|
|
193
|
-
|
|
194
|
-
teardown();
|
|
217
|
+
logger.error('forwarder tcp socket error', { conn: connId, err });
|
|
218
|
+
teardown('tcp-error');
|
|
195
219
|
});
|
|
196
|
-
ws.on('close', teardown);
|
|
220
|
+
ws.on('close', () => teardown('ws-close'));
|
|
197
221
|
ws.on('error', (err) => {
|
|
198
222
|
// Same suppression as above; also covers the benign "closed before the
|
|
199
223
|
// connection was established" event ws emits when we close() a socket
|
|
200
224
|
// that was still CONNECTING at shutdown time.
|
|
201
225
|
if (bridgeClosed) return;
|
|
202
|
-
|
|
203
|
-
teardown();
|
|
226
|
+
logger.error('forwarder upstream ws error', { conn: connId, err });
|
|
227
|
+
teardown('ws-error');
|
|
204
228
|
});
|
|
205
229
|
|
|
206
|
-
return teardown;
|
|
230
|
+
return () => teardown('shutdown');
|
|
207
231
|
}
|
|
208
232
|
|
|
209
233
|
/**
|
|
@@ -229,16 +253,3 @@ export function toBuffer(data: RawData): Buffer {
|
|
|
229
253
|
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
230
254
|
return Buffer.from(data as Uint8Array);
|
|
231
255
|
}
|
|
232
|
-
|
|
233
|
-
function logError(msg: string, err: unknown): void {
|
|
234
|
-
const payload = {
|
|
235
|
-
ts: new Date().toISOString(),
|
|
236
|
-
level: 'error',
|
|
237
|
-
msg,
|
|
238
|
-
err:
|
|
239
|
-
err instanceof Error
|
|
240
|
-
? { name: err.name, message: err.message, stack: err.stack }
|
|
241
|
-
: String(err),
|
|
242
|
-
};
|
|
243
|
-
console.error(JSON.stringify(payload));
|
|
244
|
-
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained structured logger for the TCP↔WS forwarder.
|
|
3
|
+
*
|
|
4
|
+
* The forwarder is deliberately dependency-free (no `@serverless-ircd/*`
|
|
5
|
+
* import) so it can ship as a standalone tool, but it mirrors the irc-core
|
|
6
|
+
* logging conventions so output is consistent across the monorepo:
|
|
7
|
+
*
|
|
8
|
+
* - numeric {@link LogLevel} enum with `debug < info < warn < error`;
|
|
9
|
+
* - one JSON line per record (`{ ts, level, msg, ...fields }`);
|
|
10
|
+
* - `minLevel` filtering;
|
|
11
|
+
* - injectable sinks (so tests never touch the real `console`); and
|
|
12
|
+
* - automatic `Error` serialization — any field value that is an `Error`
|
|
13
|
+
* becomes `{ name, message, stack }`, matching the legacy `logError`
|
|
14
|
+
* output the existing tests assert on.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Severity. Ordering matters: a logger set to `Info` suppresses `Debug`. */
|
|
18
|
+
export enum LogLevel {
|
|
19
|
+
Debug = 10,
|
|
20
|
+
Info = 20,
|
|
21
|
+
Warn = 30,
|
|
22
|
+
Error = 40,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** String label written as the `level` field of each record. */
|
|
26
|
+
export function logLevelLabel(level: LogLevel): LogLevelLabel {
|
|
27
|
+
switch (level) {
|
|
28
|
+
case LogLevel.Debug:
|
|
29
|
+
return 'debug';
|
|
30
|
+
case LogLevel.Info:
|
|
31
|
+
return 'info';
|
|
32
|
+
case LogLevel.Warn:
|
|
33
|
+
return 'warn';
|
|
34
|
+
case LogLevel.Error:
|
|
35
|
+
return 'error';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A parsed, case-insensitive log-level label. */
|
|
40
|
+
export type LogLevelLabel = 'debug' | 'info' | 'warn' | 'error';
|
|
41
|
+
|
|
42
|
+
const LABEL_TO_LEVEL: Record<LogLevelLabel, LogLevel> = {
|
|
43
|
+
debug: LogLevel.Debug,
|
|
44
|
+
info: LogLevel.Info,
|
|
45
|
+
warn: LogLevel.Warn,
|
|
46
|
+
error: LogLevel.Error,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parses a log-level label (case-insensitive, whitespace-trimmed) into a
|
|
51
|
+
* {@link LogLevel}. Throws on anything that is not one of the four canonical
|
|
52
|
+
* labels so a typo at startup fails loudly instead of silently degrading to a
|
|
53
|
+
* default.
|
|
54
|
+
*/
|
|
55
|
+
export function parseLogLevel(raw: string): LogLevel {
|
|
56
|
+
const key = raw.trim().toLowerCase();
|
|
57
|
+
if (key in LABEL_TO_LEVEL) {
|
|
58
|
+
return LABEL_TO_LEVEL[key as LogLevelLabel];
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`unknown log level "${raw}" (expected one of: debug, info, warn, error)`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Per-level output sinks. Defaults to the global `console`. */
|
|
64
|
+
export interface LoggerSinks {
|
|
65
|
+
debug(line: string): void;
|
|
66
|
+
info(line: string): void;
|
|
67
|
+
warn(line: string): void;
|
|
68
|
+
error(line: string): void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Minimal structured logger port the forwarder speaks. */
|
|
72
|
+
export interface Logger {
|
|
73
|
+
debug(msg: string, fields?: Record<string, unknown>): void;
|
|
74
|
+
info(msg: string, fields?: Record<string, unknown>): void;
|
|
75
|
+
warn(msg: string, fields?: Record<string, unknown>): void;
|
|
76
|
+
error(msg: string, fields?: Record<string, unknown>): void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Structured logger that writes one JSON line per record.
|
|
81
|
+
*
|
|
82
|
+
* Output shape: `{ ts, level, msg, ...fields }`. Any field value that is an
|
|
83
|
+
* `Error` is serialized to `{ name, message, stack }`; all other values pass
|
|
84
|
+
* through unchanged (so non-Error errors surface as their string form).
|
|
85
|
+
*/
|
|
86
|
+
export class ConsoleLogger implements Logger {
|
|
87
|
+
private readonly minLevel: LogLevel;
|
|
88
|
+
private readonly sinks: LoggerSinks;
|
|
89
|
+
|
|
90
|
+
constructor(sinks?: LoggerSinks, minLevel: LogLevel = LogLevel.Debug) {
|
|
91
|
+
this.sinks = sinks ?? globalConsoleSinks();
|
|
92
|
+
this.minLevel = minLevel;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
debug(msg: string, fields?: Record<string, unknown>): void {
|
|
96
|
+
this.write(LogLevel.Debug, msg, fields);
|
|
97
|
+
}
|
|
98
|
+
info(msg: string, fields?: Record<string, unknown>): void {
|
|
99
|
+
this.write(LogLevel.Info, msg, fields);
|
|
100
|
+
}
|
|
101
|
+
warn(msg: string, fields?: Record<string, unknown>): void {
|
|
102
|
+
this.write(LogLevel.Warn, msg, fields);
|
|
103
|
+
}
|
|
104
|
+
error(msg: string, fields?: Record<string, unknown>): void {
|
|
105
|
+
this.write(LogLevel.Error, msg, fields);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private write(level: LogLevel, msg: string, fields?: Record<string, unknown>): void {
|
|
109
|
+
if (level < this.minLevel) return;
|
|
110
|
+
const payload: Record<string, unknown> = {
|
|
111
|
+
ts: new Date().toISOString(),
|
|
112
|
+
level: logLevelLabel(level),
|
|
113
|
+
msg,
|
|
114
|
+
};
|
|
115
|
+
if (fields !== undefined) {
|
|
116
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
117
|
+
payload[key] = value instanceof Error ? serializeError(value) : value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const line = JSON.stringify(payload);
|
|
121
|
+
switch (level) {
|
|
122
|
+
case LogLevel.Debug:
|
|
123
|
+
this.sinks.debug(line);
|
|
124
|
+
return;
|
|
125
|
+
case LogLevel.Info:
|
|
126
|
+
this.sinks.info(line);
|
|
127
|
+
return;
|
|
128
|
+
case LogLevel.Warn:
|
|
129
|
+
this.sinks.warn(line);
|
|
130
|
+
return;
|
|
131
|
+
case LogLevel.Error:
|
|
132
|
+
this.sinks.error(line);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Returns the global `console` mapped onto {@link LoggerSinks}. */
|
|
139
|
+
function globalConsoleSinks(): LoggerSinks {
|
|
140
|
+
const c = (globalThis as { console?: Partial<Console> }).console;
|
|
141
|
+
if (c === undefined) {
|
|
142
|
+
const drop = (): void => {};
|
|
143
|
+
return { debug: drop, info: drop, warn: drop, error: drop };
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
debug: (l) => c.debug?.(l),
|
|
147
|
+
info: (l) => c.info?.(l),
|
|
148
|
+
warn: (l) => c.warn?.(l),
|
|
149
|
+
error: (l) => c.error?.(l),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function serializeError(err: Error): { name: string; message: string; stack: string | undefined } {
|
|
154
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
155
|
+
}
|