@bongos/core 1.19.82 → 1.19.84

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.
@@ -0,0 +1,420 @@
1
+ // modules/provisioning/tests/liveness.mjs
2
+ //
3
+ // The VERIFICATION OVERLAY (task 1002696; docs/design/projects-surfaces-redesign-
4
+ // direction.md §3): the heartbeat sweep that probes every active instance's
5
+ // /healthz and records what it SAW, and the API exposure of the two columns it
6
+ // writes. Covers:
7
+ // • healthzUrl / formatSweepLine — the pure probe-target + disposition-line rules
8
+ // • publicInstance — never-probed (null) stays distinct from probed-and-down
9
+ // • sweepOnce — an answering instance is up, a refusing/timing-out one is down,
10
+ // both columns are written for both, and `status` is NEVER touched
11
+ // • GET /provisioning/instances + /:id — the two fields ride the API
12
+ // • POST /provisioning/instances — an 'error' instance re-enqueues (Try again)
13
+ //
14
+ // DB-free: `db` is a recording fake and the data-access fns the ROUTES use are
15
+ // stubbed on the cached module object (the tests/provisioning_domain_attach.mjs
16
+ // pattern). The only network is loopback — ephemeral servers this file starts and
17
+ // stops itself, so a probe result is real rather than mocked.
18
+ //
19
+ // Run standalone: node modules/provisioning/tests/liveness.mjs
20
+
21
+ import { strict as assert } from 'node:assert';
22
+ import { createRequire } from 'node:module';
23
+ import http from 'node:http';
24
+
25
+ const require = createRequire(import.meta.url);
26
+ const provisioning = require('../provisioning.js');
27
+ const liveness = require('../pollers/liveness-sweep.js');
28
+
29
+ let passed = 0, failed = 0;
30
+ function t(name, fn) {
31
+ try { fn(); passed++; console.log(` PASS ${name}`); }
32
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
33
+ }
34
+ async function ta(name, fn) {
35
+ try { await fn(); passed++; console.log(` PASS ${name}`); }
36
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
37
+ }
38
+
39
+ function listen(server) {
40
+ return new Promise((r) => server.listen(0, '127.0.0.1', r));
41
+ }
42
+ // Destroy keep-alive sockets before closing — fetch's connection pool would
43
+ // otherwise hold them open past the last assertion and stall the exit.
44
+ function closeServer(server) {
45
+ if (server.closeAllConnections) server.closeAllConnections();
46
+ server.close();
47
+ }
48
+ // A port nothing is listening on — bind one, then give it back.
49
+ async function closedPort() {
50
+ const s = http.createServer();
51
+ await listen(s);
52
+ const p = s.address().port;
53
+ await new Promise((r) => s.close(r));
54
+ return p;
55
+ }
56
+ // A fake db that answers the sweep's two statements and records every write.
57
+ function fakeDb(rows) {
58
+ const updates = [];
59
+ return {
60
+ updates,
61
+ query: async (sql, params) => {
62
+ if (/SELECT id, slug, domain, port FROM provisioning_instances/.test(sql)) return { rows };
63
+ if (/UPDATE provisioning_instances/.test(sql)) {
64
+ updates.push({ sql, id: params[0], up: params[1] });
65
+ return { rows: [{ id: params[0], last_verified_at: new Date(), last_verified_up: params[1] }] };
66
+ }
67
+ throw new Error(`unexpected SQL: ${sql}`);
68
+ },
69
+ };
70
+ }
71
+
72
+ console.log('healthzUrl (where the heartbeat knocks):');
73
+
74
+ t('prefers the public address (https), falls back to the recorded loopback port', () => {
75
+ assert.equal(liveness.healthzUrl({ domain: 'Mercury.Example.com.', port: 3101 }), 'https://mercury.example.com/healthz');
76
+ assert.equal(liveness.healthzUrl({ domain: null, port: 3101 }), 'http://127.0.0.1:3101/healthz');
77
+ });
78
+
79
+ t('an unaddressed, portless (or junk-addressed) instance is unprobeable — null, never a guessed URL', () => {
80
+ assert.equal(liveness.healthzUrl({ domain: null, port: null }), null);
81
+ assert.equal(liveness.healthzUrl({ domain: 'not a host', port: 0 }), null);
82
+ assert.equal(liveness.healthzUrl({ domain: 'com', port: 99999 }), null); // bare TLD + out-of-range port
83
+ });
84
+
85
+ console.log('\nformatSweepLine (the one-line per-sweep account):');
86
+
87
+ t('renders the disposition line, naming each down instance and its reason', () => {
88
+ assert.equal(
89
+ liveness.formatSweepLine({ swept: 6, up: 4, down: 2, reasons: ['mercury:ECONNREFUSED', 'swag:timeout'] }),
90
+ '[liveness] swept 6 active: 4 up · 2 down (mercury:ECONNREFUSED, swag:timeout)'
91
+ );
92
+ });
93
+
94
+ t('an all-green sweep carries no parenthetical; unaddressed instances are counted separately', () => {
95
+ assert.equal(liveness.formatSweepLine({ swept: 3, up: 3, down: 0 }), '[liveness] swept 3 active: 3 up · 0 down');
96
+ assert.equal(
97
+ liveness.formatSweepLine({ swept: 4, up: 3, down: 0, unprobeable: 1 }),
98
+ '[liveness] swept 4 active: 3 up · 0 down · 1 unaddressed'
99
+ );
100
+ });
101
+
102
+ console.log('\npublicInstance (the overlay projection):');
103
+
104
+ t('never-probed (null) stays distinct from probed-and-down (false)', () => {
105
+ const never = provisioning.publicInstance({ id: 1, slug: 'a', status: 'active' });
106
+ assert.equal(never.last_verified_at, null);
107
+ assert.equal(never.last_verified_up, null, 'unverified is stated as unverified, never as down');
108
+ const down = provisioning.publicInstance({ id: 2, slug: 'b', status: 'active', last_verified_at: '2026-08-12T10:00:00Z', last_verified_up: false });
109
+ assert.equal(down.last_verified_at, '2026-08-12T10:00:00Z');
110
+ assert.equal(down.last_verified_up, false);
111
+ });
112
+
113
+ console.log('\nsweepOnce (real loopback probes, recording fake db):');
114
+
115
+ await ta('an answering instance is UP and a refusing one is DOWN — both columns written, status never touched', async () => {
116
+ const server = http.createServer((req, res) => {
117
+ if (req.url === '/healthz') { res.writeHead(200, { 'content-type': 'application/json' }); res.end('{"ok":true}'); return; }
118
+ res.writeHead(404); res.end();
119
+ });
120
+ await listen(server);
121
+ const rows = [
122
+ { id: 1, slug: 'mercury', domain: null, port: server.address().port },
123
+ { id: 2, slug: 'swag', domain: null, port: await closedPort() },
124
+ ];
125
+ const db = fakeDb(rows);
126
+ const lines = [];
127
+ try {
128
+ const out = await liveness.sweepOnce({ pool: db, log: (l) => lines.push(l), timeoutMs: 3000 });
129
+ assert.deepEqual(
130
+ { swept: out.swept, up: out.up, down: out.down, unprobeable: out.unprobeable },
131
+ { swept: 2, up: 1, down: 1, unprobeable: 0 }
132
+ );
133
+ assert.deepEqual(db.updates.map((u) => [u.id, u.up]).sort(), [[1, true], [2, false]]);
134
+ for (const u of db.updates) {
135
+ assert.match(u.sql, /SET last_verified_at = now\(\), last_verified_up = \$2/);
136
+ assert.ok(!/status/.test(u.sql), 'verification is an overlay — the sweep must never write status');
137
+ }
138
+ assert.match(lines[0], /^\[liveness\] swept 2 active: 1 up · 1 down \(swag:/);
139
+ } finally {
140
+ closeServer(server);
141
+ }
142
+ });
143
+
144
+ await ta('a hung instance times out (down), and a non-2xx answer is down too — not "live"', async () => {
145
+ const hung = http.createServer(() => { /* never responds */ });
146
+ const sick = http.createServer((_req, res) => { res.writeHead(503); res.end(); });
147
+ await listen(hung);
148
+ await listen(sick);
149
+ const rows = [
150
+ { id: 3, slug: 'hung', domain: null, port: hung.address().port },
151
+ { id: 4, slug: 'sick', domain: null, port: sick.address().port },
152
+ ];
153
+ const db = fakeDb(rows);
154
+ try {
155
+ const out = await liveness.sweepOnce({ pool: db, log: () => {}, timeoutMs: 300 });
156
+ assert.equal(out.up, 0);
157
+ assert.equal(out.down, 2);
158
+ assert.deepEqual(out.reasons.sort(), ['hung:timeout', 'sick:http 503']);
159
+ assert.deepEqual(db.updates.map((u) => u.up), [false, false]);
160
+ } finally {
161
+ closeServer(hung);
162
+ closeServer(sick);
163
+ }
164
+ });
165
+
166
+ await ta('an unaddressed active instance is left alone — no verdict is recorded for a probe never made', async () => {
167
+ const db = fakeDb([{ id: 5, slug: 'newborn', domain: null, port: null }]);
168
+ const lines = [];
169
+ const out = await liveness.sweepOnce({ pool: db, log: (l) => lines.push(l) });
170
+ assert.deepEqual({ swept: out.swept, up: out.up, down: out.down, unprobeable: out.unprobeable },
171
+ { swept: 1, up: 0, down: 0, unprobeable: 1 });
172
+ assert.equal(db.updates.length, 0, 'nothing written');
173
+ assert.equal(lines[0], '[liveness] swept 1 active: 0 up · 0 down · 1 unaddressed');
174
+ });
175
+
176
+ await ta('a control plane with no active instances sweeps silently (no 5-minute empty account)', async () => {
177
+ const db = fakeDb([]);
178
+ const lines = [];
179
+ const out = await liveness.sweepOnce({ pool: db, log: (l) => lines.push(l) });
180
+ assert.equal(out.swept, 0);
181
+ assert.equal(lines.length, 0);
182
+ });
183
+
184
+ console.log('\nSSRF gate (domain probes never reach private/reserved networks):');
185
+
186
+ t('private/reserved addresses are recognized; public ones are not', () => {
187
+ for (const bad of ['127.0.0.1', '10.1.2.3', '172.16.0.9', '172.31.255.1', '192.168.1.1',
188
+ '169.254.169.254', '100.64.0.1', '198.18.0.1', '0.0.0.0', '::1', '::',
189
+ 'fe80::1', 'fd00::5', '::ffff:10.0.0.5', 'garbage']) {
190
+ assert.equal(liveness.isPrivateAddress(bad), true, `${bad} must be private/refused`);
191
+ }
192
+ for (const ok of ['93.184.216.34', '161.35.132.201', '2606:2800:220:1:248:1893:25c8:1946']) {
193
+ assert.equal(liveness.isPrivateAddress(ok), false, `${ok} must be public`);
194
+ }
195
+ });
196
+
197
+ await ta('an IP-literal domain (cloud metadata) is blocked — no lookup, no probe, no verdict', async () => {
198
+ const db = fakeDb([{ id: 6, slug: 'evil', domain: '169.254.169.254', port: null }]);
199
+ const lines = [];
200
+ const out = await liveness.sweepOnce({
201
+ pool: db,
202
+ log: (l) => lines.push(l),
203
+ lookup: async () => { throw new Error('lookup must not run for an IP literal'); },
204
+ fetchImpl: async () => { throw new Error('the probe must never fire'); },
205
+ });
206
+ assert.deepEqual({ blocked: out.blocked, down: out.down, up: out.up }, { blocked: 1, down: 0, up: 0 });
207
+ assert.equal(db.updates.length, 0, 'a blocked probe records NO verdict (no oracle)');
208
+ assert.match(lines[0], /1 blocked \(evil:ip-literal\)/);
209
+ });
210
+
211
+ await ta('a domain RESOLVING to a private address is blocked the same way', async () => {
212
+ const db = fakeDb([{ id: 7, slug: 'sneaky', domain: 'internal.example.com', port: null }]);
213
+ const out = await liveness.sweepOnce({
214
+ pool: db,
215
+ log: () => {},
216
+ lookup: async () => [{ address: '10.0.0.5', family: 4 }],
217
+ fetchImpl: async () => { throw new Error('the probe must never fire'); },
218
+ });
219
+ assert.equal(out.blocked, 1);
220
+ assert.deepEqual(out.blockedReasons, ['sneaky:private-address']);
221
+ assert.equal(db.updates.length, 0);
222
+ });
223
+
224
+ await ta('an unresolvable domain still probes and records an honest DOWN (the qa.test case)', async () => {
225
+ const db = fakeDb([{ id: 8, slug: 'ghostly', domain: 'ghostly.qa.test', port: null }]);
226
+ const enotfound = Object.assign(new Error('getaddrinfo ENOTFOUND'), { code: 'ENOTFOUND' });
227
+ const out = await liveness.sweepOnce({
228
+ pool: db,
229
+ log: () => {},
230
+ lookup: async () => { throw enotfound; },
231
+ fetchImpl: async () => { throw Object.assign(new TypeError('fetch failed'), { cause: { code: 'ENOTFOUND' } }); },
232
+ });
233
+ assert.deepEqual({ blocked: out.blocked, down: out.down }, { blocked: 0, down: 1 });
234
+ assert.deepEqual(out.reasons, ['ghostly:ENOTFOUND']);
235
+ assert.deepEqual(db.updates.map((u) => u.up), [false], 'DNS-less domain = honest down, not a silent skip');
236
+ });
237
+
238
+ await ta('the gate hands back the address it vetted, so the probe can pin to it (no re-resolution)', async () => {
239
+ const gate = await liveness.domainProbeGate('mercury.example.com', {
240
+ lookup: async () => [{ address: '93.184.216.34', family: 4 }],
241
+ });
242
+ assert.equal(gate.allowed, true);
243
+ assert.deepEqual(gate.pin, { address: '93.184.216.34', family: 4 },
244
+ 'the vetted address rides back — DNS must not be consulted twice (rebinding TOCTOU)');
245
+ });
246
+
247
+ await ta('a rebinding nameserver cannot re-aim the probe — the pinned address is what gets connected', async () => {
248
+ // The gate sees a public address; a second resolution would return metadata.
249
+ // The probe must connect to the FIRST one. We assert by capturing what the
250
+ // dispatcher's pinned lookup resolves to.
251
+ let answered = 0;
252
+ const rebinding = async () => {
253
+ answered += 1;
254
+ return [{ address: answered === 1 ? '93.184.216.34' : '169.254.169.254', family: 4 }];
255
+ };
256
+ const gate = await liveness.domainProbeGate('rebind.example.com', { lookup: rebinding });
257
+ assert.equal(gate.allowed, true);
258
+ assert.equal(gate.pin.address, '93.184.216.34');
259
+ const connected = [];
260
+ await liveness.probeHealthz('https://rebind.example.com/healthz', {
261
+ timeoutMs: 500,
262
+ fetchImpl: async (_url, opts) => {
263
+ // stand in for undici: resolve through the dispatcher's pinned lookup
264
+ assert.ok(!opts.dispatcher, 'a stubbed fetch skips the real dispatcher');
265
+ connected.push(gate.pin.address);
266
+ throw Object.assign(new TypeError('fetch failed'), { cause: { code: 'ECONNREFUSED' } });
267
+ },
268
+ pin: gate.pin,
269
+ });
270
+ assert.deepEqual(connected, ['93.184.216.34'], 'the probe uses the vetted address, never the second answer');
271
+ assert.equal(answered, 1, 'exactly one DNS resolution happens for the whole probe');
272
+ });
273
+
274
+ await ta('the pin is load-bearing on the REAL client: same URL, the pinned address decides where it lands', async () => {
275
+ const server = http.createServer((req, res) => {
276
+ if (req.url === '/healthz') { res.writeHead(200); res.end('{"ok":true}'); return; }
277
+ res.writeHead(404); res.end();
278
+ });
279
+ await listen(server);
280
+ const url = `http://pinned.invalid:${server.address().port}/healthz`;
281
+ try {
282
+ // 'pinned.invalid' resolves nowhere; only the pin can make this connect.
283
+ const hit = await liveness.probeHealthz(url, { timeoutMs: 3000, pin: { address: '127.0.0.1', family: 4 } });
284
+ assert.deepEqual(hit, { up: true }, 'the pinned address is what gets connected');
285
+ const miss = await liveness.probeHealthz(url, { timeoutMs: 3000, pin: { address: '127.0.0.2', family: 4 } });
286
+ assert.equal(miss.up, false, 'a different pin cannot reach the same server — the pin, not DNS, decides');
287
+ } finally {
288
+ closeServer(server);
289
+ }
290
+ });
291
+
292
+ await ta('a redirecting healthz is DOWN — the probe never follows a redirect (SSRF pivot)', async () => {
293
+ const bouncer = http.createServer((_req, res) => {
294
+ res.writeHead(302, { location: 'http://169.254.169.254/latest/meta-data/' });
295
+ res.end();
296
+ });
297
+ await listen(bouncer);
298
+ try {
299
+ const probe = await liveness.probeHealthz(`http://127.0.0.1:${bouncer.address().port}/healthz`, { timeoutMs: 2000 });
300
+ assert.equal(probe.up, false);
301
+ assert.equal(probe.reason, 'http 302');
302
+ } finally {
303
+ closeServer(bouncer);
304
+ }
305
+ });
306
+
307
+ console.log('\nshape-aware onboard plan (direction §7 — no promises a shape cannot keep):');
308
+
309
+ t('runner-performed legs are absent from the self-host plan; control-plane shapes keep everything', () => {
310
+ const { onboardPlan, stepsForShape } = require('../onboard-plan.js');
311
+ const all = onboardPlan('greenfield');
312
+ const standalone = stepsForShape(all, 'standalone');
313
+ assert.equal(standalone.length, all.length, 'a control-plane shape gets the full sequence');
314
+ const selfHost = stepsForShape(all, 'self-host');
315
+ const selfKeys = selfHost.map((s) => s.key);
316
+ for (const runnerLeg of ['provision', 'oauth-place', 'dns-preflight']) {
317
+ assert.ok(!selfKeys.includes(runnerLeg), `${runnerLeg} assumes the platform runner — absent on self-host`);
318
+ }
319
+ for (const human of ['github-repo', 'oauth-app', 'first-sign-in', 'enter-hall']) {
320
+ assert.ok(selfKeys.includes(human), `${human} is the owner's own leg — present on every shape`);
321
+ }
322
+ assert.equal(stepsForShape(all, 'nonsense').length, all.length, 'unknown shape = unfiltered, never an empty plan');
323
+ });
324
+
325
+ // ── the routes, over a real mounted router (stubbed data access) ─────────────────
326
+ let instance = null;
327
+ let createResult = null;
328
+ const enqueueCalls = [];
329
+ provisioning.listInstancesForOwner = async () => (instance ? [instance] : []);
330
+ provisioning.getInstanceById = async (_db, id) => (instance ? { ...instance, id } : null);
331
+ provisioning.getOpenIntent = async () => null;
332
+ provisioning.createInstance = async () => createResult;
333
+ provisioning.enqueueIntent = async (_db, id, action) => {
334
+ enqueueCalls.push({ id, action });
335
+ return { intent: { id: 9, action, state: 'pending' }, created: true };
336
+ };
337
+ provisioning.recordEvent = async () => {};
338
+
339
+ const api = require('../../../src/module-api.js');
340
+ api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
341
+ api.requirePermission = () => (_req, _res, next) => next();
342
+
343
+ const express = require('express');
344
+ const provisioningRoutes = require('../routes/provisioning.js');
345
+ const app = express();
346
+ app.use(express.json());
347
+ app.use((_req, res, next) => {
348
+ res.fail = (code, opts, details) => {
349
+ const status = typeof opts === 'number' ? opts : (opts && opts.status) || 400;
350
+ res.status(status).json({ error: { code, ...(typeof opts === 'object' ? opts : {}), ...(details || {}) } });
351
+ };
352
+ next();
353
+ });
354
+ app.use('/api/bongos', provisioningRoutes());
355
+
356
+ const server = app.listen(0);
357
+ await new Promise((r) => server.once('listening', r));
358
+ const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
359
+
360
+ async function req(method, path, body) {
361
+ const res = await fetch(`${base}${path}`, {
362
+ method,
363
+ headers: body ? { 'content-type': 'application/json' } : {},
364
+ body: body ? JSON.stringify(body) : undefined,
365
+ });
366
+ const raw = await res.text();
367
+ let json = null;
368
+ try { json = JSON.parse(raw); } catch { /* non-JSON */ }
369
+ return { status: res.status, body: json, raw };
370
+ }
371
+
372
+ console.log('\nAPI exposure — the overlay rides the instance reads:');
373
+
374
+ await ta('GET /provisioning/instances carries last_verified_at + last_verified_up', async () => {
375
+ instance = {
376
+ id: 18, owner_builder_id: 7, slug: 'mercury', status: 'active', hosting_shape: 'standalone',
377
+ domain: 'mercury.cloudbongos.com', port: 3101,
378
+ last_verified_at: '2026-08-12T10:00:00.000Z', last_verified_up: true,
379
+ };
380
+ const res = await req('GET', '/provisioning/instances');
381
+ assert.equal(res.status, 200, res.raw);
382
+ const [row] = res.body.instances;
383
+ assert.equal(row.last_verified_at, '2026-08-12T10:00:00.000Z');
384
+ assert.equal(row.last_verified_up, true);
385
+ });
386
+
387
+ await ta('GET /provisioning/instances/:id carries them too — including the never-probed nulls', async () => {
388
+ instance = { id: 19, owner_builder_id: 7, slug: 'fresh', status: 'active', domain: null, port: 3102 };
389
+ const res = await req('GET', '/provisioning/instances/19');
390
+ assert.equal(res.status, 200, res.raw);
391
+ assert.ok('last_verified_at' in res.body.instance, 'the field is always present, even unset');
392
+ assert.equal(res.body.instance.last_verified_at, null);
393
+ assert.equal(res.body.instance.last_verified_up, null);
394
+ });
395
+
396
+ console.log('\n"Try again" — re-enqueue a snagged instance (redesign direction §5.3):');
397
+
398
+ t("PROVISION_ENQUEUE_FROM allows 'error' (the state a snagged card is in)", () => {
399
+ assert.ok(provisioning.PROVISION_ENQUEUE_FROM.has('error'));
400
+ });
401
+
402
+ await ta("POST /provisioning/instances on an 'error' instance re-enqueues a provision intent", async () => {
403
+ enqueueCalls.length = 0;
404
+ createResult = {
405
+ instance: { id: 20, owner_builder_id: 7, slug: 'snagged', status: 'error', hosting_shape: 'standalone', domain: null },
406
+ created: false,
407
+ };
408
+ const res = await req('POST', '/provisioning/instances', { slug: 'snagged' });
409
+ assert.equal(res.status, 200, res.raw);
410
+ assert.equal(res.body.queued, true, 'Try again actually re-enqueues');
411
+ assert.deepEqual(enqueueCalls, [{ id: 20, action: 'provision' }]);
412
+ });
413
+
414
+ server.close();
415
+
416
+ console.log(`\nprovisioning liveness overlay: ${passed} passed, ${failed} failed`);
417
+ // exitCode, not process.exit(): every server above is closed, so the loop drains
418
+ // on its own — and exiting hard while a socket handle is mid-close aborts node on
419
+ // Windows (libuv UV_HANDLE_CLOSING assertion).
420
+ process.exitCode = failed ? 1 : 0;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.82",
3
+ "version": "1.19.84",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.82",
9
+ "version": "1.19.84",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.82",
3
+ "version": "1.19.84",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",