@evident-ai/runner-cdk 0.1.1-dev.da88f8e → 3.4.1-dev.0ef5061
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -14
- package/dist/controller-lambda/handler.js +14 -8
- package/dist/evident-scale-to-zero-construct.d.ts +16 -4
- package/dist/evident-scale-to-zero-construct.js +19 -14
- package/dist/image-version-reporter-lambda/handler.js +129 -0
- package/dist/microvm/constants.js +2 -3
- package/dist/microvm/construct.d.ts +34 -2
- package/dist/microvm/construct.js +38 -0
- package/dist/microvm/controller/handle-doorbell.js +11 -8
- package/dist/microvm/controller/microvm-client.d.ts +7 -1
- package/dist/microvm/controller/shape-catalogue.d.ts +1 -1
- package/dist/microvm/controller/shape-catalogue.js +1 -1
- package/dist/microvm/image-version-reporter/construct.d.ts +35 -0
- package/dist/microvm/image-version-reporter/construct.js +91 -0
- package/dist/microvm/image-version-reporter/handler.d.ts +26 -0
- package/dist/microvm/image-version-reporter/handler.js +104 -0
- package/dist/microvm-image-context/Dockerfile +10 -7
- package/dist/microvm-image-context/hook-server.js +2 -6
- package/dist/microvm-image-context/hooks/common.sh +247 -545
- package/dist/microvm-image-context/hooks/resume +9 -10
- package/dist/microvm-image-context/hooks/run +16 -33
- package/dist/microvm-image-context/hooks/suspend +3 -0
- package/dist/microvm-image-context/hooks/terminate +3 -0
- package/dist/waker/construct.js +1 -1
- package/package.json +10 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses the `MICROVM_SHAPES` environment variable (a JSON array, written by
|
|
3
|
-
* CloudFormation from `
|
|
3
|
+
* CloudFormation from `aws/runner-cdk/src/microvm/shapes.ts` at
|
|
4
4
|
* deploy time — see D6/D7 in the plan) into a `ShapeCatalogue` the pure
|
|
5
5
|
* decision core can query, without that core ever touching `process.env`
|
|
6
6
|
* itself.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Parses the `MICROVM_SHAPES` environment variable (a JSON array, written by
|
|
4
|
-
* CloudFormation from `
|
|
4
|
+
* CloudFormation from `aws/runner-cdk/src/microvm/shapes.ts` at
|
|
5
5
|
* deploy time — see D6/D7 in the plan) into a `ShapeCatalogue` the pure
|
|
6
6
|
* decision core can query, without that core ever touching `process.env`
|
|
7
7
|
* itself.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Construct } from 'constructs';
|
|
2
|
+
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
|
|
3
|
+
import type { MicrovmImageBuildInputs } from '@evident-ai/lambda-microvm-cdk';
|
|
4
|
+
export type ImageVersionReporterProps = {
|
|
5
|
+
/** The secret whose value is the same HMAC key the controller verifies every doorbell against. */
|
|
6
|
+
doorbellSecret: secretsmanager.ISecret;
|
|
7
|
+
/** Base URL of Evident's own API — non-secret. */
|
|
8
|
+
evidentApiUrl: string;
|
|
9
|
+
/** The pool runner id the report is filed against — non-secret, public UUID. */
|
|
10
|
+
evidentRunnerId: string;
|
|
11
|
+
/** Shape name -> the `Fn::GetAtt LatestActiveImageVersion` published this deploy. */
|
|
12
|
+
imageVersions: Record<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* Shape name -> {@link MicrovmImageBuildInputs}, a STRUCTURED object (never
|
|
15
|
+
* a delimiter-joined string — see its docstring for why flattening is
|
|
16
|
+
* unsafe) that changes whenever THIS deploy's build inputs change. Unlike
|
|
17
|
+
* `imageVersions`' `Fn::GetAtt` (a runtime attribute of the MicroVM image
|
|
18
|
+
* service, whose deploy-time propagation to a dependent resource this
|
|
19
|
+
* construct cannot itself guarantee), every field here is either a
|
|
20
|
+
* synth-time-computed literal or a plain `Ref`/`Fn::GetAtt` to a resource
|
|
21
|
+
* IN THIS STACK — CloudFormation diffs the full resolved property tree, so
|
|
22
|
+
* a change anywhere in this object is a certain, not merely likely,
|
|
23
|
+
* re-invocation trigger.
|
|
24
|
+
*/
|
|
25
|
+
buildTriggers: Record<string, MicrovmImageBuildInputs>;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Deploy-time push: reports each shape's just-published MicroVM image version to
|
|
29
|
+
* Evident, once per deploy that actually changes it. `imageVersions` is the
|
|
30
|
+
* payload; `buildTriggers` is what guarantees re-invocation on exactly the
|
|
31
|
+
* deploy that matters, independent of `imageVersions`' own resolution timing.
|
|
32
|
+
*/
|
|
33
|
+
export declare class ImageVersionReporter extends Construct {
|
|
34
|
+
constructor(scope: Construct, id: string, props: ImageVersionReporterProps);
|
|
35
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ImageVersionReporter = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const cdk = __importStar(require("aws-cdk-lib"));
|
|
39
|
+
const constructs_1 = require("constructs");
|
|
40
|
+
const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
|
|
41
|
+
const cr = __importStar(require("aws-cdk-lib/custom-resources"));
|
|
42
|
+
/** The JSON field name inside the doorbell secret, same key the controller reads. */
|
|
43
|
+
const DOORBELL_SECRET_KEY = 'DOORBELL_SECRET';
|
|
44
|
+
/**
|
|
45
|
+
* Deploy-time push: reports each shape's just-published MicroVM image version to
|
|
46
|
+
* Evident, once per deploy that actually changes it. `imageVersions` is the
|
|
47
|
+
* payload; `buildTriggers` is what guarantees re-invocation on exactly the
|
|
48
|
+
* deploy that matters, independent of `imageVersions`' own resolution timing.
|
|
49
|
+
*/
|
|
50
|
+
class ImageVersionReporter extends constructs_1.Construct {
|
|
51
|
+
constructor(scope, id, props) {
|
|
52
|
+
super(scope, id);
|
|
53
|
+
const handler = new lambda.Function(this, 'Function', {
|
|
54
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
55
|
+
// Pre-bundled at PACKAGE build time, same reasoning as the waker's and the
|
|
56
|
+
// controller's own `Code.fromAsset` (see their comments) — resolved from
|
|
57
|
+
// the PACKAGE ROOT, two directories below this file either compiled
|
|
58
|
+
// (`dist/microvm/image-version-reporter`) or via ts-node
|
|
59
|
+
// (`src/microvm/image-version-reporter`).
|
|
60
|
+
code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', '..', 'dist', 'image-version-reporter-lambda')),
|
|
61
|
+
handler: 'handler.handler',
|
|
62
|
+
memorySize: 128,
|
|
63
|
+
// A deploy hook must not hold CloudFormation for its default 1 h — this is
|
|
64
|
+
// the hard stop; the handler's own internal REPORT_TIMEOUT_MS is well under it.
|
|
65
|
+
timeout: cdk.Duration.seconds(30),
|
|
66
|
+
environment: {
|
|
67
|
+
// ARN + field name only, never the value (mirrors the controller and waker).
|
|
68
|
+
DOORBELL_SECRET_ARN: props.doorbellSecret.secretArn,
|
|
69
|
+
DOORBELL_SECRET_KEY: DOORBELL_SECRET_KEY,
|
|
70
|
+
EVIDENT_API_URL: props.evidentApiUrl,
|
|
71
|
+
EVIDENT_RUNNER_ID: props.evidentRunnerId,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
props.doorbellSecret.grantRead(handler);
|
|
75
|
+
const provider = new cr.Provider(this, 'Provider', {
|
|
76
|
+
onEventHandler: handler,
|
|
77
|
+
});
|
|
78
|
+
new cdk.CustomResource(this, 'Resource', {
|
|
79
|
+
serviceToken: provider.serviceToken,
|
|
80
|
+
properties: {
|
|
81
|
+
imageVersions: props.imageVersions,
|
|
82
|
+
// Not read by the handler — its only job is to be a property value
|
|
83
|
+
// CloudFormation can see change (a structured object, so any field
|
|
84
|
+
// changing anywhere is a real property diff), so the resource is
|
|
85
|
+
// invoked on exactly the deploy that changes a build input.
|
|
86
|
+
buildTriggers: props.buildTriggers,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.ImageVersionReporter = ImageVersionReporter;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ImageVersionPair = {
|
|
2
|
+
shape: string;
|
|
3
|
+
image_version: string;
|
|
4
|
+
};
|
|
5
|
+
export type ReporterEvent = {
|
|
6
|
+
RequestType: 'Create' | 'Update' | 'Delete';
|
|
7
|
+
ResourceProperties?: {
|
|
8
|
+
imageVersions?: unknown;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export type ReporterConfig = {
|
|
12
|
+
doorbellSecretArn: string;
|
|
13
|
+
doorbellSecretKey: string;
|
|
14
|
+
evidentApiUrl: string;
|
|
15
|
+
evidentRunnerId: string;
|
|
16
|
+
};
|
|
17
|
+
type ReporterDependencies = {
|
|
18
|
+
getSecret: (secretArn: string, secretKey: string) => Promise<string>;
|
|
19
|
+
fetch: typeof fetch;
|
|
20
|
+
};
|
|
21
|
+
type ReporterResponse = {
|
|
22
|
+
PhysicalResourceId: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function handleImageVersionReport(event: ReporterEvent, getConfig: () => ReporterConfig, dependencies?: ReporterDependencies): Promise<ReporterResponse>;
|
|
25
|
+
export declare const handler: (event: ReporterEvent) => Promise<ReporterResponse>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handler = void 0;
|
|
4
|
+
exports.handleImageVersionReport = handleImageVersionReport;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
|
|
7
|
+
const PHYSICAL_RESOURCE_ID = 'evident-image-version-report';
|
|
8
|
+
const REPORT_TIMEOUT_MS = 5_000;
|
|
9
|
+
const secrets = new client_secrets_manager_1.SecretsManagerClient({});
|
|
10
|
+
async function fetchDoorbellSecret(secretArn, secretKey) {
|
|
11
|
+
const { SecretString } = await secrets.send(new client_secrets_manager_1.GetSecretValueCommand({ SecretId: secretArn }));
|
|
12
|
+
if (!SecretString) {
|
|
13
|
+
throw new Error(`doorbell secret ${secretArn} has no SecretString`);
|
|
14
|
+
}
|
|
15
|
+
const parsed = JSON.parse(SecretString);
|
|
16
|
+
const value = parsed !== null && typeof parsed === 'object'
|
|
17
|
+
? parsed[secretKey]
|
|
18
|
+
: undefined;
|
|
19
|
+
if (typeof value !== 'string' || value === '') {
|
|
20
|
+
throw new Error(`doorbell secret ${secretArn} is missing a non-empty '${secretKey}' field`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function requireEnv(name) {
|
|
25
|
+
const value = process.env[name];
|
|
26
|
+
if (!value) {
|
|
27
|
+
throw new Error(`missing required env var ${name}`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function readImageVersions(value) {
|
|
32
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
33
|
+
throw new Error('imageVersions must be an object');
|
|
34
|
+
}
|
|
35
|
+
return Object.entries(value).map(([shape, imageVersion]) => {
|
|
36
|
+
if (typeof imageVersion !== 'string') {
|
|
37
|
+
throw new Error(`imageVersions.${shape} must be a string`);
|
|
38
|
+
}
|
|
39
|
+
return { shape, image_version: imageVersion };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function response() {
|
|
43
|
+
return { PhysicalResourceId: PHYSICAL_RESOURCE_ID };
|
|
44
|
+
}
|
|
45
|
+
const runtimeDependencies = {
|
|
46
|
+
getSecret: fetchDoorbellSecret,
|
|
47
|
+
fetch: globalThis.fetch,
|
|
48
|
+
};
|
|
49
|
+
async function handleImageVersionReport(event, getConfig, dependencies = runtimeDependencies) {
|
|
50
|
+
if (event.RequestType === 'Delete') {
|
|
51
|
+
return response();
|
|
52
|
+
}
|
|
53
|
+
let runnerId = 'unknown';
|
|
54
|
+
let versions = [];
|
|
55
|
+
let responseStatus;
|
|
56
|
+
try {
|
|
57
|
+
versions = readImageVersions(event.ResourceProperties?.imageVersions);
|
|
58
|
+
const config = getConfig();
|
|
59
|
+
runnerId = config.evidentRunnerId;
|
|
60
|
+
const secret = await dependencies.getSecret(config.doorbellSecretArn, config.doorbellSecretKey);
|
|
61
|
+
const body = JSON.stringify({
|
|
62
|
+
type: 'runner.microvm_image_versions_reported',
|
|
63
|
+
versions,
|
|
64
|
+
});
|
|
65
|
+
const signature = (0, node_crypto_1.createHmac)('sha256', secret).update(body).digest('hex');
|
|
66
|
+
const url = `${config.evidentApiUrl.replace(/\/+$/, '')}/v1/runners/${encodeURIComponent(runnerId)}/microvm-image-versions`;
|
|
67
|
+
const result = await dependencies.fetch(url, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: {
|
|
70
|
+
'content-type': 'application/json',
|
|
71
|
+
'x-evident-signature': signature,
|
|
72
|
+
},
|
|
73
|
+
body,
|
|
74
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS),
|
|
75
|
+
});
|
|
76
|
+
responseStatus = result.status;
|
|
77
|
+
console.log('runner.microvm_image_versions_reported', {
|
|
78
|
+
runnerId,
|
|
79
|
+
versions,
|
|
80
|
+
status: result.status,
|
|
81
|
+
});
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
throw new Error(`Evident returned HTTP ${result.status}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
+
console.warn('MicroVM image version report failed', {
|
|
89
|
+
operation: event.RequestType,
|
|
90
|
+
runnerId,
|
|
91
|
+
versions,
|
|
92
|
+
...(responseStatus === undefined ? {} : { status: responseStatus }),
|
|
93
|
+
error: message,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return response();
|
|
97
|
+
}
|
|
98
|
+
const handler = (event) => handleImageVersionReport(event, () => ({
|
|
99
|
+
doorbellSecretArn: requireEnv('DOORBELL_SECRET_ARN'),
|
|
100
|
+
doorbellSecretKey: requireEnv('DOORBELL_SECRET_KEY'),
|
|
101
|
+
evidentApiUrl: requireEnv('EVIDENT_API_URL'),
|
|
102
|
+
evidentRunnerId: requireEnv('EVIDENT_RUNNER_ID'),
|
|
103
|
+
}));
|
|
104
|
+
exports.handler = handler;
|
|
@@ -37,7 +37,7 @@ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
|
|
37
37
|
# binary assertion further down fails the build if any required binary is gone.
|
|
38
38
|
RUN apt-get update \
|
|
39
39
|
&& apt-get install -y --no-install-recommends \
|
|
40
|
-
ca-certificates curl git jq less procps ripgrep \
|
|
40
|
+
ca-certificates curl git jq less procps psmisc ripgrep \
|
|
41
41
|
postgresql postgresql-contrib direnv \
|
|
42
42
|
gnupg unzip \
|
|
43
43
|
# --- GitHub CLI (`gh`) from the official apt repo ---
|
|
@@ -71,8 +71,8 @@ RUN apt-get update \
|
|
|
71
71
|
# restore` attempt — then starts a backgrounded `litestream replicate` once
|
|
72
72
|
# opencode has opened the DB (WI-3); `/suspend`/`/resume`/`/terminate` flush,
|
|
73
73
|
# restart and stop it in turn (WI-4). Do not remove as dead code.
|
|
74
|
-
ARG LITESTREAM_VERSION=0.5.
|
|
75
|
-
ARG LITESTREAM_SHA256=
|
|
74
|
+
ARG LITESTREAM_VERSION=0.5.16
|
|
75
|
+
ARG LITESTREAM_SHA256=678022e4103145302598e35d37f8718392d42e153feeb1e2d4a64dd0cd3aaf10
|
|
76
76
|
RUN curl -fsSL -o /tmp/litestream.tar.gz \
|
|
77
77
|
"https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-${LITESTREAM_VERSION}-linux-arm64.tar.gz" \
|
|
78
78
|
&& echo "${LITESTREAM_SHA256} /tmp/litestream.tar.gz" | sha256sum -c - \
|
|
@@ -171,10 +171,10 @@ ENV HOME=/home/runner
|
|
|
171
171
|
# permanent no-op and every VM from this snapshot shares one machine id. Empty
|
|
172
172
|
# is the correct unset state — it is the absence of an identity, so nothing
|
|
173
173
|
# per-VM-unique enters the shared snapshot.
|
|
174
|
-
RUN mkdir -p /var/lib/runner-pg /var/lib/dbus \
|
|
174
|
+
RUN mkdir -p /var/lib/runner-pg /var/lib/dbus /home/runner/.local/state/evident \
|
|
175
175
|
&& install -o runner -g runner -m 0644 /dev/null /etc/machine-id \
|
|
176
176
|
&& install -o runner -g runner -m 0644 /dev/null /var/lib/dbus/machine-id \
|
|
177
|
-
|
|
177
|
+
&& chown -R runner:runner /var/lib/runner-pg /home/runner
|
|
178
178
|
COPY --chown=10001:10001 repo ${WORKSPACE}
|
|
179
179
|
WORKDIR ${WORKSPACE}
|
|
180
180
|
USER runner
|
|
@@ -204,9 +204,12 @@ RUN git reset --quiet
|
|
|
204
204
|
# database stay per-VM (see entrypoint.sh's pre-warm in evident-runner).
|
|
205
205
|
RUN if [ -f pnpm-lock.yaml ]; then \
|
|
206
206
|
pnpm install --frozen-lockfile \
|
|
207
|
-
&& pnpm run build --filter='./packages/*'
|
|
207
|
+
&& pnpm run build --filter='./packages/*' \
|
|
208
|
+
--filter='./aws/runner-cdk' \
|
|
209
|
+
--filter='./aws/lambda-microvm-cdk' \
|
|
210
|
+
--filter='./aws/lambda-microvm-runtime'; \
|
|
208
211
|
else \
|
|
209
|
-
echo "[workspace-prep] no pnpm-lock.yaml in the baked repository — skipping the dependency install and the
|
|
212
|
+
echo "[workspace-prep] no pnpm-lock.yaml in the baked repository — skipping the dependency install and the workspace build; the agent installs on first use."; \
|
|
210
213
|
fi
|
|
211
214
|
|
|
212
215
|
# Data dir for the local dev Postgres cluster; listens on 5433 to match this
|
|
@@ -275,14 +275,10 @@ var require_dist = __commonJS({
|
|
|
275
275
|
}
|
|
276
276
|
});
|
|
277
277
|
|
|
278
|
-
//
|
|
278
|
+
// ../../runner/docker-images/microvm/hook-server.ts
|
|
279
279
|
var import_lambda_microvm_runtime = __toESM(require_dist());
|
|
280
|
-
|
|
281
|
-
// src/microvm/constants.ts
|
|
282
|
-
var HOOKS_PORT = 8080;
|
|
283
280
|
var HOOKS_DIR = "/etc/evident/hooks";
|
|
284
|
-
|
|
285
|
-
// src/microvm/image/hook-server.ts
|
|
281
|
+
var HOOKS_PORT = 8080;
|
|
286
282
|
var server = (0, import_lambda_microvm_runtime.createNodeRuntime)({ hooksDir: HOOKS_DIR });
|
|
287
283
|
var port = Number(process.env.HOOKS_PORT ?? HOOKS_PORT);
|
|
288
284
|
server.listen(port, "0.0.0.0", () => {
|